taler-typescript-core

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

commit fdf0d7b8c6d363863760785f50c6d31741633c90
parent c7a779aeacd341a94a8bf4c81c0dbcb0dc1c7485
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Wed, 18 Mar 2026 15:41:54 -0300

fix #11176

Diffstat:
Mpackages/merchant-backoffice-ui/src/components/SolveMFA.tsx | 399++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mpackages/merchant-backoffice-ui/src/components/exception/QR.tsx | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mpackages/merchant-backoffice-ui/src/components/modal/index.tsx | 5+++--
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx | 20+++++++++++---------
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx | 10+++++-----
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx | 23+++++++++--------------
Mpackages/merchant-backoffice-ui/src/paths/login/index.tsx | 225+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/newAccount/index.tsx | 205++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx | 97+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mpackages/merchant-backoffice-ui/src/scss/main.scss | 8--------
Mpackages/taler-util/src/qr.ts | 4+++-
11 files changed, 564 insertions(+), 491 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx b/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx @@ -6,7 +6,7 @@ import { ChallengeResponse, HttpStatusCode, TalerErrorCode, - TanChannel + TanChannel, } from "@gnu-taler/taler-util"; import { ButtonBetterBulma, @@ -119,7 +119,6 @@ function RetransmissionCodeLimitExpiration({ ); } - function SolveChallenge({ challenge, expiration, @@ -300,125 +299,129 @@ function SolveChallenge({ <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> - <div class="modal-card" style={{ width: "100%", margin: 0 }}> - <header - class="modal-card-head" - style={{ border: "1px solid", borderBottom: 0 }} - > - <p class="modal-card-title"> - <i18n.Translate>Validation code sent.</i18n.Translate> - </p> - </header> - <FormProvider<Form> - name="settings" - errors={errors} - object={value} - valueHandler={valueHandler} - > - <section - class="modal-card-body" - style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + <div class="modal is-active" style={{ position: "initial" }}> + <div class="modal-card" style={{ width: "100%", margin: 0 }}> + <header + class="modal-card-head" + style={{ border: "1px solid", borderBottom: 0 }} > - <p> - {(function (): VNode { - switch (challenge.tan_channel) { - case TanChannel.SMS: - if (showFull[TanChannel.SMS]) { + <p class="modal-card-title"> + <i18n.Translate>Validation code sent.</i18n.Translate> + </p> + </header> + <FormProvider<Form> + name="settings" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <section + class="modal-card-body" + style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + > + <p> + {(function (): VNode { + switch (challenge.tan_channel) { + case TanChannel.SMS: + if (showFull[TanChannel.SMS]) { + return ( + <i18n.Translate> + The verification code sent to the phone " + <b>{showFull[TanChannel.SMS]}</b>" + </i18n.Translate> + ); + } return ( <i18n.Translate> - The verification code sent to the phone " - <b>{showFull[TanChannel.SMS]}</b>" + The verification code sent to the phone number + ending with "<b>{challenge.tan_info}</b>" </i18n.Translate> ); - } - return ( - <i18n.Translate> - The verification code sent to the phone number - ending with "<b>{challenge.tan_info}</b>" - </i18n.Translate> - ); - case TanChannel.EMAIL: - if (showFull[TanChannel.EMAIL]) { + case TanChannel.EMAIL: + if (showFull[TanChannel.EMAIL]) { + return ( + <i18n.Translate> + The verification code sent to the email address + "<b>{showFull[TanChannel.EMAIL]}</b>" + </i18n.Translate> + ); + } return ( <i18n.Translate> - The verification code sent to the email address " - <b>{showFull[TanChannel.EMAIL]}</b>" + The verification code sent to the email address + starting with "<b>{challenge.tan_info}</b>" </i18n.Translate> ); - } - return ( - <i18n.Translate> - The verification code sent to the email address - starting with "<b>{challenge.tan_info}</b>" - </i18n.Translate> - ); - } - })()} - </p> - - <InputCode<Form> - name="code" - key={codeKey} - label={i18n.str`Verification code`} - dashesIndex={[3]} - size={8} - focus={focus} - readonly={!tries.solvable || showExpired} - filter={(c) => { - const v = Number.parseInt(c, 10); - if (Number.isNaN(v) || v > 9 || v < 0) return undefined; - return String(v); - }} - /> - - {showExpired ? ( - <div style={{ display: "flex" }}> - <p - class="has-text-danger" - style={{ alignContent: "center", marginRight: 8 }} - > - <i18n.Translate>Code expired.</i18n.Translate> - </p> - </div> - ) : undefined} - <div> - <div style={{ display: "flex" }}> - <p style={{ alignContent: "center", marginRight: 8 }}> - <i18n.Translate>Didn't received the code?</i18n.Translate> - </p> - <ButtonBetterBulma - class="button" - type="button" - onClick={resend} - > - <i18n.Translate>Resend</i18n.Translate> - </ButtonBetterBulma> + } + })()} + </p> + + <InputCode<Form> + name="code" + key={codeKey} + label={i18n.str`Verification code`} + dashesIndex={[3]} + size={8} + focus={focus} + readonly={!tries.solvable || showExpired} + filter={(c) => { + const v = Number.parseInt(c, 10); + if (Number.isNaN(v) || v > 9 || v < 0) return undefined; + return String(v); + }} + /> + + {showExpired ? ( + <div style={{ display: "flex" }}> + <p + class="has-text-danger" + style={{ alignContent: "center", marginRight: 8 }} + > + <i18n.Translate>Code expired.</i18n.Translate> + </p> + </div> + ) : undefined} + <div> + <div style={{ display: "flex" }}> + <p style={{ alignContent: "center", marginRight: 8 }}> + <i18n.Translate> + Didn't received the code? + </i18n.Translate> + </p> + <ButtonBetterBulma + class="button" + type="button" + onClick={resend} + > + <i18n.Translate>Resend</i18n.Translate> + </ButtonBetterBulma> + </div> </div> - </div> - <RetransmissionCodeLimitExpiration - expiration={currentRetransmission} - /> - </section> - <footer - class="modal-card-foot " - style={{ - justifyContent: "space-between", - border: "1px solid", - borderTop: 0, - }} - > - <button class="button" type="button" onClick={onCancel}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - <ButtonBetterBulma - type="submit" - disabled={!tries.numTries} - onClick={verify} + <RetransmissionCodeLimitExpiration + expiration={currentRetransmission} + /> + </section> + <footer + class="modal-card-foot " + style={{ + justifyContent: "space-between", + border: "1px solid", + borderTop: 0, + }} > - <i18n.Translate>Verify</i18n.Translate> - </ButtonBetterBulma> - </footer> - </FormProvider> + <button class="button" type="button" onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <ButtonBetterBulma + type="submit" + disabled={!tries.numTries} + onClick={verify} + > + <i18n.Translate>Verify</i18n.Translate> + </ButtonBetterBulma> + </footer> + </FormProvider> + </div> </div> </div> </div> @@ -548,95 +551,101 @@ export function SolveMFAChallenges({ <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> - <div class="modal-card" style={{ width: "100%", margin: 0 }}> - <header - class="modal-card-head" - style={{ border: "1px solid", borderBottom: 0 }} - > - <p class="modal-card-title"> - <i18n.Translate> - Multi-factor authentication required. - </i18n.Translate> - </p> - </header> - <section - class="modal-card-body" - style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} - > - {currentChallenge.combi_and ? ( - <i18n.Translate> - You must complete all of these requirements. - </i18n.Translate> - ) : ( - <i18n.Translate> - You need to complete at least one of this requirements. - </i18n.Translate> - )} - </section> - {currentChallenge.challenges.map((challenge, idx) => { - const noNeedToComplete = - hasSolvedEnough || - solved.indexOf(challenge.challenge_id) !== -1; - - const doSend = noNeedToComplete - ? sendMessage - : sendMessage.withArgs(challenge); - - return ( - <section - class="modal-card-body" - style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} - > - {(function (ch: TanChannel): VNode { - switch (ch) { - case TanChannel.SMS: - return ( - <i18n.Translate> - An SMS to the phone number ending with{" "} - <span>{challenge.tan_info}</span> - </i18n.Translate> - ); - case TanChannel.EMAIL: - return ( - <i18n.Translate> - An email to the address starting with{" "} - <span>{challenge.tan_info}</span> - </i18n.Translate> - ); - } - })(challenge.tan_channel)} - - <div + <div class="modal is-active" style={{ position: "initial" }}> + <div class="modal-card" style={{ width: "100%", margin: 0 }}> + <header + class="modal-card-head" + style={{ border: "1px solid", borderBottom: 0 }} + > + <p class="modal-card-title"> + <i18n.Translate> + Multi-factor authentication required. + </i18n.Translate> + </p> + </header> + <section + class="modal-card-body" + style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + > + {currentChallenge.combi_and ? ( + <i18n.Translate> + You must complete all of these requirements. + </i18n.Translate> + ) : ( + <i18n.Translate> + You need to complete at least one of this requirements. + </i18n.Translate> + )} + </section> + {currentChallenge.challenges.map((challenge, idx) => { + const noNeedToComplete = + hasSolvedEnough || + solved.indexOf(challenge.challenge_id) !== -1; + + const doSend = noNeedToComplete + ? sendMessage + : sendMessage.withArgs(challenge); + + return ( + <section + class="modal-card-body" style={{ - justifyContent: "space-between", - display: "flex", + border: "1px solid", + borderTop: 0, + borderBottom: 0, }} > - <div /> - <ButtonBetterBulma - type="button" - onClick={doSend} - focus={idx === 0 && focus} + {(function (ch: TanChannel): VNode { + switch (ch) { + case TanChannel.SMS: + return ( + <i18n.Translate> + An SMS to the phone number ending with{" "} + <span>{challenge.tan_info}</span> + </i18n.Translate> + ); + case TanChannel.EMAIL: + return ( + <i18n.Translate> + An email to the address starting with{" "} + <span>{challenge.tan_info}</span> + </i18n.Translate> + ); + } + })(challenge.tan_channel)} + + <div + style={{ + justifyContent: "space-between", + display: "flex", + }} > - <i18n.Translate>Continue</i18n.Translate> - </ButtonBetterBulma> - </div> - </section> - ); - })} - <footer - class="modal-card-foot " - style={{ - justifyContent: "space-between", - border: "1px solid", - borderTop: 0, - }} - > - <button class="button" onClick={onCancel}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - <div /> - </footer> + <div /> + <ButtonBetterBulma + type="button" + onClick={doSend} + focus={idx === 0 && focus} + > + <i18n.Translate>Continue</i18n.Translate> + </ButtonBetterBulma> + </div> + </section> + ); + })} + <footer + class="modal-card-foot " + style={{ + justifyContent: "space-between", + border: "1px solid", + borderTop: 0, + }} + > + <button class="button" onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <div /> + </footer> + </div> </div> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/components/exception/QR.tsx b/packages/merchant-backoffice-ui/src/components/exception/QR.tsx @@ -14,17 +14,24 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { TalerUri, TalerUris, TranslatedString } from "@gnu-taler/taler-util"; import { h, VNode } from "preact"; import { useEffect, useRef } from "preact/hooks"; import qrcode from "qrcode-generator"; +import logo from "../../assets/logo-2021.svg"; export function QR({ + children, text, + style, ...rest -}: { text: string } & h.JSX.HTMLAttributes<HTMLDivElement>): VNode { +}: { + text: string; + children?: VNode<HTMLImageElement>; +} & h.JSX.HTMLAttributes<HTMLDivElement>): VNode { const divRef = useRef<HTMLDivElement>(null); useEffect(() => { - const qr = qrcode(0, "L"); + const qr = qrcode(0, "H"); qr.addData(text); qr.make(); if (divRef.current) { @@ -45,5 +52,53 @@ export function QR({ } }); + if (children) { + return ( + <div {...rest} style={{ ...(style as any), position: "relative" }}> + <div ref={divRef} /> + <div + style={{ + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + border: "2px solid black", + backgroundColor: "white", + display: "flex", + padding: 4, + alignContent: "center", + borderRadius: "10%", + }} + > + {children} + </div> + </div> + ); + } return <div {...rest} ref={divRef} />; } + +export function QR_Taler({ uri }: { uri: TalerUri }): VNode { + const stringUri = TalerUris.toString(uri); + return ( + <QR style={{ width: "90%", maxWidth: 400, margin:"auto" }} text={stringUri}> + <img src={logo} style={{ width: 100, height: 50 }} /> + </QR> + ); +} + +export function QR_TOTP({ otpAuthURI }: { otpAuthURI: string }): VNode { + return ( + <QR style={{ width: "90%", maxWidth: 400, margin:"auto" }} text={otpAuthURI}> + <div style={{ fontWeight: "bold" }}>T-OTP</div> + </QR> + ); +} + +export function QR_Bank({ text, label }: { text: string, label: TranslatedString }): VNode { + return ( + <QR style={{ width: "90%", maxWidth: 400, margin:"auto" }} text={text}> + <div style={{ fontWeight: "bold" }}>{label}</div> + </QR> + ); +} diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx @@ -21,6 +21,7 @@ import { Amounts, + assertUnreachable, getQrCodesForPayto, Paytos, PaytoType, @@ -36,7 +37,7 @@ import { ComponentChildren, Fragment, h, VNode } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js"; import { Spinner } from "../exception/loading.js"; -import { QR } from "../exception/QR.js"; +import { QR, QR_Bank } from "../exception/QR.js"; import { doAutoFocus } from "../form/Input.js"; import { useSessionContext } from "../../context/session.js"; @@ -481,7 +482,7 @@ export function ValidBankAccount({ }} > <Accordion name={q.type} openedByDefault> - <QR text={q.qrContent} /> + <QR_Bank text={q.qrContent} label={i18n.str`Banking app`} /> </Accordion> </td> </tr> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -25,29 +25,33 @@ import { Amounts, HostPortPath, MerchantContractVersion, + Result, TalerMerchantApi, + TalerUris, TransactionWireTransfer, assertUnreachable, stringifyRefundUri, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, + NotificationCardBulma, RenderAmountBulma, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { format, formatDistance } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; +import { QR_Taler } from "../../../../components/exception/QR.js"; import { FormProvider } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDate } from "../../../../components/form/InputDate.js"; -import { InputDurationSelector } from "../../../../components/form/InputDurationSelector.js"; +import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; import { InputGroup } from "../../../../components/form/InputGroup.js"; import { InputLocation } from "../../../../components/form/InputLocation.js"; import { TextField } from "../../../../components/form/TextField.js"; -import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; +import { ConfirmModal } from "../../../../components/modal/index.js"; import { ProductList } from "../../../../components/product/ProductList.js"; +import { Tooltip } from "../../../../components/Tooltip.js"; import { useSessionContext } from "../../../../context/session.js"; import { datetimeFormatForPreferences, @@ -56,10 +60,6 @@ import { import { mergeRefunds } from "../../../../utils/amount.js"; import { RefundModal } from "../list/Table.js"; import { Event, Timeline } from "./Timeline.js"; -import { ConfirmModal } from "../../../../components/modal/index.js"; -import { Tooltip } from "../../../../components/Tooltip.js"; -import { QR } from "../../../../components/exception/QR.js"; -import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; const TALER_SCREEN_ID = 44; @@ -1034,7 +1034,9 @@ function UnpaidPage({ <div style={{ width: "100%", display: "flex", justifyContent: "center" }} > - <QR style={{width:"90%", maxWidth:400}} text={value.taler_pay_uri!} /> + <QR_Taler + uri={Result.unpack(TalerUris.fromString(value.taler_pay_uri!))} + /> </div> </section> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx @@ -17,7 +17,7 @@ import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; -import { QR } from "../../../../components/exception/QR.js"; +import { QR, QR_TOTP } from "../../../../components/exception/QR.js"; import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; import { useSessionContext } from "../../../../context/session.js"; @@ -33,9 +33,9 @@ interface Props { export function CreatedSuccessfully({ entity, onConfirm }: Props): VNode { const { i18n } = useTranslationContext(); const { state } = useSessionContext(); - const issuer = state.backendUrl.href; - const qrText = `otpauth://totp/${state.instance}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`; - const qrTextSafe = `otpauth://totp/${state.instance}/${ + const issuer = state.backendUrl.href.replace(/.*:\/\//, ""); // remove http(s):// + const qrText = `otpauth://totp/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`; + const qrTextSafe = `otpauth://totp/${ entity.otp_device_id }?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring( 0, @@ -81,7 +81,7 @@ export function CreatedSuccessfully({ entity, onConfirm }: Props): VNode { </div> </div> <div style={{ width: "100%", display: "flex", justifyContent: "center" }}> - <QR style={{ width: "90%", maxWidth: 400 }} text={qrText} /> + <QR_TOTP otpAuthURI={qrText} /> </div> <div style={{ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -22,13 +22,13 @@ import { HostPortPath, TalerMerchantApi, - stringifyPayTemplateUri, + TalerUris, } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { VNode, h } from "preact"; -import { QR } from "../../../../components/exception/QR.js"; -import { useSessionContext } from "../../../../context/session.js"; +import { h, VNode } from "preact"; import { useRef } from "preact/hooks"; +import { QR_Taler } from "../../../../components/exception/QR.js"; +import { useSessionContext } from "../../../../context/session.js"; const TALER_SCREEN_ID = 64; @@ -45,13 +45,8 @@ export function QrPage({ id: templateId, onBack }: Props): VNode { const { state } = useSessionContext(); const merchantBaseUrl = state.backendUrl.href as HostPortPath; - - const payTemplateUri = stringifyPayTemplateUri({ - merchantBaseUrl, - templateId, - // FIXME! - //templateParams: {}, - }); + const uri = TalerUris.createTalerPayTemplate(merchantBaseUrl, templateId) + const stringUri = TalerUris.toString(uri) const printThis = useRef<HTMLElement>(null); return ( @@ -60,11 +55,11 @@ export function QrPage({ id: templateId, onBack }: Props): VNode { <div style={{ width: "100%", display: "flex", justifyContent: "center" }} > - <QR style={{width:"90%", maxWidth:400}} text={payTemplateUri} /> + <QR_Taler uri={uri} /> </div> <pre style={{ textAlign: "center" }}> - <a target="_blank" rel="noreferrer" href={payTemplateUri}> - {payTemplateUri} + <a target="_blank" rel="noreferrer" href={stringUri}> + {stringUri} </a> </pre> </section> diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -92,7 +92,7 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { await maybeTryFirstMFA(lib.instance, mfa, resp.body); } - return resp + return resp; }, !username || !password ? undefined : [username, password, []], ); @@ -132,121 +132,130 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { return ( <div> <LocalNotificationBannerBulma notification={notification} /> + <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> - <div style={{ width: "100%", margin: 0 }}> - <header - class="modal-card-head" - style={{ border: "1px solid", borderBottom: 0 }} - > - <p class="modal-card-title"> - <i18n.Translate>Login required</i18n.Translate> - </p> - </header> - <FormProvider> - <section - class="modal-card-body" - style={{ borderRight: "1px solid",borderLeft: "1px solid", borderTop: 0, borderBottom: 0, overflow: "hidden"}} + <div class="modal is-active" style={{ position: "initial" }}> + <div class="modal-card" style={{ width: "100%", margin: 0 }}> + <header + class="modal-card-head" + style={{ border: "1px solid", borderBottom: 0 }} > - <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> + <p class="modal-card-title"> + <i18n.Translate>Login required</i18n.Translate> + </p> + </header> + <FormProvider> + <section + class="modal-card-body" + style={{ + borderRight: "1px solid", + borderLeft: "1px solid", + borderTop: 0, + borderBottom: 0, + 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> - <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 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> - </div> - </section> - <footer - class="modal-card-foot " - style={{ - justifyContent: "space-between", - border: "1px solid", - borderTop: 0, - }} - > - {!config.have_self_provisioning ? ( - <div /> - ) : ( - <a - href={ - !username || username === "admin" - ? undefined - : `#/account/reset/${username}` - } - class="button " - disabled={!username || username === "admin"} - > - <i18n.Translate>Forgot password</i18n.Translate> - </a> - )} - <ButtonBetterBulma onClick={login} type="submit"> - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </footer> - </FormProvider> + </section> + <footer + class="modal-card-foot " + style={{ + justifyContent: "space-between", + border: "1px solid", + borderTop: 0, + }} + > + {!config.have_self_provisioning ? ( + <div /> + ) : ( + <a + href={ + !username || username === "admin" + ? undefined + : `#/account/reset/${username}` + } + class="button " + disabled={!username || username === "admin"} + > + <i18n.Translate>Forgot password</i18n.Translate> + </a> + )} + <ButtonBetterBulma onClick={login} type="submit"> + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </footer> + </FormProvider> + </div> </div> {!showCreateAccount ? undefined : ( <div style={{ marginTop: 8 }}> diff --git a/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx @@ -25,7 +25,7 @@ import { HttpStatusCode, InstanceConfigurationMessage, MerchantAuthMethod, - TanChannel + TanChannel, } from "@gnu-taler/taler-util"; import { buildStorageKey, @@ -255,113 +255,118 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { <LocalNotificationBannerBulma notification={notification} /> <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> - <div style={{ width: "100%", margin: 0 }}> - <header - class="modal-card-head" - style={{ border: "1px solid", borderBottom: 0 }} + <div class="modal is-active" style={{ position: "initial" }}> + <div + class="modal-card" + style={{ width: "100%", height: "100%", margin: 0 }} > - <p class="modal-card-title"> - <i18n.Translate>Self provision</i18n.Translate> - </p> - </header> - <FormProvider<Account> - name="settings" - errors={errors} - object={value} - valueHandler={valueHandler} - > - <section - class="modal-card-body" - style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + <FormProvider<Account> + name="settings" + errors={errors} + object={value} + valueHandler={valueHandler} > - <InputWithAddon<Account> - name="id" - label={i18n.str`Username`} - tooltip={i18n.str`Name of the instance in URLs. The 'admin' instance is special in that it is used to administer other instances.`} - autoComplete="username" - focus - /> + <header + class="modal-card-head" + style={{ border: "1px solid", borderBottom: 0 }} + > + <p class="modal-card-title"> + <i18n.Translate>Self provision</i18n.Translate> + </p> + </header> + <section + class="modal-card-body" + style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + > + <InputWithAddon<Account> + name="id" + label={i18n.str`Username`} + tooltip={i18n.str`Name of the instance in URLs. The 'admin' instance is special in that it is used to administer other instances.`} + autoComplete="username" + focus + /> - <Input<Account> - name="name" - label={i18n.str`Business name`} - tooltip={i18n.str`Legal name of the business represented by this instance.`} - autoComplete="organization" - /> - <InputPassword<Account> - expand - label={i18n.str`New password`} - tooltip={i18n.str`Next password to be used`} - name="password" - autoComplete="new-password" - /> - <InputPassword<Account> - label={i18n.str`Repeat password`} - tooltip={i18n.str`Confirm the same password`} - name="repeat" - expand - autoComplete="new-password" - /> - {serverRequiresEmail ? ( <Input<Account> - label={i18n.str`Email`} - tooltip={i18n.str`Contact email`} - name="email" - inputType="email" - autoComplete="email" + name="name" + label={i18n.str`Business name`} + tooltip={i18n.str`Legal name of the business represented by this instance.`} + autoComplete="organization" /> - ) : undefined} - {serverRequiresSms ? ( - <Input<Account> - label={i18n.str`Phone`} - tooltip={i18n.str`Contact phone number`} - name="phone" - inputType="tel" - autoComplete="tel" + <InputPassword<Account> + expand + label={i18n.str`New password`} + tooltip={i18n.str`Next password to be used`} + name="password" + autoComplete="new-password" /> - ) : undefined} - <InputToggle - label={i18n.str`Accept the Terms of service`} - name="tos" - help={ - <i18n.Translate> - I understand and agree to the{" "} - <a - href="/terms" - target="_blank" - tabIndex={-1} - referrerpolicy="no-referrer" - > - <i18n.Translate>Terms of service</i18n.Translate> - </a> - </i18n.Translate> - } - tooltip={i18n.str`You must accept the Terms of service to continue.`} - /> - </section> - <footer - class="modal-card-foot " - style={{ - justifyContent: "space-between", - border: "1px solid", - borderTop: 0, - }} - > - <button - class="button" - type="button" - onClick={() => { - saveForm({}); - onCancel(); + <InputPassword<Account> + label={i18n.str`Repeat password`} + tooltip={i18n.str`Confirm the same password`} + name="repeat" + expand + autoComplete="new-password" + /> + {serverRequiresEmail ? ( + <Input<Account> + label={i18n.str`Email`} + tooltip={i18n.str`Contact email`} + name="email" + inputType="email" + autoComplete="email" + /> + ) : undefined} + {serverRequiresSms ? ( + <Input<Account> + label={i18n.str`Phone`} + tooltip={i18n.str`Contact phone number`} + name="phone" + inputType="tel" + autoComplete="tel" + /> + ) : undefined} + <InputToggle + label={i18n.str`Accept the Terms of service`} + name="tos" + help={ + <i18n.Translate> + I understand and agree to the{" "} + <a + href="/terms" + target="_blank" + tabIndex={-1} + referrerpolicy="no-referrer" + > + <i18n.Translate>Terms of service</i18n.Translate> + </a> + </i18n.Translate> + } + tooltip={i18n.str`You must accept the Terms of service to continue.`} + /> + </section> + <footer + class="modal-card-foot " + style={{ + justifyContent: "space-between", + border: "1px solid", + borderTop: 0, }} > - <i18n.Translate>Cancel</i18n.Translate> - </button> - <ButtonBetterBulma onClick={create} type="submit"> - <i18n.Translate>Create</i18n.Translate> - </ButtonBetterBulma> - </footer> - </FormProvider> + <button + class="button" + type="button" + onClick={() => { + saveForm({}); + onCancel(); + }} + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + <ButtonBetterBulma onClick={create} type="submit"> + <i18n.Translate>Create</i18n.Translate> + </ButtonBetterBulma> + </footer> + </FormProvider> + </div> </div> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx @@ -144,55 +144,58 @@ export function ResetAccount({ <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> - <div class="modal-card" style={{ width: "100%", margin: 0 }}> - <header - class="modal-card-head" - style={{ border: "1px solid", borderBottom: 0 }} - > - <p class="modal-card-title"> - <i18n.Translate> - Resetting access to the instance "{instanceId}" - </i18n.Translate> - </p> - </header> - <FormProvider<Form> - name="settings" - errors={errors} - object={value} - valueHandler={valueHandler} - > - <section - class="modal-card-body" - style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + <div class="modal is-active" style={{ position: "initial" }}> + <div class="modal-card" style={{ width: "100%", margin: 0 }}> + <header + class="modal-card-head" + style={{ border: "1px solid", borderBottom: 0 }} > - <InputPassword<Form> - label={i18n.str`New password`} - name="password" - expand - autoComplete="new-password" - /> - <InputPassword<Form> - label={i18n.str`Repeat password`} - name="repeat" - autoComplete="new-password" - /> - </section> - <footer - class="modal-card-foot " - style={{ - justifyContent: "space-between", - border: "1px solid", - borderTop: 0, - }} + <p class="modal-card-title"> + <i18n.Translate> + Resetting access to the instance "{instanceId}" + </i18n.Translate> + </p> + </header> + <FormProvider<Form> + name="settings" + errors={errors} + object={value} + valueHandler={valueHandler} > - <button type="button" class="button" onClick={onCancel}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - <ButtonBetterBulma type="submit" onClick={reset}> - <i18n.Translate>Reset</i18n.Translate> - </ButtonBetterBulma> - </footer> - </FormProvider> + <section + class="modal-card-body" + style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + > + <InputPassword<Form> + label={i18n.str`New password`} + name="password" + expand + autoComplete="new-password" + /> + <InputPassword<Form> + label={i18n.str`Repeat password`} + name="repeat" + expand + autoComplete="new-password" + /> + </section> + <footer + class="modal-card-foot " + style={{ + justifyContent: "space-between", + border: "1px solid", + borderTop: 0, + }} + > + <button type="button" class="button" onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <ButtonBetterBulma type="submit" onClick={reset}> + <i18n.Translate>Reset</i18n.Translate> + </ButtonBetterBulma> + </footer> + </FormProvider> + </div> </div> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/scss/main.scss b/packages/merchant-backoffice-ui/src/scss/main.scss @@ -201,11 +201,3 @@ input.mfa-code:focus { input.mfa-code[type=number] { -moz-appearance: textfield; } -.modal-card-body { - -webkit-overflow-scrolling: touch; - background-color: white; - flex-grow: 1; - flex-shrink: 1; - overflow: initial; - padding: 20px; -} diff --git a/packages/taler-util/src/qr.ts b/packages/taler-util/src/qr.ts @@ -134,6 +134,8 @@ function encodePaytoAsEpcQr(paytoUri: string): EncodeResult { }; } +// declare const __qr_id: unique symbol; +export type SupportedBankQr = ("epc-qr" | "spc");// & { [__qr_id]: true }; /** * Specification of a QR code that includes payment information. */ @@ -144,7 +146,7 @@ export interface QrCodeSpec { * Depending on the type, different visual styles * might be applied. */ - type: string; + type: SupportedBankQr; /** * Content of the QR code that should be rendered.