gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-taler-android] 01/12: [wallet] Initial implementation of DD36.


From: gnunet
Subject: [taler-taler-android] 01/12: [wallet] Initial implementation of DD36.
Date: Mon, 08 Jan 2024 21:58:34 +0100

This is an automated email from the git hooks/post-receive script.

torsten-grote pushed a commit to branch master
in repository taler-android.

commit 8936e2d7adf6bd25a2cacdc18dc9e31db1cec8d2
Author: Iván Ávalos <avalos@disroot.org>
AuthorDate: Tue Dec 5 13:01:40 2023 -0600

    [wallet] Initial implementation of DD36.
---
 .../taler/wallet/balances/CurrencySpecification.kt |  28 ++
 .../taler/wallet/compose/QrCodeUriComposable.kt    |  21 +-
 .../java/net/taler/wallet/compose/ShareButton.kt   |  26 +-
 .../transactions/TransactionWithdrawalFragment.kt  |  10 +-
 .../net/taler/wallet/transactions/Transactions.kt  |  70 ++++-
 .../withdraw/TransactionWithdrawalComposable.kt    |  42 ++-
 .../net/taler/wallet/withdraw/WithdrawManager.kt   | 155 +++++++----
 .../manual/ManualWithdrawSuccessFragment.kt        |  63 +++--
 .../taler/wallet/withdraw/manual/ScreenBitcoin.kt  | 169 ------------
 .../net/taler/wallet/withdraw/manual/ScreenIBAN.kt | 160 -----------
 .../taler/wallet/withdraw/manual/ScreenTransfer.kt | 301 +++++++++++++++++++++
 .../wallet/withdraw/manual/TransferBitcoin.kt      | 110 ++++++++
 .../taler/wallet/withdraw/manual/TransferIBAN.kt   |  86 ++++++
 wallet/src/main/res/values/strings.xml             |   6 +
 14 files changed, 786 insertions(+), 461 deletions(-)

diff --git 
a/wallet/src/main/java/net/taler/wallet/balances/CurrencySpecification.kt 
b/wallet/src/main/java/net/taler/wallet/balances/CurrencySpecification.kt
new file mode 100644
index 0000000..2297c21
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/balances/CurrencySpecification.kt
@@ -0,0 +1,28 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.balances
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CurrencySpecification(
+    val name: String,
+    val numFractionalInputDigits: Int,
+    val numFractionalNormalDigits: Int,
+    val numFractionalTrailingZeroDigits: Int,
+    val altUnitNames: Map<String, String>,
+)
\ No newline at end of file
diff --git 
a/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt 
b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt
index 2d7ffa1..4991094 100644
--- a/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt
@@ -25,21 +25,21 @@ import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ContentCopy
 import androidx.compose.material3.Button
 import androidx.compose.material3.ButtonColors
 import androidx.compose.material3.ButtonDefaults
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ContentCopy
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.produceState
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment.Companion.CenterHorizontally
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.asImageBitmap
@@ -136,14 +136,13 @@ fun CopyToClipboardButton(
         colors = colors,
         onClick = { copyToClipBoard(context, label, content) },
     ) {
-        Row(verticalAlignment = Alignment.CenterVertically) {
-            Icon(Icons.Default.ContentCopy, buttonText)
-            Text(
-                modifier = Modifier.padding(start = 8.dp),
-                text = buttonText,
-                style = MaterialTheme.typography.bodyLarge,
-            )
-        }
+        Icon(
+            Icons.Default.ContentCopy,
+            buttonText,
+            modifier = Modifier.size(ButtonDefaults.IconSize),
+        )
+        Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+        Text(buttonText)
     }
 }
 
diff --git a/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt 
b/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt
index ebf2a2f..f3a84dd 100644
--- a/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt
+++ b/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt
@@ -19,22 +19,19 @@ package net.taler.wallet.compose
 import android.content.Intent
 import android.content.Intent.ACTION_SEND
 import android.content.Intent.EXTRA_TEXT
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Share
 import androidx.compose.material3.Button
 import androidx.compose.material3.ButtonColors
 import androidx.compose.material3.ButtonDefaults
 import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Share
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment.Companion.CenterVertically
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
 import androidx.core.content.ContextCompat.startActivity
 import net.taler.wallet.R
 
@@ -59,13 +56,12 @@ fun ShareButton(
             startActivity(context, shareIntent, null)
         },
     ) {
-        Row(verticalAlignment = CenterVertically) {
-            Icon(Icons.Default.Share, buttonText)
-            Text(
-                modifier = Modifier.padding(start = 8.dp),
-                text = buttonText,
-                style = MaterialTheme.typography.bodyLarge,
-            )
-        }
+        Icon(
+            Icons.Default.Share,
+            buttonText,
+            modifier = Modifier.size(ButtonDefaults.IconSize),
+        )
+        Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+        Text(buttonText)
     }
 }
diff --git 
a/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt
 
