commit 7908944cbc91687752b4f7f961814d7691246c88
parent c7875d8949e88e3381faaae1f48276e3e749fa8e
Author: Sebastian <sebasjm@taler-systems.com>
Date: Thu, 11 Jun 2026 16:32:02 -0300
fix #11481 for manual withdrawal
Diffstat:
2 files changed, 279 insertions(+), 197 deletions(-)
diff --git a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
@@ -19,17 +19,16 @@ import {
Amounts,
AmountString,
assertUnreachable,
- parsePaytoUri,
+ encodeCrock,
Paytos,
- PaytoUriCyclos,
- PaytoUriIBAN,
- PaytoUriTaler,
- PaytoUriTalerBank,
- PaytoUriTalerHttp,
- PaytoUriUnknown,
+ PaytoType,
+ QrCodeSpec,
+ Result,
segwitMinAmount,
- stringifyPaytoUri,
- stringifyPayUri,
+ TransferOption,
+ TransferOptionPayto,
+ TransferOptionSwissQrBill,
+ TransferOptionUri,
TranslatedString,
WithdrawalExchangeAccountDetails,
} from "@gnu-taler/taler-util";
@@ -56,6 +55,34 @@ export interface BankDetailsProps {
accounts: WithdrawalExchangeAccountDetails[];
}
+function accountOrder(
+ a: WithdrawalExchangeAccountDetails,
+ b: WithdrawalExchangeAccountDetails,
+) {
+ const prio = (b.priority ?? 0) - (a.priority ?? 0);
+ if (prio !== 0) return prio;
+ return a.paytoUri.localeCompare(b.paytoUri);
+}
+function optionOrder(a: TransferOption, b: TransferOption) {
+ const type = a.type.localeCompare(b.type);
+ if (type !== 0) return type;
+ switch (a.type) {
+ case "payto":
+ return a.paytoUri.localeCompare((b as TransferOptionPayto).paytoUri);
+ case "uri":
+ return a.uri.localeCompare((b as TransferOptionUri).uri);
+ case "ch-qr-bill":
+ return a.paytoUri.localeCompare(
+ (b as TransferOptionSwissQrBill).paytoUri,
+ );
+ default:
+ assertUnreachable(a);
+ }
+}
+
+type OptionWithAccount = TransferOption & {
+ account: WithdrawalExchangeAccountDetails;
+};
export function BankDetailsByPaytoType({
subject,
amount,
@@ -68,144 +95,184 @@ export function BankDetailsByPaytoType({
return <div>the exchange account list is empty</div>;
}
- const accounts = unsortedAccounts.sort((a, b) => {
- return (b.priority ?? 0) - (a.priority ?? 0);
- });
-
- const selectedAccount = accounts[index];
- const altCurrency = selectedAccount.currencySpecification?.name;
-
- const payto = parsePaytoUri(selectedAccount.paytoUri);
-
- if (!payto) return <Fragment />;
- // make sure the payto has the right params
- payto.params["amount"] = altCurrency
- ? selectedAccount.transferAmount!
- : Amounts.stringify(amount);
- payto.params["message"] = subject;
-
- if (payto.isKnown && payto.targetType === "bitcoin") {
- const min = segwitMinAmount(amount.currency);
- const addrs = payto.segwitAddrs.map(
- (a) => `${a} ${Amounts.stringifyValue(min)}`,
- );
- addrs.unshift(`${payto.targetPath} ${Amounts.stringifyValue(amount)}`);
- const copyContent = addrs.join("\n");
- return (
- <Frame
- title={i18n.str`Bitcoin transfer details`}
- accounts={accounts}
- updateIndex={setIndex}
- currentIndex={index}
- defaultCurrency={amount.currency}
- >
- <p>
- <i18n.Translate>
- The exchange need a transaction with 3 output, one output is the
- exchange account and the other two are segwit fake address for
- metadata with an minimum amount.
- </i18n.Translate>
- </p>
-
- <p>
- <i18n.Translate>
- In bitcoincore wallet use 'Add Recipient' button to add
- two additional recipient and copy addresses and amounts
- </i18n.Translate>
- </p>
- <table>
- <tr>
- <td>
- <div>
- {payto.targetPath} <Amount value={amount} hideCurrency /> BTC
- </div>
- {payto.segwitAddrs.map((addr, i) => (
- <div key={i}>
- {addr} <Amount value={min} hideCurrency /> BTC
- </div>
- ))}
- </td>
- <td></td>
- <td>
- <CopyButton getContent={() => copyContent} />
- </td>
- </tr>
- </table>
- <p>
- <i18n.Translate>
- Make sure the amount show{" "}
- {Amounts.stringifyValue(Amounts.sum([amount, min, min]).amount)}{" "}
- BTC, else you have to change the base unit to BTC
- </i18n.Translate>
- </p>
- </Frame>
- );
- }
+ const accounts = unsortedAccounts.sort(accountOrder);
+ const options = accounts.flatMap((account) =>
+ account.transferOptions.sort(optionOrder).map(
+ (op): OptionWithAccount => ({
+ ...op,
+ account,
+ }),
+ ),
+ );
- if (payto.isKnown && payto.targetType === "ethereum") {
- const min = segwitMinAmount(amount.currency);
- const copyContent = `${payto.targetPath} ${Amounts.stringifyValue(amount)}`;
- return (
- <Frame
- title={i18n.str`Ethereum transfer details`}
- accounts={accounts}
- updateIndex={setIndex}
- currentIndex={index}
- defaultCurrency={amount.currency}
- >
- <p>
- <i18n.Translate>
- You need to wire to the service provider account.
- </i18n.Translate>
- </p>
+ const selectedOption = options[index];
+ const altCurrency = selectedOption.account.currencySpecification?.name;
+
+ switch (selectedOption.type) {
+ case "ch-qr-bill":
+ case "payto": {
+ const payto = Result.unpack(Paytos.fromString(selectedOption.paytoUri));
+ // make sure the payto has the right params
+ payto.params["amount"] = altCurrency
+ ? selectedOption.account.transferAmount!
+ : Amounts.stringify(amount);
+ payto.params["message"] = subject;
+
+ if (payto.targetType === PaytoType.Bitcoin) {
+ const min = segwitMinAmount(amount.currency);
+ const addrs = payto.segwitAddrs.map(
+ (a) => `${a} ${Amounts.stringifyValue(min)}`,
+ );
+ addrs.unshift(`${payto.address} ${Amounts.stringifyValue(amount)}`);
+ const copyContent = addrs.join("\n");
+ return (
+ <Frame
+ title={i18n.str`Bitcoin transfer details`}
+ accounts={options}
+ updateIndex={setIndex}
+ currentIndex={index}
+ defaultCurrency={amount.currency}
+ >
+ <p>
+ <i18n.Translate>
+ The exchange need a transaction with 3 output, one output is the
+ exchange account and the other two are segwit fake address for
+ metadata with an minimum amount.
+ </i18n.Translate>
+ </p>
- <table>
- <tr>
- <td>
- <div>
- {payto.targetPath} <Amount value={amount} hideCurrency /> ETH
- </div>
- </td>
- <td></td>
- <td>
- <CopyButton getContent={() => copyContent} />
- </td>
- </tr>
- </table>
- <p>
+ <p>
+ <i18n.Translate>
+ In bitcoincore wallet use 'Add Recipient' button to
+ add two additional recipient and copy addresses and amounts
+ </i18n.Translate>
+ </p>
+ <table>
+ <tr>
+ <td>
+ <div>
+ {payto.address} <Amount value={amount} hideCurrency /> BTC
+ </div>
+ {payto.segwitAddrs.map((addr, i) => (
+ <div key={i}>
+ {addr} <Amount value={min} hideCurrency /> BTC
+ </div>
+ ))}
+ </td>
+ <td></td>
+ <td>
+ <CopyButton getContent={() => copyContent} />
+ </td>
+ </tr>
+ </table>
+ <p>
+ <i18n.Translate>
+ Make sure the amount show{" "}
+ {Amounts.stringifyValue(Amounts.sum([amount, min, min]).amount)}{" "}
+ BTC, else you have to change the base unit to BTC
+ </i18n.Translate>
+ </p>
+ </Frame>
+ );
+ }
+
+ if (payto.targetType === PaytoType.Ethereum) {
+ const min = segwitMinAmount(amount.currency);
+ const copyContent = `${payto.address} ${Amounts.stringifyValue(amount)}`;
+ return (
+ <Frame
+ title={i18n.str`Ethereum transfer details`}
+ accounts={options}
+ updateIndex={setIndex}
+ currentIndex={index}
+ defaultCurrency={amount.currency}
+ >
+ <p>
+ <i18n.Translate>
+ You need to wire to the service provider account.
+ </i18n.Translate>
+ </p>
+
+ <table>
+ <tr>
+ <td>
+ <div>
+ {payto.address} <Amount value={amount} hideCurrency /> ETH
+ </div>
+ </td>
+ <td></td>
+ <td>
+ <CopyButton getContent={() => copyContent} />
+ </td>
+ </tr>
+ </table>
+ <p>
+ <i18n.Translate>
+ Make sure the amount show {Amounts.stringifyValue(amount)} ETH
+ </i18n.Translate>
+ </p>
+ </Frame>
+ );
+ }
+
+ return (
+ <Frame
+ title={i18n.str`Bank transfer details`}
+ accounts={options}
+ updateIndex={setIndex}
+ currentIndex={index}
+ defaultCurrency={amount.currency}
+ >
+ <IBANAccountInfoTable
+ payto={payto}
+ qrCodes={selectedOption.qrCodes}
+ refCode={
+ selectedOption.type === "ch-qr-bill"
+ ? selectedOption.qrReferenceNumber
+ : undefined
+ }
+ subject={subject}
+ />
+ </Frame>
+ );
+ }
+ case "uri": {
+ return (
+ <Frame
+ title={i18n.str`URI details`}
+ accounts={options}
+ updateIndex={setIndex}
+ currentIndex={index}
+ defaultCurrency={amount.currency}
+ >
<i18n.Translate>
- Make sure the amount show {Amounts.stringifyValue(amount)} ETH
+ The selected option is not currently supported. URI:{" "}
+ {selectedOption.uri}
</i18n.Translate>
- </p>
- </Frame>
- );
+ </Frame>
+ );
+ }
+ default:
+ assertUnreachable(selectedOption);
}
-
- return (
- <Frame
- title={i18n.str`Bank transfer details`}
- accounts={accounts}
- updateIndex={setIndex}
- currentIndex={index}
- defaultCurrency={amount.currency}
- >
- <IBANAccountInfoTable payto={payto} subject={subject} />
- </Frame>
- );
}
function IBANAccountInfoTable({
payto,
+ qrCodes,
subject,
+ refCode,
}: {
subject: string;
+ qrCodes: QrCodeSpec[];
+ refCode?: string;
payto:
- | PaytoUriUnknown
- | PaytoUriIBAN
- | PaytoUriTalerBank
- | PaytoUriCyclos
- | PaytoUriTalerHttp
- | PaytoUriTaler;
+ | Paytos.PaytoUnsupported
+ | Paytos.PaytoIBAN
+ | Paytos.PaytoTalerReserve
+ | Paytos.PaytoCyclos
+ | Paytos.PaytoTalerReserveHttp
+ | Paytos.PaytoTalerBank;
}) {
const { i18n } = useTranslationContext();
const api = useBackendContext();
@@ -213,50 +280,17 @@ function IBANAccountInfoTable({
const [showQrs, setShowQrs] = useState(false);
const hook = useAsyncAsHook(async () => {
- const qrs = await api.wallet.call(WalletApiOperation.GetQrCodesForPayto, {
- paytoUri: stringifyPaytoUri(payto),
- });
const banks = await api.wallet.call(
WalletApiOperation.GetBankingChoicesForPayto,
{
- paytoUri: stringifyPaytoUri(payto),
+ paytoUri: Paytos.toFullString(payto),
},
);
- return { qrs, banks };
+ return { banks };
}, []);
- const qrCodes = !hook || hook.hasError ? [] : hook.response.qrs.codes;
const banksSites = !hook || hook.hasError ? [] : hook.response.banks.choices;
- const accountPart = !payto.isKnown ? (
- <Fragment>
- <Row name={i18n.str`Account`} value={payto.targetPath} />
- </Fragment>
- ) : payto.targetType === "x-taler-bank" ? (
- <Fragment>
- <Row name={i18n.str`Bank host`} value={payto.host} />
- <Row name={i18n.str`Bank account`} value={payto.account} />
- </Fragment>
- ) : payto.targetType === "cyclos" ? (
- <Fragment>
- <Row name={i18n.str`Cyclos host`} value={payto.host} />
- <Row name={i18n.str`Account id`} value={payto.account} />
- </Fragment>
- ) : payto.targetType === "iban" ? (
- <Fragment>
- {payto.bic !== undefined ? (
- <Row name={i18n.str`BIC`} value={payto.bic} />
- ) : undefined}
- <Row name={i18n.str`IBAN`} value={payto.iban} />
- </Fragment>
- ) : payto.targetType === "taler-reserve" ||
- payto.targetType === "taler-reserve-http" ? (
- <Fragment>
- <Row name={i18n.str`Exchange`} value={payto.exchange} />
- <Row name={i18n.str`Reserve Pub`} value={payto.reservePub} />
- </Fragment>
- ) : undefined;
-
const receiverName =
payto.params["receiver-name"] || payto.params["receiver"] || undefined;
@@ -264,6 +298,7 @@ function IBANAccountInfoTable({
const receiverTown = payto.params["receiver-town"] || undefined;
+ const referenceCode = refCode;
return (
<Fragment>
<tr>
@@ -331,7 +366,54 @@ function IBANAccountInfoTable({
</i18n.Translate>
</td>
</tr>
- {accountPart}
+ {(function () {
+ switch (payto.targetType) {
+ case undefined:
+ return (
+ <Fragment>
+ <Row name={i18n.str`Account`} value={payto.displayName} />
+ </Fragment>
+ );
+ case PaytoType.IBAN:
+ return (
+ <Fragment>
+ {payto.bic !== undefined ? (
+ <Row name={i18n.str`BIC`} value={payto.bic} />
+ ) : undefined}
+ <Row name={i18n.str`IBAN`} value={payto.iban} />
+ </Fragment>
+ );
+ case PaytoType.Cyclos:
+ return (
+ <Fragment>
+ <Row name={i18n.str`Cyclos host`} value={payto.url} />
+ <Row name={i18n.str`Account id`} value={payto.account} />
+ </Fragment>
+ );
+
+ case PaytoType.TalerBank:
+ return (
+ <Fragment>
+ <Row name={i18n.str`Bank host`} value={payto.url} />
+ <Row name={i18n.str`Bank account`} value={payto.account} />
+ </Fragment>
+ );
+ case PaytoType.TalerReserve:
+ case PaytoType.TalerReserveHttp:
+ return (
+ <Fragment>
+ <Row name={i18n.str`Exchange`} value={payto.exchange} />
+ <Row
+ name={i18n.str`Reserve Pub`}
+ value={encodeCrock(payto.reservePub)}
+ />
+ </Fragment>
+ );
+
+ default:
+ assertUnreachable(payto);
+ }
+ })()}
{receiverName ? (
<Row name={i18n.str`Receiver name`} value={receiverName} />
) : undefined}
@@ -344,6 +426,13 @@ function IBANAccountInfoTable({
{receiverTown ? (
<Row name={i18n.str`Receiver town`} value={receiverTown} />
) : undefined}
+ {referenceCode ? (
+ <Row
+ name={i18n.str`Reference code`}
+ value={referenceCode}
+ literal
+ />
+ ) : undefined}
<tr>
<td colSpan={3}>
@@ -393,11 +482,11 @@ function IBANAccountInfoTable({
PayTo URI standard
</a>
, you can use this{" "}
- <a href={stringifyPaytoUri(payto)}>PayTo URI</a> link instead.
+ <a href={Paytos.toFullString(payto)}>PayTo URI</a> link instead.
</i18n.Translate>
</td>
<td>
- <CopyButton getContent={() => stringifyPaytoUri(payto)} />
+ <CopyButton getContent={() => Paytos.toFullString(payto)} />
</td>
</tr>
@@ -428,8 +517,14 @@ function IBANAccountInfoTable({
) : undefined}
</Fragment>
)}
+ </tbody>
+ </table>
+ </Fragment>
+ );
+}
- {/* {qrCodes.length < 1 ? undefined : (
+{
+ /* {qrCodes.length < 1 ? undefined : (
<Fragment>
<div>
<a
@@ -452,11 +547,7 @@ function IBANAccountInfoTable({
/>
) : undefined}
</Fragment>
- )} */}
- </tbody>
- </table>
- </Fragment>
- );
+ )} */
}
function CopyButton({ getContent }: { getContent: () => string }): VNode {
@@ -542,8 +633,9 @@ function Frame({
currentIndex: number;
updateIndex: (idx: number) => void;
defaultCurrency: string;
- accounts: WithdrawalExchangeAccountDetails[];
+ accounts: OptionWithAccount[];
}): VNode {
+ const { i18n } = useTranslationContext();
return (
<section
style={{
@@ -564,12 +656,12 @@ function Frame({
<div></div>
</div>
- {children}
-
{accounts.length > 1 ? (
<Fragment>
{accounts.map((ac, acIdx) => {
- const accountLabel = ac.bankLabel ?? `Account #${acIdx + 1}`;
+ // const accountLabel =
+ // ac.account.bankLabel ?? i18n.str`Option #${acIdx + 1}`;
+ const desc = ac.type === "ch-qr-bill" ? i18n.str`Swiss Bill` : i18n.str`Wire transfer`
return (
<Button
key={acIdx}
@@ -578,21 +670,14 @@ function Frame({
updateIndex(acIdx);
}}
>
- {accountLabel} (
- {ac.currencySpecification?.name ?? defaultCurrency})
+ {ac.account.currencySpecification?.name ?? defaultCurrency} - {desc}
</Button>
);
})}
-
- {/* <Button variant={currency === altCurrency ? "contained" : "outlined"}
- onClick={async () => {
- setCurrency(altCurrency)
- }}
- >
- <i18n.Translate>{altCurrency}</i18n.Translate>
- </Button> */}
</Fragment>
) : undefined}
+
+ {children}
</section>
);
}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
@@ -455,12 +455,9 @@ function exchangeSelectionState(
} else {
onSuccess(res.transactionId);
}
- } catch (e) {
- console.error(e);
- // if (e instanceof TalerError) {
- // }
+ } finally {
+ setDoingWithdraw(false);
}
- setDoingWithdraw(false);
}
const toBeSent = amountHook.response.amount.raw;