commit 88cd7dfa507d6b44db00a2afc72ae048cd81ad0c
parent ece7aabb541a58ed74b7579393572af67ef8b40c
Author: Sebastian <sebasjm@taler-systems.com>
Date: Thu, 2 Jul 2026 16:12:18 -0300
fix #11460
Diffstat:
2 files changed, 242 insertions(+), 155 deletions(-)
diff --git a/packages/taler-merchant-webui/src/components/modal/index.tsx b/packages/taler-merchant-webui/src/components/modal/index.tsx
@@ -25,21 +25,25 @@ import {
getQrCodesForPayto,
Paytos,
PaytoType,
+ Result,
TranslatedString,
} from "@gnu-taler/taler-util";
import {
Button,
QR_Bank,
QR_SwissBank,
+ RenderAmountBulma,
SafeHandler,
+ simpleSafeHandler,
useCommonPreferences,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
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 { Loading, Spinner } from "../exception/loading.js";
import { doAutoFocus } from "../form/Input.js";
+import { useSessionContext } from "../../context/session.js";
const TALER_SCREEN_ID = 18;
@@ -85,7 +89,7 @@ export function ConfirmModal({
}: ConfirmProps): VNode {
const { i18n } = useTranslationContext();
return (
- <div class="modal is-active" style={zIndex ? {zIndex } : undefined}>
+ <div class="modal is-active" style={zIndex ? { zIndex } : undefined}>
<div
class="modal-background "
onClick={preventBackgroundClose ? undefined : onCancel}
@@ -358,16 +362,59 @@ interface ValidateBankAccountModalProps {
origin: Paytos.URI;
targets: Paytos.URI[];
currency: string | undefined;
+ exchangeBaseUrl: string;
+ wireAccountHash: string;
}
export function ValidBankAccount({
onCancel,
origin,
- targets,
- currency: exchangeCurrency,
+ exchangeBaseUrl,
+ wireAccountHash,
}: ValidateBankAccountModalProps): VNode {
const { i18n } = useTranslationContext();
- const payto = targets[0];
- const subject = payto.params["message"];
+ const { state, lib, config } = useSessionContext();
+ const [targetIndex, setTargetIndex] = useState(0);
+
+ const [kycAuth, setKeyAuth] =
+ useState<
+ Awaited<ReturnType<typeof lib.instance.getInstancePrivateAccountKycauth>>
+ >();
+ useEffect(() => {
+ if (!state.token) return;
+ lib.instance
+ .getInstancePrivateAccountKycauth(state.token, {
+ exchangeBaseUrl,
+ wireAccountHash,
+ })
+ .then((r) => {
+ setKeyAuth(r);
+ });
+ }, []);
+ if (!kycAuth) {
+ return (
+ <ConfirmModal
+ title={i18n.str`Getting bank transfer instructions`}
+ onCancel={onCancel}
+ >
+ <div style={{ height: 300, display: "flex" }}>
+ <Loading />
+ </div>
+ </ConfirmModal>
+ );
+ }
+ if (kycAuth.type === "fail") {
+ return (
+ <ConfirmModal
+ title={i18n.str`Getting bank transfer instructions`}
+ onCancel={onCancel}
+ >
+ <div>failed {kycAuth.case}</div>
+ </ConfirmModal>
+ );
+ }
+ const all_ins = kycAuth.body.wire_instructions;
+ const instructions = all_ins[targetIndex];
+ const payto = Result.unpack(Paytos.fromString(instructions.target_payto));
const accountPart =
payto.targetType === PaytoType.TalerBank ? (
@@ -383,7 +430,6 @@ export function ValidBankAccount({
<Row name={i18n.str`IBAN`} value={payto.iban} />
</Fragment>
) : undefined;
-
const receiverName =
payto.params["receiver-name"] || payto.params["receiver"] || undefined;
const receiverPostalCode = payto.params["receiver-postal-code"] || undefined;
@@ -401,160 +447,199 @@ export function ValidBankAccount({
? `${origin.address.substring(0, 8)}...`
: origin.displayName;
- // FIXME: define a minimum by currency somewhere https://bugs.gnunet.org/view.php?id=11169
- const sendAmount = !exchangeCurrency
- ? undefined
- : Amounts.parseOrThrow(`${exchangeCurrency}:0.01`);
-
- if (sendAmount) {
- payto.params["amount"] = Amounts.stringify(sendAmount);
- }
const qrs = getQrCodesForPayto(Paytos.toFullString(payto));
const strPayto = Paytos.toFullString(payto);
const [{ showDebugInfo }] = useCommonPreferences();
- return (
- <ConfirmModal
- title={i18n.str`Validate bank account: ${from}`}
- onCancel={onCancel}
- >
- {!showDebugInfo ? undefined : (
- <pre>{JSON.stringify({ strPayto, payto }, undefined, 2)}</pre>
- )}
- <p style={{ paddingTop: 0 }}>
- <i18n.Translate>
- In order to prove that you are the beneficial owner of the bank
- account, you are required to wire a small amount to a specified bank
- account with the subject below.
- </i18n.Translate>
- </p>
- <tr>
- <td colSpan={3}>
- {!qrs.length ? undefined : (
- <div style={{ overflowY: "auto", height: "95%", padding: 5 }}>
- <p>
- <i18n.Translate>
- If your bank application allows you make a wire transfer using
- standard QR codes then try scanning the following:
- </i18n.Translate>
- </p>
- <table style={{ width: "100%" }}>
- {qrs.map((q, idx) => {
- return (
- <tr key={idx}>
- <td style={{}}>
- {(function () {
- switch (q.type) {
- case "epc-qr":
- return (
- <QR_Bank
- text={q.qrContent}
- label={i18n.str`Bank app`}
- />
- );
- case "spc":
- return <QR_SwissBank text={q.qrContent} />;
- default:
- assertUnreachable(q.type);
- }
- })()}
- </td>
- </tr>
- );
- })}
- </table>
- </div>
+ switch (instructions.subject.type) {
+ case "URI":
+ return (
+ <ConfirmModal
+ title={i18n.str`Failed to get bank transfer instructions`}
+ onCancel={onCancel}
+ >
+ <div>unsupported instructions URI</div>
+ </ConfirmModal>
+ );
+ case "CH_QR_BILL":
+ case "SIMPLE": {
+ const sendAmount = Amounts.parseOrThrow(
+ instructions.subject.credit_amount,
+ );
+ return (
+ <ConfirmModal
+ title={i18n.str`Validate bank account: ${from}`}
+ onCancel={onCancel}
+ confirm={
+ all_ins.length > 1
+ ? {
+ label: i18n.str`See other option`,
+ handler: simpleSafeHandler(() => {
+ setTargetIndex((idx) => (idx + 1) % all_ins.length);
+ }),
+ }
+ : undefined
+ }
+ >
+ {!showDebugInfo ? undefined : (
+ <pre>
+ {JSON.stringify({ strPayto, payto, all_ins }, undefined, 2)}
+ </pre>
)}
- </td>
- </tr>
- <div class="">
- <table>
- <tbody>
- <tr>
- <td colSpan={3}>
- <i18n.Translate>Step 1:</i18n.Translate>
-
- <i18n.Translate>
- Copy and paste this IBAN and the legal name of the beneficial
- owner into the respective fields in your preferred banking app
- or online banking website.
- </i18n.Translate>
- </td>
- </tr>
- {accountPart}
- {receiverName ? (
- <Row name={i18n.str`Beneficiary`} value={receiverName} />
- ) : undefined}
- <tr>
- <td colSpan={3}>
- <i18n.Translate>Step 2:</i18n.Translate>
-
- <i18n.Translate>
- Copy this string and paste it into the 'Subject' or 'Purpose'
- field in your preferred banking app or online banking website.
- </i18n.Translate>
- </td>
- </tr>
- <Row name={i18n.str`Subject`} value={subject} literal />
- {receiverPostalCode ? (
- <Row
- name={i18n.str`Receiver postal code`}
- value={receiverPostalCode}
- />
- ) : undefined}
- {receiverTown ? (
- <Row name={i18n.str`Receiver town`} value={receiverTown} />
- ) : undefined}
+ <p style={{ paddingTop: 0 }}>
+ <i18n.Translate>
+ In order to prove that you are the beneficial owner of the bank
+ account, you are required to wire a small amount to a specified
+ bank account with the subject below.
+ </i18n.Translate>
+ </p>
+ <tr>
+ <td colSpan={3}>
+ {!qrs.length ? undefined : (
+ <div style={{ overflowY: "auto", height: "95%", padding: 5 }}>
+ <p>
+ <i18n.Translate>
+ If your bank application allows you make a wire transfer
+ using standard QR codes then try scanning the following:
+ </i18n.Translate>
+ </p>
+ <table style={{ width: "100%" }}>
+ {qrs.map((q, idx) => {
+ return (
+ <tr key={idx}>
+ <td style={{}}>
+ {(function () {
+ switch (q.type) {
+ case "epc-qr":
+ return (
+ <QR_Bank
+ text={q.qrContent}
+ label={i18n.str`Bank app`}
+ />
+ );
+ case "spc":
+ return <QR_SwissBank text={q.qrContent} />;
- <tr>
- <td colSpan={3}>
- <i18n.Translate>Step 3:</i18n.Translate>
-
- <i18n.Translate>
- Select the smallest possible amount for a wire transfer in
- your preferred banking app or online banking website.
- </i18n.Translate>
- </td>
- </tr>
- <tr>
- <td colSpan={3}>
- <b>
- <i18n.Translate>
- Make sure ALL data is correct, especially the subject, and
- that you are choosing the bank account from which you
- definitely want to make the wire transfer. You can use the
- copy buttons (<CopyIcon />) to avoid typos or make use of
- the PayTo URI below to copy just one value.
- </i18n.Translate>
- </b>
- </td>
- </tr>
- <tr>
- <td colSpan={2} width="100%">
- <i18n.Translate>
- As an alternative, in case your bank already supports the{" "}
- <a
- target="_blank"
- rel="noreferrer"
- title="RFC 8905 for designating targets for payments"
- href="https://tools.ietf.org/html/rfc8905"
- >
- PayTo URI standard
- </a>
- , you can use this{" "}
- <a href={Paytos.toFullString(payto)}>PayTo URI</a> link
- instead.
- </i18n.Translate>
- </td>
- <td>
- <CopyButton getContent={() => Paytos.toFullString(payto)} />
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </ConfirmModal>
- );
+ default:
+ assertUnreachable(q.type);
+ }
+ })()}
+ </td>
+ </tr>
+ );
+ })}
+ </table>
+ </div>
+ )}
+ </td>
+ </tr>
+ <div class="">
+ <table>
+ <tbody>
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 1:</i18n.Translate>
+
+ <i18n.Translate>
+ Copy and paste this IBAN and the legal name of the
+ beneficial owner into the respective fields in your
+ preferred banking app or online banking website.
+ </i18n.Translate>
+ </td>
+ </tr>
+ {accountPart}
+ {receiverName ? (
+ <Row name={i18n.str`Beneficiary`} value={receiverName} />
+ ) : undefined}
+ <Row
+ name={i18n.str`Amount`}
+ value={
+ <RenderAmountBulma
+ value={sendAmount}
+ specMap={config.currencies}
+ />
+ }
+ />
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 2:</i18n.Translate>
+
+ <i18n.Translate>
+ Copy this string and paste it into the 'Subject' or
+ 'Purpose' field in your preferred banking app or online
+ banking website.
+ </i18n.Translate>
+ </td>
+ </tr>
+ {instructions.subject.type === "CH_QR_BILL" ? (
+ <Row name={i18n.str`Reference`} value={instructions.subject.qr_reference_number} literal />
+ ) : undefined}
+ {instructions.subject.type === "SIMPLE" ? (
+ <Row name={i18n.str`Subject`} value={instructions.subject.subject} literal />
+ ) : undefined}
+ {receiverPostalCode ? (
+ <Row
+ name={i18n.str`Receiver postal code`}
+ value={receiverPostalCode}
+ />
+ ) : undefined}
+ {receiverTown ? (
+ <Row name={i18n.str`Receiver town`} value={receiverTown} />
+ ) : undefined}
+
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 3:</i18n.Translate>
+
+ <i18n.Translate>
+ Select the smallest possible amount for a wire transfer in
+ your preferred banking app or online banking website.
+ </i18n.Translate>
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={3}>
+ <b>
+ <i18n.Translate>
+ Make sure ALL data is correct, especially the subject,
+ and that you are choosing the bank account from which
+ you definitely want to make the wire transfer. You can
+ use the copy buttons (<CopyIcon />) to avoid typos or
+ make use of the PayTo URI below to copy just one value.
+ </i18n.Translate>
+ </b>
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2} width="100%">
+ <i18n.Translate>
+ As an alternative, in case your bank already supports the{" "}
+ <a
+ target="_blank"
+ rel="noreferrer"
+ title="RFC 8905 for designating targets for payments"
+ href="https://tools.ietf.org/html/rfc8905"
+ >
+ PayTo URI standard
+ </a>
+ , you can use this{" "}
+ <a href={Paytos.toFullString(payto)}>PayTo URI</a> link
+ instead.
+ </i18n.Translate>
+ </td>
+ <td>
+ <CopyButton getContent={() => Paytos.toFullString(payto)} />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </ConfirmModal>
+ );
+ }
+ default:
+ assertUnreachable(instructions.subject);
+ }
}
function Accordion({
diff --git a/packages/taler-merchant-webui/src/paths/instance/kyc/list/index.tsx b/packages/taler-merchant-webui/src/paths/instance/kyc/list/index.tsx
@@ -163,6 +163,8 @@ function ShowInstructionForKycRedirect({
targets={tgs}
onCancel={onCancel}
currency={e.exchange_currency}
+ exchangeBaseUrl={e.exchange_url}
+ wireAccountHash={e.h_wire}
/>
);
case TalerMerchantApi.MerchantAccountKycStatus.NO_EXCHANGE_KEY: {