taler-typescript-core

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

commit 9cb63331da2cf54bac54cd5ef98763e2d31e5e27
parent 1913ea0f622268fa6b9c5a91cb3a317d3c850cd0
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Tue,  7 Apr 2026 15:49:50 -0300

fix #11320

Diffstat:
Dpackages/merchant-backoffice-ui/src/assets/ch-flag.svg | 5-----
Mpackages/merchant-backoffice-ui/src/components/exception/QR.tsx | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mpackages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx | 7+++++++
Mpackages/merchant-backoffice-ui/src/scss/main.scss | 13+++++++++++++
Mpackages/merchant-backoffice-ui/tsconfig.json | 3+++
5 files changed, 130 insertions(+), 23 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/assets/ch-flag.svg b/packages/merchant-backoffice-ui/src/assets/ch-flag.svg @@ -1,5 +0,0 @@ -<?xml version="1.0"?> -<svg width="512" height="512" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> -<path d="m0 0h32v32h-32z" fill="#00"/> -<path d="m13 6h6v7h7v6h-7v7h-6v-7h-7v-6h7z" fill="#fff"/> -</svg> diff --git a/packages/merchant-backoffice-ui/src/components/exception/QR.tsx b/packages/merchant-backoffice-ui/src/components/exception/QR.tsx @@ -18,26 +18,52 @@ 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"; -import chFlag from "../../assets/ch-flag.svg"; +import logo from "@assets/svg/logo/qr-logo.svg"; +import chFlag from "@assets/svg/swiss-qr-flag.svg"; +import { infinite } from "swr/infinite"; + +function generate_qr( + text: string, + params: { + typeNumber?: TypeNumber; + errorCorrectionLevel?: ErrorCorrectionLevel; + } = {}, +) { + const qr = qrcode( + params.typeNumber ?? 0, + (params.errorCorrectionLevel = "H"), + ); + qr.addData(text, "Byte"); + qr.make(); + const image = qr.createSvgTag({ + scalable: true, + margin: 0, + }); + return `data:image/svg+xml,${encodeURIComponent(image)}`; +} export function QR({ children, text, style, + errorCorrectionLevel = "H", + typeNumber = 0, ...rest }: { text: string; + typeNumber?: TypeNumber; + errorCorrectionLevel?: ErrorCorrectionLevel; children?: VNode<HTMLImageElement>; } & h.JSX.HTMLAttributes<HTMLDivElement>): VNode { const divRef = useRef<HTMLDivElement>(null); useEffect(() => { - const qr = qrcode(0, "H"); - qr.addData(text); + const qr = qrcode(typeNumber, errorCorrectionLevel); + qr.addData(text, "Byte"); qr.make(); if (divRef.current) { const image = qr.createSvgTag({ scalable: true, + margin: 0, }); const imageURL = `data:image/svg+xml,${encodeURIComponent(image)}`; divRef.current.innerHTML = `<img src=${JSON.stringify( @@ -63,12 +89,7 @@ export function QR({ top: "50%", left: "50%", transform: "translate(-50%, -50%)", - border: "2px solid black", - backgroundColor: "white", - display: "flex", - padding: 4, alignContent: "center", - borderRadius: "10%", }} > {children} @@ -82,30 +103,98 @@ export function QR({ export function QR_Taler({ uri }: { uri: TalerUri }): VNode { const stringUri = TalerUris.toString(uri); return ( - <QR - style={{ width: "90%", maxWidth: 400, margin: "auto" }} - text={stringUri} + <div + style={{ + width: "100%", + height: "100%", + maxWidth: 400, + margin: "auto", + padding: 10, // size of the animated border + borderRadius: 20, + background: `conic-gradient( + from var(--angle), + #0042B3 0deg, + #F1F1F4 20deg, + #F1F1F4 150deg, + #F1F1F4 160deg, + #0042B3 180deg, + #F1F1F4 200deg, + #F1F1F4 330deg, + #F1F1F4 340deg, + #0042B3) padding-box`, + animation: "rotate 10s linear infinite", + position: "relative", + }} > - <img src={logo} style={{ width: 100, height: 50 }} /> - </QR> + <div + style={{ + padding: 10, // separate the qr from the animated border + borderRadius: 20, // match the radius from the parent + backgroundColor: "white", // hide the background + }} + > + <img style={{ margin: 5 }} src={generate_qr(stringUri)} /> + </div> + <div + style={{ + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + alignContent: "center", + display: "flex", + backgroundColor: "transparent", // section 19.90.4.3 + }} + > + <img src={logo} style={{ width: 100, height: 50 }} /> + </div> + </div> ); } export function QR_TOTP({ otpAuthURI }: { otpAuthURI: string }): VNode { return ( <QR - style={{ width: "90%", maxWidth: 400, margin: "auto" }} + style={{ width: "90%", maxWidth: 400, margin: "auto", padding: 10 }} text={otpAuthURI} > - <div style={{ fontWeight: "bold" }}>T-OTP</div> + <div + style={{ + display: "flex", + border: "1px solid black", + backgroundColor: "white", + fontWeight: "bold", + padding: 5, + }} + > + T-OTP + </div> </QR> ); } +/** + * Based on the definition of Swiss Implementation Guidelines + * for the QR-bill + * @param param0 + * @returns + */ export function QR_SwissBank({ text }: { text: string }): VNode { return ( - <QR style={{ width: "90%", maxWidth: 400, margin: "auto" }} text={text}> - <img src={chFlag} style={{ width: 50, height: 50 }} /> + <QR + style={{ width: "90%", maxWidth: 400, margin: "auto", padding: 10 }} + text={text} + typeNumber={0} // section 6.2 + errorCorrectionLevel="M" // section 6.1 + > + <div + style={{ + display: "flex", + backgroundColor: "white", + }} + > + <img src={chFlag} style={{ width: 50, height: 50 }} /> + </div> </QR> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx @@ -40,6 +40,7 @@ import { } from "../../../../components/modal/index.js"; import { useInstanceKYCDetails } from "../../../../hooks/instance.js"; import { ListPage } from "./ListPage.js"; +import { useEffect } from "preact/hooks"; const TALER_SCREEN_ID = 40; @@ -54,6 +55,12 @@ export default function ListKYC(_p: Props): VNode { const [showingInstructions, setShowingInstructions] = useState< TalerMerchantApi.MerchantAccountKycRedirect | undefined >(undefined); + + // useEffect(() => { + // if (!result || result instanceof TalerError || result.type === "fail") return; + // if (!result.body.kyc_data.length) return; + // setShowingInstructions(result.body.kyc_data[0]) + // }) if (!result) return <Loading />; if (result instanceof TalerError) { return <ErrorLoadingMerchant error={result} />; diff --git a/packages/merchant-backoffice-ui/src/scss/main.scss b/packages/merchant-backoffice-ui/src/scss/main.scss @@ -201,3 +201,15 @@ input.mfa-code:focus { input.mfa-code[type=number] { -moz-appearance: textfield; } + +@property --angle { + syntax: "<angle>"; + initial-value: 0deg; + inherits: false; +} + +@keyframes rotate { + to { + --angle: 360deg; + } +} +\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/tsconfig.json b/packages/merchant-backoffice-ui/tsconfig.json @@ -11,6 +11,9 @@ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, "jsxFactory": "h" /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */, "jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories + "paths": { + "@assets/*": ["../../contrib/taler-assets/*"], + }, "noEmit": true /* Do not emit outputs. */, /* Module Resolution Options */ /* Advanced Options */