taler-typescript-core

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

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:
Mpackages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx | 469+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mpackages/taler-wallet-webextension/src/cta/Withdraw/state.ts | 7++-----
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 &apos;Add Recipient&apos; 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 &apos;Add Recipient&apos; 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;