b/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt
index 0cd6d60..234a2dd 100644
--- 
a/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt
+++ 
b/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt
@@ -77,13 +77,13 @@ class TransactionWithdrawalFragment : 
TransactionDetailFragment(), ActionListene
             ActionListener.Type.CONFIRM_MANUAL -> {
                 if (tx !is TransactionWithdrawal) return
                 if (tx.withdrawalDetails !is ManualTransfer) return
-                // TODO what if there's more than one or no URI?
-                if (tx.withdrawalDetails.exchangePaytoUris.isEmpty()) return
+                if 
(tx.withdrawalDetails.exchangeCreditAccountDetails?.isEmpty() != false) return
                 val status = createManualTransferRequired(
-                    amount = tx.amountRaw,
-                    exchangeBaseUrl = tx.exchangeBaseUrl,
-                    uriStr = tx.withdrawalDetails.exchangePaytoUris[0],
                     transactionId = tx.transactionId,
+                    exchangeBaseUrl = tx.exchangeBaseUrl,
+                    amountRaw = tx.amountRaw,
+                    amountEffective = tx.amountEffective,
+                    withdrawalAccountList = 
tx.withdrawalDetails.exchangeCreditAccountDetails,
                 )
                 withdrawManager.viewManualWithdrawal(status)
                 findNavController().navigate(
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt 
b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
index e7f17c0..681fadb 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
@@ -42,6 +42,7 @@ import net.taler.wallet.TAG
 import net.taler.wallet.backend.TalerErrorCode
 import net.taler.wallet.backend.TalerErrorInfo
 import net.taler.wallet.cleanExchange
+import net.taler.wallet.balances.CurrencySpecification
 import net.taler.wallet.refund.RefundPaymentInfo
 import net.taler.wallet.transactions.TransactionMajorState.None
 import net.taler.wallet.transactions.TransactionMajorState.Pending
@@ -182,12 +183,7 @@ sealed class WithdrawalDetails {
     @Serializable
     @SerialName("manual-transfer")
     class ManualTransfer(
-        /**
-         * Payto URIs that the exchange supports.
-         *
-         * Already contains the amount and message.
-         */
-        val exchangePaytoUris: List<String>,
+        val exchangeCreditAccountDetails: 
List<WithdrawalExchangeAccountDetails>? = null,
     ) : WithdrawalDetails()
 
     @Serializable
@@ -208,6 +204,68 @@ sealed class WithdrawalDetails {
     ) : WithdrawalDetails()
 }
 
+@Serializable
+data class WithdrawalExchangeAccountDetails (
+    /**
+     * Payto URI to credit the exchange.
+     *
+     * Depending on whether the (manual!) withdrawal is accepted or just
+     * being checked, this already includes the subject with the
+     * reserve public key.
+     */
+    val paytoUri: String,
+
+    /**
+     * Transfer amount. Might be in a different currency than the requested
+     * amount for withdrawal.
+     *
+     * Redundant with the amount in paytoUri, just included to avoid parsing.
+     */
+    val transferAmount: Amount? = null,
+
+    /**
+     * Currency specification for the external currency.
+     *
+     * Only included if this account requires a currency conversion.
+     */
+    val currencySpecification: CurrencySpecification? = null,
+
+    /**
+     * Further restrictions for sending money to the
+     * exchange.
+     */
+    val creditRestrictions: List<AccountRestriction>? = null,
+)
+
+@Serializable
+sealed class AccountRestriction {
+    @Serializable
+    @SerialName("deny")
+    data object DenyAllAccount: AccountRestriction()
+
+    @Serializable
+    @SerialName("regex")
+    data class RegexAccount(
+        // Regular expression that the payto://-URI of the
+        // partner account must follow.  The regular expression
+        // should follow posix-egrep, but without support for character
+        // classes, GNU extensions, back-references or intervals. See
+        // 
https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html
+        // for a description of the posix-egrep syntax. Applications
+        // may support regexes with additional features, but exchanges
+        // must not use such regexes.
+        val paytoRegex: String,
+
+        // Hint for a human to understand the restriction
+        // (that is hopefully easier to comprehend than the regex itself).
+        val humanHint: String,
+
+        // Map from IETF BCP 47 language tags to localized
+        // human hints.
+        val humanHintI18n: Map<String, String>? = null,
+    ): AccountRestriction()
+}
+
 @Serializable
 @SerialName("payment")
 class TransactionPayment(
diff --git 
a/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt
 
b/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt
index 378e283..fda1815 100644
--- 
a/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt
+++ 
b/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt
@@ -38,6 +38,7 @@ import net.taler.wallet.R
 import net.taler.wallet.backend.TalerErrorCode
 import net.taler.wallet.backend.TalerErrorInfo
 import net.taler.wallet.cleanExchange
+import net.taler.wallet.balances.CurrencySpecification
 import net.taler.wallet.transactions.ActionButton
 import net.taler.wallet.transactions.ActionListener
 import net.taler.wallet.transactions.AmountType
@@ -54,6 +55,7 @@ import net.taler.wallet.transactions.TransactionState
 import net.taler.wallet.transactions.TransactionWithdrawal
 import net.taler.wallet.transactions.TransitionsComposable
 import net.taler.wallet.transactions.WithdrawalDetails.ManualTransfer
+import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails
 
 @Composable
 fun TransactionWithdrawalComposable(
@@ -75,17 +77,15 @@ fun TransactionWithdrawalComposable(
             text = t.timestamp.ms.toAbsoluteTime(context).toString(),
             style = MaterialTheme.typography.bodyLarge,
         )
-        TransactionAmountComposable(
-            label = stringResource(id = R.string.withdraw_total),
-            amount = t.amountEffective,
-            amountType = AmountType.Positive,
-        )
+
         ActionButton(tx = t, listener = actionListener)
+
         TransactionAmountComposable(
-            label = stringResource(id = R.string.amount_chosen),
+            label = stringResource(R.string.amount_chosen),
             amount = t.amountRaw,
             amountType = AmountType.Neutral,
         )
+
         val fee = t.amountRaw - t.amountEffective
         if (!fee.isZero()) {
             TransactionAmountComposable(
@@ -94,11 +94,20 @@ fun TransactionWithdrawalComposable(
                 amountType = AmountType.Negative,
             )
         }
+
+        TransactionAmountComposable(
+            label = stringResource(id = R.string.withdraw_total),
+            amount = t.amountEffective,
+            amountType = AmountType.Positive,
+        )
+
         TransactionInfoComposable(
             label = stringResource(id = R.string.withdraw_exchange),
             info = cleanExchange(t.exchangeBaseUrl),
         )
+        
         TransitionsComposable(t, devMode, onTransition)
+
         if (devMode && t.error != null) {
             ErrorTransactionButton(error = t.error)
         }
@@ -114,15 +123,30 @@ fun TransactionWithdrawalComposablePreview() {
         txState = TransactionState(Pending),
         txActions = listOf(Retry, Suspend, Abort),
         exchangeBaseUrl = "https://exchange.demo.taler.net/";,
-        withdrawalDetails = ManualTransfer(exchangePaytoUris = emptyList()),
+        withdrawalDetails = ManualTransfer(
+            exchangeCreditAccountDetails = listOf(
+                WithdrawalExchangeAccountDetails(
+                    paytoUri = "payto://IBAN/1231231231",
+                    transferAmount = Amount.fromJSONString("NETZBON:42.23"),
+                    currencySpecification = CurrencySpecification(
+                        name = "NETZBON",
+                        numFractionalInputDigits = 2,
+                        numFractionalNormalDigits = 2,
+                        numFractionalTrailingZeroDigits = 2,
+                        altUnitNames = mapOf("0" to "NETZBON"),
+                    ),
+                ),
+            ),
+        ),
         amountRaw = Amount.fromString("TESTKUDOS", "42.23"),
         amountEffective = Amount.fromString("TESTKUDOS", "42.1337"),
         error = TalerErrorInfo(code = 
TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
     )
+
     val listener = object : ActionListener {
-        override fun onActionButtonClicked(tx: Transaction, type: 
ActionListener.Type) {
-        }
+        override fun onActionButtonClicked(tx: Transaction, type: 
ActionListener.Type) {}
     }
+
     Surface {
         TransactionWithdrawalComposable(t, true, listener) {}
     }
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt 
b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
index 90b8570..4661946 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
@@ -33,10 +33,12 @@ import net.taler.wallet.backend.TalerErrorInfo
 import net.taler.wallet.backend.WalletBackendApi
 import net.taler.wallet.exchanges.ExchangeFees
 import net.taler.wallet.exchanges.ExchangeItem
+import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails
 import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails
 
 sealed class WithdrawStatus {
     data class Loading(val talerWithdrawUri: String? = null) : WithdrawStatus()
+
     data class NeedsExchange(val exchangeSelection: Event<ExchangeSelection>) 
: WithdrawStatus()
 
     data class TosReviewRequired(
@@ -44,6 +46,8 @@ sealed class WithdrawStatus {
         val exchangeBaseUrl: String,
         val amountRaw: Amount,
         val amountEffective: Amount,
+        val numCoins: Int,
+        val withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
         val ageRestrictionOptions: List<Int>? = null,
         val tosText: String,
         val tosEtag: String,
@@ -55,36 +59,50 @@ sealed class WithdrawStatus {
         val exchangeBaseUrl: String,
         val amountRaw: Amount,
         val amountEffective: Amount,
+        val numCoins: Int,
+        val withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
         val ageRestrictionOptions: List<Int>? = null,
     ) : WithdrawStatus()
 
-    object Withdrawing : WithdrawStatus()
+    data object Withdrawing : WithdrawStatus()
+
     data class Success(val currency: String, val transactionId: String) : 
WithdrawStatus()
-    sealed class ManualTransferRequired : WithdrawStatus() {
-        abstract val uri: Uri
-        abstract val transactionId: String?
-    }
 
-    data class ManualTransferRequiredIBAN(
+    class ManualTransferRequired(
+        val transactionId: String?,
+        val transactionAmountRaw: Amount,
+        val transactionAmountEffective: Amount,
         val exchangeBaseUrl: String,
-        override val uri: Uri,
+        val withdrawalTransfers: List<TransferData>,
+    ) : WithdrawStatus()
+
+    data class Error(val message: String?) : WithdrawStatus()
+}
+
+sealed class TransferData {
+    abstract val subject: String
+    abstract val amountRaw: Amount
+    abstract val amountEffective: Amount
+    abstract val withdrawalAccount: WithdrawalExchangeAccountDetails
+
+    val currency get() = withdrawalAccount.transferAmount?.currency
+
+    data class IBAN(
+        override val subject: String,
+        override val amountRaw: Amount,
+        override val amountEffective: Amount,
+        override val withdrawalAccount: WithdrawalExchangeAccountDetails,
         val iban: String,
-        val subject: String,
-        val amountRaw: Amount,
-        override val transactionId: String?,
-    ) : ManualTransferRequired()
+    ): TransferData()
 
-    data class ManualTransferRequiredBitcoin(
-        val exchangeBaseUrl: String,
-        override val uri: Uri,
+    data class Bitcoin(
+        override val subject: String,
+        override val amountRaw: Amount,
+        override val amountEffective: Amount,
+        override val withdrawalAccount: WithdrawalExchangeAccountDetails,
         val account: String,
-        val segwitAddrs: List<String>,
-        val subject: String,
-        val amountRaw: Amount,
-        override val transactionId: String?,
-    ) : ManualTransferRequired()
-
-    data class Error(val message: String?) : WithdrawStatus()
+        val segwitAddresses: List<String>,
+    ): TransferData()
 }
 
 sealed class WithdrawTestStatus {
@@ -101,10 +119,12 @@ data class WithdrawalDetailsForUri(
 )
 
 @Serializable
-data class WithdrawalDetails(
+data class ManualWithdrawalDetails(
     val tosAccepted: Boolean,
     val amountRaw: Amount,
     val amountEffective: Amount,
+    val numCoins: Int,
+    val withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
     val ageRestrictionOptions: List<Int>? = null,
 )
 
@@ -115,7 +135,9 @@ data class AcceptWithdrawalResponse(
 
 @Serializable
 data class AcceptManualWithdrawalResponse(
-    val exchangePaytoUris: List<String>,
+    val reservePub: String,
+    val withdrawalAccountsList: List<WithdrawalExchangeAccountDetails>,
+    val transactionId: String,
 )
 
 data class ExchangeSelection(
@@ -176,7 +198,7 @@ class WithdrawManager(
         uri: String? = null,
     ) = scope.launch {
         withdrawStatus.value = WithdrawStatus.Loading(uri)
-        api.request("getWithdrawalDetailsForAmount", 
WithdrawalDetails.serializer()) {
+        api.request("getWithdrawalDetailsForAmount", 
ManualWithdrawalDetails.serializer()) {
             put("exchangeBaseUrl", exchangeBaseUrl)
             put("amount", amount.toJSONString())
         }.onError { error ->
@@ -188,6 +210,8 @@ class WithdrawManager(
                     exchangeBaseUrl = exchangeBaseUrl,
                     amountRaw = details.amountRaw,
                     amountEffective = details.amountEffective,
+                    numCoins = details.numCoins,
+                    withdrawalAccountList = details.withdrawalAccountList,
                     ageRestrictionOptions = details.ageRestrictionOptions,
                 )
             } else getExchangeTos(exchangeBaseUrl, details, 
showTosImmediately, uri)
@@ -196,7 +220,7 @@ class WithdrawManager(
 
     private fun getExchangeTos(
         exchangeBaseUrl: String,
-        details: WithdrawalDetails,
+        details: ManualWithdrawalDetails,
         showImmediately: Boolean,
         uri: String?,
     ) = scope.launch {
@@ -210,6 +234,8 @@ class WithdrawManager(
                 exchangeBaseUrl = exchangeBaseUrl,
                 amountRaw = details.amountRaw,
                 amountEffective = details.amountEffective,
+                numCoins = details.numCoins,
+                withdrawalAccountList = details.withdrawalAccountList,
                 ageRestrictionOptions = details.ageRestrictionOptions,
                 tosText = it.content,
                 tosEtag = it.currentEtag,
@@ -234,6 +260,8 @@ class WithdrawManager(
                 exchangeBaseUrl = s.exchangeBaseUrl,
                 amountRaw = s.amountRaw,
                 amountEffective = s.amountEffective,
+                numCoins = s.numCoins,
+                withdrawalAccountList = s.withdrawalAccountList,
                 ageRestrictionOptions = s.ageRestrictionOptions,
             )
         }
@@ -275,10 +303,8 @@ class WithdrawManager(
             handleError("acceptManualWithdrawal", it)
         }.onSuccess { response ->
             withdrawStatus.value = createManualTransferRequired(
-                amount = status.amountRaw,
-                exchangeBaseUrl = status.exchangeBaseUrl,
-                // TODO what if there's more than one or no URI?
-                uriStr = response.exchangePaytoUris[0],
+                status = status,
+                response = response,
             )
         }
     }
@@ -301,33 +327,48 @@ class WithdrawManager(
 }
 
 fun createManualTransferRequired(
-    amount: Amount,
+    transactionId: String,
     exchangeBaseUrl: String,
-    uriStr: String,
-    transactionId: String? = null,
-): WithdrawStatus.ManualTransferRequired {
-    val uri = Uri.parse(uriStr.replace("receiver-name=", "receiver_name="))
-    if ("bitcoin".equals(uri.authority, true)) {
-        val msg = uri.getQueryParameter("message").orEmpty()
-        val reg = "\\b([A-Z0-9]{52})\\b".toRegex().find(msg)
-        val reserve = reg?.value ?: uri.getQueryParameter("subject")!!
-        val segwitAddrs = Bech32.generateFakeSegwitAddress(reserve, 
uri.pathSegments.first())
-        return WithdrawStatus.ManualTransferRequiredBitcoin(
-            exchangeBaseUrl = exchangeBaseUrl,
-            uri = uri,
-            account = uri.lastPathSegment!!,
-            segwitAddrs = segwitAddrs,
-            subject = reserve,
-            amountRaw = amount,
-            transactionId = transactionId,
+    amountRaw: Amount,
+    amountEffective: Amount,
+    withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
+) = WithdrawStatus.ManualTransferRequired(
+    transactionId = transactionId,
+    transactionAmountRaw = amountRaw,
+    transactionAmountEffective = amountEffective,
+    exchangeBaseUrl = exchangeBaseUrl,
+    withdrawalTransfers = withdrawalAccountList.map {
+        val uri = Uri.parse(it.paytoUri.replace("receiver-name=", 
"receiver_name="))
+        if ("bitcoin".equals(uri.authority, true)) {
+            val msg = uri.getQueryParameter("message").orEmpty()
+            val reg = "\\b([A-Z0-9]{52})\\b".toRegex().find(msg)
+            val reserve = reg?.value ?: uri.getQueryParameter("subject")!!
+            val segwitAddresses = Bech32.generateFakeSegwitAddress(reserve, 
uri.pathSegments.first())
+            TransferData.Bitcoin(
+                account = uri.lastPathSegment!!,
+                segwitAddresses = segwitAddresses,
+                subject = reserve,
+                amountRaw = amountRaw,
+                amountEffective = amountEffective,
+                withdrawalAccount = it.copy(paytoUri = uri.toString())
+            )
+        } else TransferData.IBAN(
+            iban = uri.lastPathSegment!!,
+            subject = uri.getQueryParameter("message") ?: "Error: No message 
in URI",
+            amountRaw = amountRaw,
+            amountEffective = amountEffective,
+            withdrawalAccount = it.copy(paytoUri = uri.toString())
         )
-    }
-    return WithdrawStatus.ManualTransferRequiredIBAN(
-        exchangeBaseUrl = exchangeBaseUrl,
-        uri = uri,
-        iban = uri.lastPathSegment!!,
-        subject = uri.getQueryParameter("message") ?: "Error: No message in 
URI",
-        amountRaw = amount,
-        transactionId = transactionId,
-    )
-}
+    },
+)
+
+fun createManualTransferRequired(
+    status: ReceivedDetails,
+    response: AcceptManualWithdrawalResponse,
+): WithdrawStatus.ManualTransferRequired = createManualTransferRequired(
+    transactionId = response.transactionId,
+    exchangeBaseUrl = status.exchangeBaseUrl,
+    amountRaw = status.amountRaw,
+    amountEffective = status.amountEffective,
+    withdrawalAccountList = response.withdrawalAccountsList,
+)
\ No newline at end of file
diff --git 
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
 
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
index fa3f38b..8d83427 100644
--- 
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
+++ 
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
@@ -17,11 +17,12 @@
 package net.taler.wallet.withdraw.manual
 
 import android.content.Intent
+import android.net.Uri
 import android.os.Bundle
-import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.ui.platform.ComposeView
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
@@ -29,53 +30,57 @@ import androidx.navigation.fragment.findNavController
 import net.taler.common.startActivitySafe
 import net.taler.wallet.MainViewModel
 import net.taler.wallet.R
-import net.taler.wallet.TAG
 import net.taler.wallet.compose.TalerSurface
-import net.taler.wallet.showError
+import net.taler.wallet.withdraw.TransferData
 import net.taler.wallet.withdraw.WithdrawStatus
 
 class ManualWithdrawSuccessFragment : Fragment() {
     private val model: MainViewModel by activityViewModels()
-    private val transactionManager by lazy { model.transactionManager }
     private val withdrawManager by lazy { model.withdrawManager }
+
+    private lateinit var status: WithdrawStatus.ManualTransferRequired
+
     override fun onCreateView(
         inflater: LayoutInflater, container: ViewGroup?,
         savedInstanceState: Bundle?,
     ): View = ComposeView(requireContext()).apply {
-        val status = withdrawManager.withdrawStatus.value as 
WithdrawStatus.ManualTransferRequired
-        val intent = Intent().apply {
-            data = status.uri
-        }
-        // TODO test if this works with an actual payto:// handling app
-        val componentName = 
intent.resolveActivity(requireContext().packageManager)
-        val onBankAppClick = if (componentName == null) null else {
-            { requireContext().startActivitySafe(intent) }
-        }
-        val tid = status.transactionId
-        val onCancelClick = if (tid == null) null else {
-            {
-                transactionManager.deleteTransaction(tid) {
-                    Log.e(TAG, "Error deleteTransaction $it")
-                    showError(it)
+        status = withdrawManager.withdrawStatus.value as 
WithdrawStatus.ManualTransferRequired
+
+        // Set action bar subtitle and unset on exit
+        if (status.withdrawalTransfers.size > 1) {
+            val activity = requireActivity() as AppCompatActivity
+
+            activity.apply {
+                supportActionBar?.subtitle = 
getString(R.string.withdraw_subtitle)
+            }
+
+            findNavController().addOnDestinationChangedListener { controller, 
destination, args ->
+                if (destination.id != 
R.id.nav_exchange_manual_withdrawal_success) {
+                    activity.apply {
+                        supportActionBar?.subtitle = null
+                    }
                 }
-                
findNavController().navigate(R.id.action_nav_exchange_manual_withdrawal_success_to_nav_main)
             }
         }
+
         setContent {
             TalerSurface {
-                when (status) {
-                    is WithdrawStatus.ManualTransferRequiredBitcoin -> {
-                        ScreenBitcoin(status, onBankAppClick, onCancelClick)
-                    }
-
-                    is WithdrawStatus.ManualTransferRequiredIBAN -> {
-                        ScreenIBAN(status, onBankAppClick, onCancelClick)
-                    }
-                }
+                ScreenTransfer(
+                    status = status,
+                    bankAppClick = { onBankAppClick(it) },
+                )
             }
         }
     }
 
+    private fun onBankAppClick(transfer: TransferData) {
+        val intent = Intent().apply { data = 
Uri.parse(transfer.withdrawalAccount.paytoUri) }
+        val componentName = 
intent.resolveActivity(requireContext().packageManager)
+        if (componentName != null) {
+            requireContext().startActivitySafe(intent)
+        }
+    }
+
     override fun onStart() {
         super.onStart()
         activity?.setTitle(R.string.withdraw_title)
diff --git 
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt 
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt
deleted file mode 100644
index fa20072..0000000
--- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under 
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
- * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.withdraw.manual
-
-import android.net.Uri
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Alignment.Companion.End
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.em
-import net.taler.common.Amount
-import net.taler.wallet.CURRENCY_BTC
-import net.taler.wallet.R
-import net.taler.wallet.compose.CopyToClipboardButton
-import net.taler.wallet.withdraw.WithdrawStatus
-
-@Composable
-fun ScreenBitcoin(
-    status: WithdrawStatus.ManualTransferRequiredBitcoin,
-    bankAppClick: (() -> Unit)?,
-    onCancelClick: (() -> Unit)?,
-) {
-    val scrollState = rememberScrollState()
-    Column(modifier = Modifier
-        .wrapContentWidth(Alignment.CenterHorizontally)
-        .verticalScroll(scrollState)
-        .padding(all = 16.dp)
-    ) {
-        Text(
-            text = stringResource(R.string.withdraw_manual_bitcoin_title),
-            style = MaterialTheme.typography.headlineSmall,
-        )
-        Text(
-            text = stringResource(R.string.withdraw_manual_bitcoin_intro),
-            style = MaterialTheme.typography.bodyLarge,
-            modifier = Modifier
-                .padding(vertical = 8.dp)
-        )
-        BitcoinSegwitAddrs(
-            amount = status.amountRaw,
-            addr = status.account,
-            segwitAddresses = status.segwitAddrs
-        )
-        if (bankAppClick != null) {
-            Button(
-                onClick = bankAppClick,
-                modifier = Modifier
-                    .padding(vertical = 16.dp)
-                    .align(Alignment.CenterHorizontally),
-            ) {
-                Text(text = 
stringResource(R.string.withdraw_manual_ready_bank_button))
-            }
-        }
-        if (onCancelClick != null) {
-            Button(
-                onClick = onCancelClick,
-                colors = ButtonDefaults.buttonColors(containerColor = 
MaterialTheme.colorScheme.error),
-                modifier = Modifier
-                    .padding(vertical = 16.dp)
-                    .align(End),
-            ) {
-                Text(
-                    text = 
stringResource(R.string.withdraw_manual_ready_cancel),
-                    color = MaterialTheme.colorScheme.onError,
-                )
-            }
-        }
-    }
-}
-
-@Composable
-fun BitcoinSegwitAddrs(amount: Amount, addr: String, segwitAddresses: 
List<String>) {
-    Column {
-        CopyToClipboardButton(
-            modifier = Modifier.align(End),
-            label = "Bitcoin",
-            content = getCopyText(amount, addr, segwitAddresses),
-        )
-        Row(modifier = Modifier.padding(vertical = 8.dp)) {
-            Column(modifier = Modifier.weight(0.3f)) {
-                Text(
-                    text = addr,
-                    style = MaterialTheme.typography.bodyLarge,
-                    fontWeight = FontWeight.Normal,
-                    fontSize = 3.em
-                )
-                Text(
-                    text = amount.withCurrency("BTC").toString(),
-                    style = MaterialTheme.typography.bodyLarge,
-                    fontWeight = FontWeight.Bold,
-                )
-            }
-        }
-        for (segwitAddress in segwitAddresses) {
-            Row(modifier = Modifier.padding(vertical = 8.dp)) {
-                Column(modifier = Modifier.weight(0.3f)) {
-                    Text(
-                        text = segwitAddress,
-                        style = MaterialTheme.typography.bodyLarge,
-                        fontWeight = FontWeight.Normal,
-                        fontSize = 3.em,
-                    )
-                    Text(
-                        text = SEGWIT_MIN.toString(),
-                        style = MaterialTheme.typography.bodyLarge,
-                        fontWeight = FontWeight.Bold,
-                    )
-                }
-            }
-        }
-    }
-}
-
-private val SEGWIT_MIN = Amount("BTC", 0, 294)
-
-private fun getCopyText(amount: Amount, addr: String, segwitAddresses: 
List<String>): String {
-    val sr = segwitAddresses.joinToString(separator = "\n") { s ->
-        "\n$s ${SEGWIT_MIN}\n"
-    }
-    return "$addr ${amount.withCurrency("BTC")}\n$sr"
-}
-
-@Preview
-@Composable
-fun PreviewScreenBitcoin() {
-    Surface {
-        ScreenBitcoin(WithdrawStatus.ManualTransferRequiredBitcoin(
-            exchangeBaseUrl = "bitcoin.ice.bfh.ch",
-            uri = Uri.parse("https://taler.net";),
-            account = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
-            segwitAddrs = listOf(
-                "bc1qqleages8702xvg9qcyu02yclst24xurdrynvxq",
-                "bc1qsleagehks96u7jmqrzcf0fw80ea5g57qm3m84c"
-            ),
-            subject = "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
-            amountRaw = Amount(CURRENCY_BTC, 0, 14000000),
-            transactionId = "",
-        ), {}) {}
-    }
-}
diff --git 
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt 
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt
deleted file mode 100644
index 537f3ad..0000000
--- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under 
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
- * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.withdraw.manual
-
-import android.net.Uri
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ContentCopy
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import net.taler.common.Amount
-import net.taler.wallet.R
-import net.taler.wallet.compose.copyToClipBoard
-import net.taler.wallet.withdraw.WithdrawStatus
-
-@Composable
-fun ScreenIBAN(
-    status: WithdrawStatus.ManualTransferRequiredIBAN,
-    bankAppClick: (() -> Unit)?,
-    onCancelClick: (() -> Unit)?,
-) {
-    val scrollState = rememberScrollState()
-    Column(modifier = Modifier
-        .wrapContentWidth(Alignment.CenterHorizontally)
-        .verticalScroll(scrollState)
-        .padding(all = 16.dp)
-    ) {
-        Text(
-            text = stringResource(R.string.withdraw_manual_ready_title),
-            style = MaterialTheme.typography.headlineSmall,
-        )
-        Text(
-            text = stringResource(R.string.withdraw_manual_ready_intro,
-                status.amountRaw.toString()),
-            style = MaterialTheme.typography.bodyLarge,
-            modifier = Modifier
-                .padding(vertical = 8.dp)
-        )
-        DetailRow(stringResource(R.string.withdraw_manual_ready_iban), 
status.iban)
-        DetailRow(stringResource(R.string.withdraw_manual_ready_subject), 
status.subject)
-        DetailRow(stringResource(R.string.amount_chosen), 
status.amountRaw.toString())
-        DetailRow(stringResource(R.string.withdraw_exchange), 
status.exchangeBaseUrl, false)
-        Text(
-            text = stringResource(R.string.withdraw_manual_ready_warning),
-            style = MaterialTheme.typography.bodyMedium,
-            color = colorResource(R.color.notice_text),
-            modifier = Modifier
-                .align(Alignment.CenterHorizontally)
-                .padding(all = 8.dp)
-                .background(colorResource(R.color.notice_background))
-                .border(BorderStroke(2.dp, 
colorResource(R.color.notice_border)))
-                .padding(all = 16.dp)
-        )
-        if (bankAppClick != null) {
-            Button(
-                onClick = bankAppClick,
-                modifier = Modifier
-                    .padding(vertical = 16.dp)
-                    .align(Alignment.CenterHorizontally),
-            ) {
-                Text(text = 
stringResource(R.string.withdraw_manual_ready_bank_button))
-            }
-        }
-        if (onCancelClick != null) {
-            Button(
-                onClick = onCancelClick,
-                colors = ButtonDefaults.buttonColors(containerColor = 
MaterialTheme.colorScheme.error),
-                modifier = Modifier
-                    .padding(vertical = 16.dp)
-                    .align(Alignment.End),
-            ) {
-                Text(
-                    text = 
stringResource(R.string.withdraw_manual_ready_cancel),
-                    color = MaterialTheme.colorScheme.onError,
-                )
-            }
-        }
-    }
-}
-
-@Composable
-fun DetailRow(label: String, content: String, copy: Boolean = true) {
-    val context = LocalContext.current
-    Row {
-        Column(
-            modifier = Modifier
-                .weight(0.3f)) {
-            Text(
-                text = label,
-                style = MaterialTheme.typography.bodyLarge,
-                fontWeight = if (copy) FontWeight.Bold else FontWeight.Normal,
-            )
-            if (copy) {
-                IconButton(
-                    onClick = { copyToClipBoard(context, label, content) },
-                ) { Icon(Icons.Default.ContentCopy, 
stringResource(R.string.copy)) }
-            }
-        }
-        Text(
-            text = content,
-            style = MaterialTheme.typography.bodyLarge,
-            modifier = Modifier
-                .padding(bottom = 8.dp)
-                .weight(0.7f)
-                .then(if (copy) Modifier else Modifier.alpha(0.7f))
-        )
-    }
-}
-
-@Preview
-@Composable
-fun PreviewScreenIBAN() {
-    Surface {
-        ScreenIBAN(WithdrawStatus.ManualTransferRequiredIBAN(
-            exchangeBaseUrl = "test.exchange.taler.net",
-            uri = Uri.parse("https://taler.net";),
-            iban = "ASDQWEASDZXCASDQWE",
-            subject = "Taler Withdrawal 
P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG",
-            amountRaw = Amount("KUDOS", 10, 0),
-            transactionId = "",
-        ), {}) {}
-    }
-}
diff --git 
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt 
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt
new file mode 100644
index 0000000..508fcc3
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt
@@ -0,0 +1,301 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw.manual
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ScrollableTabRow
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Tab
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.CURRENCY_BTC
+import net.taler.wallet.R
+import net.taler.wallet.balances.CurrencySpecification
+import net.taler.wallet.compose.copyToClipBoard
+import net.taler.wallet.transactions.AmountType
+import net.taler.wallet.transactions.TransactionAmountComposable
+import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails
+import net.taler.wallet.withdraw.TransferData
+import net.taler.wallet.withdraw.WithdrawStatus
+
+@Composable
+fun ScreenTransfer(
+    status: WithdrawStatus.ManualTransferRequired,
+    bankAppClick: ((transfer: TransferData) -> Unit)?,
+) {
+    // TODO: show some placeholder
+    if (status.withdrawalTransfers.isEmpty()) return
+
+    val defaultTransfer = status.withdrawalTransfers[0]
+    var selectedTransfer by remember { mutableStateOf(defaultTransfer) }
+
+    Column {
+        if (status.withdrawalTransfers.size > 1) {
+            TransferAccountChooser(
+                accounts = status.withdrawalTransfers.map { 
it.withdrawalAccount },
+                selectedAccount = selectedTransfer.withdrawalAccount,
+                onSelectAccount = { account ->
+                    status.withdrawalTransfers.find {
+                        it.withdrawalAccount.paytoUri == account.paytoUri
+                    }?.let { selectedTransfer = it }
+                }
+            )
+        }
+
+        val scrollState = rememberScrollState()
+        Column(
+            modifier = Modifier
+                .verticalScroll(scrollState)
+                .padding(all = 16.dp),
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            Text(
+                text = stringResource(R.string.withdraw_manual_ready_title),
+                style = MaterialTheme.typography.headlineSmall,
+            )
+
+            when (val transfer = selectedTransfer) {
+                is TransferData.IBAN -> TransferIBAN(
+                    transfer = transfer,
+                    exchangeBaseUrl = status.exchangeBaseUrl,
+                    transactionAmountRaw = status.transactionAmountRaw,
+                    transactionAmountEffective = 
status.transactionAmountEffective,
+                )
+
+                is TransferData.Bitcoin -> TransferBitcoin(
+                    transfer = transfer,
+                    transactionAmountRaw = status.transactionAmountRaw,
+                    transactionAmountEffective = 
status.transactionAmountEffective,
+                )
+            }
+
+            if (bankAppClick != null) {
+                Button(
+                    onClick = { bankAppClick(selectedTransfer) },
+                    modifier = Modifier
+                        .padding(top = 16.dp)
+                ) {
+                    Text(text = 
stringResource(R.string.withdraw_manual_ready_bank_button))
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun DetailRow(
+    label: String,
+    content: String,
+    copy: Boolean = true,
+) {
+    val context = LocalContext.current
+    Column(
+        modifier = Modifier.fillMaxWidth(),
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        Text(
+            modifier = Modifier.padding(top = 16.dp, start = 6.dp, end = 6.dp),
+            text = label,
+            style = MaterialTheme.typography.bodyMedium,
+        )
+
+        Row(
+            modifier = Modifier.padding(top = 8.dp, start = 6.dp, end = 6.dp, 
bottom = 16.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            Text(
+                modifier = if (copy) Modifier.weight(1f) else Modifier,
+                text = content,
+                style = MaterialTheme.typography.bodyLarge,
+                fontFamily = if (copy) FontFamily.Monospace else 
FontFamily.Default,
+                textAlign = TextAlign.Center,
+            )
+
+            if (copy) {
+                IconButton(
+                    modifier = Modifier.padding(start = 8.dp),
+                    onClick = { copyToClipBoard(context, label, content) },
+                ) {
+                    Icon(
+                        imageVector = Icons.Default.ContentCopy,
+                        contentDescription = stringResource(R.string.copy),
+                    )
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun WithdrawalAmountTransfer(
+    amountRaw: Amount,
+    amountEffective: Amount,
+    conversionAmountRaw: Amount,
+) {
+    Column(
+        modifier = Modifier.fillMaxWidth(),
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        TransactionAmountComposable(
+            label = stringResource(R.string.withdraw_transfer),
+            amount = conversionAmountRaw,
+            amountType = AmountType.Neutral,
+        )
+
+        if (amountRaw.currency != conversionAmountRaw.currency) {
+            TransactionAmountComposable(
+                label = stringResource(R.string.withdraw_conversion),
+                amount = amountRaw,
+                amountType = AmountType.Neutral,
+            )
+        }
+
+        val fee = amountRaw - amountEffective
+        if (!fee.isZero()) {
+            TransactionAmountComposable(
+                label = stringResource(id = R.string.withdraw_fees),
+                amount = fee,
+                amountType = AmountType.Negative,
+            )
+        }
+
+        TransactionAmountComposable(
+            label = stringResource(id = R.string.withdraw_total),
+            amount = amountEffective,
+            amountType = AmountType.Positive,
+        )
+    }
+}
+
+@Composable
+fun TransferAccountChooser(
+    modifier: Modifier = Modifier,
+    accounts: List<WithdrawalExchangeAccountDetails>,
+    selectedAccount: WithdrawalExchangeAccountDetails,
+    onSelectAccount: (account: WithdrawalExchangeAccountDetails) -> Unit,
+) {
+    val selectedIndex = accounts.indexOfFirst {
+        it.paytoUri == selectedAccount.paytoUri
+    }
+
+    ScrollableTabRow(
+        selectedTabIndex = selectedIndex,
+        modifier = modifier,
+    ) {
+        accounts.forEachIndexed { index, account ->
+            Tab(
+                selected = selectedAccount.paytoUri == account.paytoUri,
+                onClick = { onSelectAccount(account) },
+                text = {
+                    if (account.currencySpecification?.name != null) {
+                        Text(stringResource(
+                            R.string.withdraw_account_currency,
+                            index + 1,
+                            account.currencySpecification.name,
+                        ))
+                    } else if (account.transferAmount?.currency != null) {
+                        Text(stringResource(
+                            R.string.withdraw_account_currency,
+                            index + 1,
+                            account.transferAmount.currency,
+                        ))
+                    } else Text(stringResource(R.string.withdraw_account, 
index + 1))
+                },
+            )
+        }
+    }
+}
+
+@Preview
+@Composable
+fun ScreenTransferPreview() {
+    Surface {
+        ScreenTransfer(
+            status = WithdrawStatus.ManualTransferRequired(
+                transactionId = "",
+                transactionAmountRaw = Amount.fromJSONString("KUDOS:10"),
+                transactionAmountEffective = 
Amount.fromJSONString("KUDOS:9.5"),
+                exchangeBaseUrl = "test.exchange.taler.net",
+                withdrawalTransfers = listOf(
+                    TransferData.IBAN(
+                        iban = "ASDQWEASDZXCASDQWE",
+                        subject = "Taler Withdrawal 
P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG",
+                        amountRaw = Amount("KUDOS", 10, 0),
+                        amountEffective = Amount("KUDOS", 9, 5),
+                        withdrawalAccount = WithdrawalExchangeAccountDetails(
+                            paytoUri = "https://taler.net/kudos";,
+                            transferAmount = Amount("KUDOS", 10, 0),
+                            currencySpecification = CurrencySpecification(
+                                "KUDOS",
+                                numFractionalInputDigits = 2,
+                                numFractionalNormalDigits = 2,
+                                numFractionalTrailingZeroDigits = 2,
+                                altUnitNames = emptyMap(),
+                            ),
+                        ),
+                    ),
+                    TransferData.Bitcoin(
+                        account = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
+                        segwitAddresses = listOf(
+                            "bc1qqleages8702xvg9qcyu02yclst24xurdrynvxq",
+                            "bc1qsleagehks96u7jmqrzcf0fw80ea5g57qm3m84c"
+                        ),
+                        subject = 
"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
+                        amountRaw = Amount(CURRENCY_BTC, 0, 14000000),
+                        amountEffective = Amount(CURRENCY_BTC, 0, 14000000),
+                        withdrawalAccount = WithdrawalExchangeAccountDetails(
+                            paytoUri = "https://taler.net/btc";,
+                            transferAmount = Amount("BTC", 0, 14000000),
+                            currencySpecification = CurrencySpecification(
+                                "Bitcoin",
+                                numFractionalInputDigits = 2,
+                                numFractionalNormalDigits = 2,
+                                numFractionalTrailingZeroDigits = 2,
+                                altUnitNames = emptyMap(),
+                            ),
+                        ),
+                    )
+                ),
+            ),
+            bankAppClick = {},
+        )
+    }
+}
\ No newline at end of file
diff --git 
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt 
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt
new file mode 100644
index 0000000..292f1d5
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt
@@ -0,0 +1,110 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw.manual
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.compose.CopyToClipboardButton
+import net.taler.wallet.withdraw.TransferData
+
+@Composable
+fun TransferBitcoin(
+    transfer: TransferData.Bitcoin,
+    transactionAmountRaw: Amount,
+    transactionAmountEffective: Amount,
+) {
+    Column(
+        modifier = Modifier.padding(all = 16.dp),
+        horizontalAlignment = CenterHorizontally,
+    ) {
+        Text(
+            text = stringResource(R.string.withdraw_manual_bitcoin_intro),
+            style = MaterialTheme.typography.bodyLarge,
+            modifier = Modifier
+                .padding(vertical = 8.dp)
+        )
+
+        BitcoinSegwitAddresses(
+            amount = transfer.amountRaw,
+            address = transfer.account,
+            segwitAddresses = transfer.segwitAddresses,
+        )
+
+        transfer.withdrawalAccount.transferAmount?.let { amount ->
+            WithdrawalAmountTransfer(
+                amountRaw = transactionAmountRaw,
+                amountEffective = transactionAmountEffective,
+                conversionAmountRaw = amount,
+            )
+        }
+    }
+}
+
+@Composable
+fun BitcoinSegwitAddresses(amount: Amount, address: String, segwitAddresses: 
List<String>) {
+    Column {
+        val allSegwitAddresses = listOf(address) + segwitAddresses
+        for (segwitAddress in allSegwitAddresses) {
+            Row(modifier = Modifier.padding(vertical = 8.dp)) {
+                Column(modifier = Modifier.weight(0.3f)) {
+                    Text(
+                        text = segwitAddress,
+                        fontWeight = FontWeight.Normal,
+                        fontFamily = FontFamily.Monospace,
+                        style = MaterialTheme.typography.bodySmall,
+                    )
+                    Text(
+                        text = if (segwitAddress == address)
+                            amount.withCurrency("BTC").toString()
+                        else SEGWIT_MIN.toString(),
+                        style = MaterialTheme.typography.bodyLarge,
+                        fontWeight = FontWeight.Bold,
+                    )
+                }
+            }
+        }
+
+        CopyToClipboardButton(
+            modifier = Modifier
+                .padding(top = 16.dp, start = 6.dp, end = 6.dp)
+                .align(CenterHorizontally),
+            label = "Bitcoin",
+            content = getCopyText(amount, address, segwitAddresses),
+        )
+    }
+}
+
+private val SEGWIT_MIN = Amount("BTC", 0, 294)
+
+private fun getCopyText(amount: Amount, addr: String, segwitAddresses: 
List<String>): String {
+    val sr = segwitAddresses.joinToString(separator = "\n") { s ->
+        "\n$s ${SEGWIT_MIN}\n"
+    }
+    return "$addr ${amount.withCurrency("BTC")}\n$sr"
+}
\ No newline at end of file
diff --git 
a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt 
b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt
new file mode 100644
index 0000000..a9e5f59
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt
@@ -0,0 +1,86 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw.manual
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.wallet.R
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.withdraw.TransferData
+
+@Composable
+fun TransferIBAN(
+    transfer: TransferData.IBAN,
+    exchangeBaseUrl: String,
+    transactionAmountRaw: Amount,
+    transactionAmountEffective: Amount,
+) {
+    Column(
+        modifier = Modifier.padding(all = 16.dp),
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        Text(
+            text = stringResource(
+                R.string.withdraw_manual_ready_intro,
+                transfer.amountRaw.toString()),
+            style = MaterialTheme.typography.bodyLarge,
+            modifier = Modifier
+                .padding(vertical = 8.dp)
+        )
+
+        Text(
+            text = stringResource(R.string.withdraw_manual_ready_warning),
+            style = MaterialTheme.typography.bodyMedium,
+            color = colorResource(R.color.notice_text),
+            modifier = Modifier
+                .align(Alignment.CenterHorizontally)
+                .padding(all = 8.dp)
+                .background(colorResource(R.color.notice_background))
+                .border(BorderStroke(2.dp, 
colorResource(R.color.notice_border)))
+                .padding(all = 16.dp)
+        )
+
+        DetailRow(stringResource(R.string.withdraw_manual_ready_iban), 
transfer.iban)
+        DetailRow(stringResource(R.string.withdraw_manual_ready_subject), 
transfer.subject)
+
+        TransactionInfoComposable(
+            label = stringResource(R.string.withdraw_exchange),
+            info = cleanExchange(exchangeBaseUrl),
+        )
+
+        transfer.withdrawalAccount.transferAmount?.let { amount ->
+            WithdrawalAmountTransfer(
+                amountRaw = transactionAmountRaw,
+                amountEffective = transactionAmountEffective,
+                conversionAmountRaw = amount,
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/wallet/src/main/res/values/strings.xml 
b/wallet/src/main/res/values/strings.xml
index 731b03a..436116e 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -189,6 +189,7 @@ GNU Taler is immune against many types of fraud, such as 
phishing of credit card
 
     <string name="withdraw_initiated">Withdrawal initiated</string>
     <string name="withdraw_title">Withdrawal</string>
+    <string name="withdraw_subtitle">Select target bank account</string>
     <string name="withdraw_total">Withdraw</string>
     <string name="withdraw_fees">Fee</string>
     <string name="withdraw_restrict_age">Restrict Usage to Age</string>
@@ -216,6 +217,11 @@ GNU Taler is immune against many types of fraud, such as 
phishing of credit card
     <string name="withdraw_error_title">Withdrawal Error</string>
     <string name="withdraw_error_message">Withdrawing is currently not 
possible. Please try again later!</string>
     <string name="withdraw_error_test">Error withdrawing TESTKUDOS</string>
+    <string name="withdraw_account">Account #%1$d</string>
+    <string name="withdraw_account_currency">Account #%1$d (%2$s)</string>
+    <string name="withdraw_transfer">Transfer</string>
+    <string name="withdraw_conversion">Conversion</string>
+    <string name="withdraw_conversion_support">This exchange supports currency 
conversion</string>
 
     <string name="exchange_settings_title">Exchanges</string>
     <string name="exchange_settings_summary">Manage list of exchanges known to 
this wallet</string>

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]