commit 9cb63331da2cf54bac54cd5ef98763e2d31e5e27
parent 1913ea0f622268fa6b9c5a91cb3a317d3c850cd0
Author: Sebastian <sebasjm@taler-systems.com>
Date: Tue, 7 Apr 2026 15:49:50 -0300
fix #11320
Diffstat:
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 */