From ca2102669e540080ec26d41fa866c9fcddabb22f Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Sun, 3 Dec 2023 22:58:10 -0600 Subject: [wallet] Initial (WIP) implementation of currency conversion --- .../transactions/TransactionWithdrawalFragment.kt | 7 +- .../net/taler/wallet/transactions/Transactions.kt | 8 + .../taler/wallet/withdraw/ConversionComposable.kt | 156 +++++++++++++++++ .../withdraw/TransactionWithdrawalComposable.kt | 45 +++-- .../net/taler/wallet/withdraw/WithdrawManager.kt | 122 +++++++------ .../manual/ManualWithdrawSuccessFragment.kt | 54 +++--- .../taler/wallet/withdraw/manual/ScreenBitcoin.kt | 169 ------------------ .../net/taler/wallet/withdraw/manual/ScreenIBAN.kt | 160 ----------------- .../taler/wallet/withdraw/manual/ScreenTransfer.kt | 189 +++++++++++++++++++++ .../wallet/withdraw/manual/TransferBitcoin.kt | 111 ++++++++++++ .../taler/wallet/withdraw/manual/TransferIBAN.kt | 71 ++++++++ 11 files changed, 670 insertions(+), 422 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/withdraw/ConversionComposable.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 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 6dc079f..f19fa4a 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt @@ -80,10 +80,11 @@ class TransactionWithdrawalFragment : TransactionDetailFragment(), ActionListene // TODO what if there's more than one or no URI? if (tx.withdrawalDetails.exchangeCreditAccounts?.isEmpty() != false) return val status = createManualTransferRequired( - amount = tx.amountRaw, - exchangeBaseUrl = tx.exchangeBaseUrl, - uriStr = tx.withdrawalDetails.exchangeCreditAccounts[0].paytoUri, transactionId = tx.transactionId, + exchangeBaseUrl = tx.exchangeBaseUrl, + amountRaw = tx.amountRaw, + amountEffective = tx.amountEffective, + withdrawalAccountList = tx.withdrawalDetails.exchangeCreditAccounts, ) 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 6cd5602..3af6aaf 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.currency.CurrencySpecification import net.taler.wallet.refund.RefundPaymentInfo import net.taler.wallet.transactions.TransactionMajorState.None import net.taler.wallet.transactions.TransactionMajorState.Pending @@ -223,6 +224,13 @@ data class WithdrawalExchangeAccountDetails ( */ 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. diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ConversionComposable.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ConversionComposable.kt new file mode 100644 index 0000000..cbe4a65 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/withdraw/ConversionComposable.kt @@ -0,0 +1,156 @@ +/* + * 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 + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.res.stringResource +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.SelectionChip +import net.taler.wallet.currency.CurrencySpecification +import net.taler.wallet.transactions.AmountType +import net.taler.wallet.transactions.TransactionAmountComposable +import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails + +@Composable +fun ConversionComposable( + amountRaw: Amount, + amountEffective: Amount, + accounts: List?, +) { + val altCurrencies = accounts + ?.filter { it.currencySpecification != null } + ?.map { it.currencySpecification!!.name } ?: emptyList() + var selectedCurrency by remember { mutableStateOf(amountRaw.currency) } + val selectedAccount = accounts?.find { + it.currencySpecification?.name == selectedCurrency + } + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (altCurrencies.isNotEmpty()) { + TransferCurrencyChooser( + currencies = listOf(amountRaw.currency) + altCurrencies, + selectedCurrency = selectedCurrency, + onSelectedCurrency = { selectedCurrency = it } + ) + } + + selectedAccount?.transferAmount?.let { transferAmount -> + TransactionAmountComposable( + label = "Transfer", + amount = transferAmount, + amountType = AmountType.Neutral, + ) + } + + TransactionAmountComposable( + label = if (selectedCurrency == amountRaw.currency) { + stringResource(R.string.amount_chosen) + } else { + "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, + ) + } + } +} + +@Composable +fun TransferCurrencyChooser( + modifier: Modifier = Modifier, + currencies: List, + selectedCurrency: String, + onSelectedCurrency: (currency: String) -> Unit, +) { + if (currencies.isEmpty()) return + val currencyOptions: List = currencies.distinct() + + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp), + text = "This exchange allows currency conversion.", + style = MaterialTheme.typography.bodyMedium, + ) + + LazyRow { + items(items = currencyOptions) { currency -> + SelectionChip( + modifier = Modifier.padding(horizontal = 4.dp), + label = { Text(currency) }, + selected = currency == selectedCurrency, + value = currency, + onSelected = onSelectedCurrency, + ) + } + } + } +} + +@Preview +@Composable +fun ConversionComposablePreview() { + Surface { + ConversionComposable( + amountRaw = Amount.fromJSONString("CHF:10"), + amountEffective = Amount.fromJSONString("CHF:9.5"), + accounts = listOf( + WithdrawalExchangeAccountDetails( + paytoUri = "payto://IBAN/1231231231", + transferAmount = Amount.fromJSONString("NETZBON:10"), + currencySpecification = CurrencySpecification( + name = "NETZBON", + numFractionalInputDigits = 2, + numFractionalNormalDigits = 2, + numFractionalTrailingZeroDigits = 2, + altUnitNames = mapOf("0" to "NETZBON"), + ), + ), + ), + ) + } +} \ No newline at end of file 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 aab22d3..3930966 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.currency.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,30 +77,30 @@ 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), - amount = t.amountRaw, - amountType = AmountType.Neutral, - ) - val fee = t.amountRaw - t.amountEffective - if (!fee.isZero()) { - TransactionAmountComposable( - label = stringResource(id = R.string.withdraw_fees), - amount = fee, - amountType = AmountType.Negative, + + if (t.withdrawalDetails is ManualTransfer) { + ConversionComposable( + amountRaw = t.amountRaw, + amountEffective = t.amountEffective, + accounts = t.withdrawalDetails.exchangeCreditAccounts, ) } + 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 +116,30 @@ fun TransactionWithdrawalComposablePreview() { txState = TransactionState(Pending), txActions = listOf(Retry, Suspend, Abort), exchangeBaseUrl = "https://exchange.demo.taler.net/", - withdrawalDetails = ManualTransfer(exchangeCreditAccounts = emptyList()), + withdrawalDetails = ManualTransfer( + exchangeCreditAccounts = 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 e18ab1a..c309a07 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt @@ -38,6 +38,7 @@ 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( @@ -63,33 +64,43 @@ sealed class WithdrawStatus { 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 exchangeBaseUrl: String, + val withdrawalTransfers: List, + ) : WithdrawStatus() + + data class Error(val message: String?) : WithdrawStatus() +} + +sealed class TransferData { + abstract val uri: Uri + abstract val subject: String + abstract val amountRaw: Amount + abstract val amountEffective: Amount + + val currency get() = amountRaw.currency + + data class IBAN( override val uri: Uri, + override val subject: String, + override val amountRaw: Amount, + override val amountEffective: Amount, val iban: String, - val subject: String, - val amountRaw: Amount, - override val transactionId: String?, - ) : ManualTransferRequired() + ): TransferData() - data class ManualTransferRequiredBitcoin( - val exchangeBaseUrl: String, + data class Bitcoin( override val uri: Uri, + override val subject: String, + override val amountRaw: Amount, + override val amountEffective: Amount, 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 { @@ -290,10 +301,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.withdrawalAccountsList[0].paytoUri, + status = status, + response = response, ) } } @@ -316,33 +325,46 @@ 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, + amountRaw: Amount, + amountEffective: Amount, + withdrawalAccountList: List, +) = WithdrawStatus.ManualTransferRequired( + transactionId = transactionId, + 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( + uri = uri, + account = uri.lastPathSegment!!, + segwitAddresses = segwitAddresses, + subject = reserve, + amountRaw = amountRaw, + amountEffective = amountEffective, + ) + } else TransferData.IBAN( uri = uri, - account = uri.lastPathSegment!!, - segwitAddrs = segwitAddrs, - subject = reserve, - amountRaw = amount, - transactionId = transactionId, + iban = uri.lastPathSegment!!, + subject = uri.getQueryParameter("message") ?: "Error: No message in URI", + amountRaw = amountRaw, + amountEffective = amountEffective, ) - } - 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..44bb1f8 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 @@ -32,47 +32,49 @@ 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 + status = withdrawManager.withdrawStatus.value as WithdrawStatus.ManualTransferRequired + + setContent { + TalerSurface { + ScreenTransfer( + status = status, + bankAppClick = { onBankAppClick(it) }, + onCancelClick = { onCancelClick() }, + ) + } } - // TODO test if this works with an actual payto:// handling app + } + + private fun onBankAppClick(transfer: TransferData) { + val intent = Intent().apply { data = transfer.uri } 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) - } - findNavController().navigate(R.id.action_nav_exchange_manual_withdrawal_success_to_nav_main) - } + if (componentName != null) { + requireContext().startActivitySafe(intent) } - setContent { - TalerSurface { - when (status) { - is WithdrawStatus.ManualTransferRequiredBitcoin -> { - ScreenBitcoin(status, onBankAppClick, onCancelClick) - } + } - is WithdrawStatus.ManualTransferRequiredIBAN -> { - ScreenIBAN(status, onBankAppClick, onCancelClick) - } - } + private fun onCancelClick() { + status.transactionId?.let { tid -> + transactionManager.deleteTransaction(tid) { + Log.e(TAG, "Error deleteTransaction $it") + showError(it) } + + findNavController().navigate(R.id.action_nav_exchange_manual_withdrawal_success_to_nav_main) } } 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..ad74363 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt @@ -0,0 +1,189 @@ +/* + * 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 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.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.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.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.draw.alpha +import androidx.compose.ui.platform.LocalContext +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.CURRENCY_BTC +import net.taler.wallet.R +import net.taler.wallet.compose.copyToClipBoard +import net.taler.wallet.withdraw.TransferCurrencyChooser +import net.taler.wallet.withdraw.TransferData +import net.taler.wallet.withdraw.WithdrawStatus + +@Composable +fun ScreenTransfer( + status: WithdrawStatus.ManualTransferRequired, + bankAppClick: ((transfer: TransferData) -> Unit)?, + onCancelClick: (() -> Unit)?, +) { + // TODO: show some placeholder + if (status.withdrawalTransfers.isEmpty()) return + + 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, + ) + + val defaultCurrency = status.withdrawalTransfers[0].currency + var selectedCurrency by remember { mutableStateOf(defaultCurrency) } + val selectedTransfer = status.withdrawalTransfers.firstOrNull { it.currency == selectedCurrency } + + if (status.withdrawalTransfers.size > 1) { + TransferCurrencyChooser( + currencies = status.withdrawalTransfers.map { it.currency }, + selectedCurrency = selectedCurrency, + onSelectedCurrency = { selectedCurrency = it } + ) + } + + when (selectedTransfer) { + is TransferData.IBAN -> TransferIBAN( + data = selectedTransfer, + exchangeBaseUrl = status.exchangeBaseUrl, + ) + is TransferData.Bitcoin -> TransferBitcoin( + data = selectedTransfer, + ) + else -> { + // TODO: show some placeholder + } + } + + if (bankAppClick != null && selectedTransfer != null) { + Button( + onClick = { bankAppClick(selectedTransfer) }, + modifier = Modifier + .padding(top = 16.dp) + ) { + 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) + ) { + 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 ScreenTransferPreview() { + Surface { + ScreenTransfer( + status = WithdrawStatus.ManualTransferRequired( + transactionId = "", + exchangeBaseUrl = "test.exchange.taler.net", + withdrawalTransfers = listOf( + TransferData.IBAN( + uri = Uri.parse("https://taler.net"), + iban = "ASDQWEASDZXCASDQWE", + subject = "Taler Withdrawal P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG", + amountRaw = Amount("KUDOS", 10, 0), + amountEffective = Amount("KUDOS", 9, 5), + ), + TransferData.Bitcoin( + uri = Uri.parse("https://taler.net"), + account = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + segwitAddresses = listOf( + "bc1qqleages8702xvg9qcyu02yclst24xurdrynvxq", + "bc1qsleagehks96u7jmqrzcf0fw80ea5g57qm3m84c" + ), + subject = "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00", + amountRaw = Amount(CURRENCY_BTC, 0, 14000000), + amountEffective = Amount(CURRENCY_BTC, 0, 14000000), + ) + ), + ), + bankAppClick = {}, + onCancelClick = {}, + ) + } +} \ 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..c89b7cc --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt @@ -0,0 +1,111 @@ +/* + * 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.foundation.layout.wrapContentWidth +import androidx.compose.material3.MaterialTheme +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.unit.dp +import androidx.compose.ui.unit.em +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( + data: TransferData.Bitcoin, +) { + Column(modifier = Modifier + .wrapContentWidth(Alignment.CenterHorizontally) + .padding(all = 16.dp) + ) { + Text( + text = stringResource(R.string.withdraw_manual_bitcoin_intro), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .padding(vertical = 8.dp) + ) + + BitcoinSegwitAddresses( + amount = data.amountRaw, + address = data.account, + segwitAddresses = data.segwitAddresses, + ) + } +} + +@Composable +fun BitcoinSegwitAddresses(amount: Amount, address: String, segwitAddresses: List) { + Column { + CopyToClipboardButton( + modifier = Modifier.align(End), + label = "Bitcoin", + content = getCopyText(amount, address, segwitAddresses), + ) + Row(modifier = Modifier.padding(vertical = 8.dp)) { + Column(modifier = Modifier.weight(0.3f)) { + Text( + text = address, + 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" +} \ 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..d94cdff --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt @@ -0,0 +1,71 @@ +/* + * 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.foundation.layout.wrapContentWidth +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.wallet.R +import net.taler.wallet.withdraw.TransferData + +@Composable +fun TransferIBAN( + data: TransferData.IBAN, + exchangeBaseUrl: String, +) { + Column(modifier = Modifier + .wrapContentWidth(Alignment.CenterHorizontally) + .padding(all = 16.dp) + ) { + Text( + text = stringResource( + R.string.withdraw_manual_ready_intro, + data.amountRaw.toString()), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .padding(vertical = 8.dp) + ) + + DetailRow(stringResource(R.string.withdraw_manual_ready_iban), data.iban) + DetailRow(stringResource(R.string.withdraw_manual_ready_subject), data.subject) + DetailRow(stringResource(R.string.amount_chosen), data.amountRaw.toString()) + DetailRow(stringResource(R.string.withdraw_exchange), 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) + ) + } +} \ No newline at end of file -- cgit v1.2.3