diff options
author | Iván Ávalos <avalos@disroot.org> | 2023-12-03 22:58:10 -0600 |
---|---|---|
committer | Iván Ávalos <avalos@disroot.org> | 2023-12-03 22:58:10 -0600 |
commit | ca2102669e540080ec26d41fa866c9fcddabb22f (patch) | |
tree | 31b81a48b9c372b8d7a3ee7a990ddb546f437e21 | |
parent | b274a74a4e077020786aae22f14e607c8c0e2266 (diff) | |
download | taler-android-ca2102669e540080ec26d41fa866c9fcddabb22f.tar.gz taler-android-ca2102669e540080ec26d41fa866c9fcddabb22f.tar.bz2 taler-android-ca2102669e540080ec26d41fa866c9fcddabb22f.zip |
[wallet] Initial (WIP) implementation of currency conversion
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/transactions/TransactionWithdrawalFragment.kt | 7 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt | 8 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/withdraw/ConversionComposable.kt | 156 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt | 45 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt | 122 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt | 54 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt (renamed from wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt) | 133 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt (renamed from wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt) | 84 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt | 71 |
9 files changed, 464 insertions, 216 deletions
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 @@ -224,6 +225,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 <http://www.gnu.org/licenses/> + */ + +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<WithdrawalExchangeAccountDetails>?, +) { + 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<String>, + selectedCurrency: String, + onSelectedCurrency: (currency: String) -> Unit, +) { + if (currencies.isEmpty()) return + val currencyOptions: List<String> = 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<ExchangeSelection>) : WithdrawStatus() data class TosReviewRequired( @@ -63,33 +64,43 @@ sealed class WithdrawStatus { val ageRestrictionOptions: List<Int>? = null, ) : WithdrawStatus() - object Withdrawing : WithdrawStatus() + data object Withdrawing : WithdrawStatus() + data class Success(val currency: String, val transactionId: String) : WithdrawStatus() - sealed class ManualTransferRequired : WithdrawStatus() { - abstract val uri: Uri - abstract val transactionId: String? - } - data class ManualTransferRequiredIBAN( + class ManualTransferRequired( + val transactionId: String?, val exchangeBaseUrl: String, + val withdrawalTransfers: List<TransferData>, + ) : 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<String>, - val subject: String, - val amountRaw: Amount, - override val transactionId: String?, - ) : ManualTransferRequired() - - data class Error(val message: String?) : WithdrawStatus() + val segwitAddresses: List<String>, + ): TransferData() } sealed class WithdrawTestStatus { @@ -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<WithdrawalExchangeAccountDetails>, +) = 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/ScreenIBAN.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt index 537f3ad..ad74363 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenIBAN.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt @@ -1,6 +1,6 @@ /* * This file is part of GNU Taler - * (C) 2022 Taler Systems S.A. + * (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 @@ -17,15 +17,13 @@ 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.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 @@ -33,78 +31,89 @@ 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.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.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.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 ScreenIBAN( - status: WithdrawStatus.ManualTransferRequiredIBAN, - bankAppClick: (() -> Unit)?, +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 - .wrapContentWidth(Alignment.CenterHorizontally) - .verticalScroll(scrollState) - .padding(all = 16.dp) + 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, ) - 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) { + + 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, + onClick = { bankAppClick(selectedTransfer) }, modifier = Modifier - .padding(vertical = 16.dp) - .align(Alignment.CenterHorizontally), + .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) - .align(Alignment.End), ) { Text( text = stringResource(R.string.withdraw_manual_ready_cancel), @@ -146,15 +155,35 @@ fun DetailRow(label: String, content: String, copy: Boolean = true) { @Preview @Composable -fun PreviewScreenIBAN() { +fun ScreenTransferPreview() { 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 = "", - ), {}) {} + 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/ScreenBitcoin.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt index fa20072..c89b7cc 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenBitcoin.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt @@ -1,6 +1,6 @@ /* * This file is part of GNU Taler - * (C) 2022 Taler Systems S.A. + * (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 @@ -16,17 +16,11 @@ 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 @@ -34,81 +28,48 @@ 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 +import net.taler.wallet.withdraw.TransferData @Composable -fun ScreenBitcoin( - status: WithdrawStatus.ManualTransferRequiredBitcoin, - bankAppClick: (() -> Unit)?, - onCancelClick: (() -> Unit)?, +fun TransferBitcoin( + data: TransferData.Bitcoin, ) { - 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 + + BitcoinSegwitAddresses( + amount = data.amountRaw, + address = data.account, + segwitAddresses = data.segwitAddresses, ) - if (bankAppClick != null) { - Button( - onClick = bankAppClick, - modifier = Modifier - .padding(vertical = 16.dp) - .align(Alignment.CenterHorizontally), - ) { - Text(text = stringResource(R.string.withdraw_manual_ready_bank_button)) - } - } - if (onCancelClick != null) { - Button( - onClick = onCancelClick, - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), - modifier = Modifier - .padding(vertical = 16.dp) - .align(End), - ) { - Text( - text = stringResource(R.string.withdraw_manual_ready_cancel), - color = MaterialTheme.colorScheme.onError, - ) - } - } } } @Composable -fun BitcoinSegwitAddrs(amount: Amount, addr: String, segwitAddresses: List<String>) { +fun BitcoinSegwitAddresses(amount: Amount, address: String, segwitAddresses: List<String>) { Column { CopyToClipboardButton( modifier = Modifier.align(End), label = "Bitcoin", - content = getCopyText(amount, addr, segwitAddresses), + content = getCopyText(amount, address, segwitAddresses), ) Row(modifier = Modifier.padding(vertical = 8.dp)) { Column(modifier = Modifier.weight(0.3f)) { Text( - text = addr, + text = address, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Normal, fontSize = 3.em @@ -147,23 +108,4 @@ private fun getCopyText(amount: Amount, addr: String, segwitAddresses: List<Stri "\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 = "", - ), {}) {} - } -} +}
\ 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 <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.withdraw.manual + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.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 |