From 8936e2d7adf6bd25a2cacdc18dc9e31db1cec8d2 Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Tue, 5 Dec 2023 13:01:40 -0600 Subject: [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 ++++++ 13 files changed, 780 insertions(+), 461 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/balances/CurrencySpecification.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt create mode 100644 wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt create mode 100644 wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt create mode 100644 wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt (limited to 'wallet/src/main/java/net/taler') 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 + */ + +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, +) \ 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, + val exchangeCreditAccountDetails: List? = 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? = 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? = 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) : 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, val ageRestrictionOptions: List? = 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, val ageRestrictionOptions: List? = 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, + ) : 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, - val subject: String, - val amountRaw: Amount, - override val transactionId: String?, - ) : ManualTransferRequired() - - data class Error(val message: String?) : WithdrawStatus() + val segwitAddresses: List, + ): 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, val ageRestrictionOptions: List? = null, ) @@ -115,7 +135,9 @@ data class AcceptWithdrawalResponse( @Serializable data class AcceptManualWithdrawalResponse( - val exchangePaytoUris: List, + val reservePub: String, + val withdrawalAccountsList: List, + 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, +) = 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 - */ - -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) { - 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 { - 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 - */ - -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 + */ + +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, + 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 + */ + +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) { + 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 { + 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 + */ + +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 -- cgit v1.2.3