taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

commit 03ae2c2c29d88b93217bce6d1baec37c807866d1
parent 792c6155146e8c8d8f48d0378425fa83d7687d36
Author: Iván Ávalos <avalos@disroot.org>
Date:   Wed, 22 Jul 2026 21:34:54 +0200

[wallet] implement transferOptions

Diffstat:
Mwallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt | 2+-
Mwallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt | 3+++
Mwallet/src/main/java/net/taler/wallet/transactions/TransactionActionsComposable.kt | 77++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mwallet/src/main/java/net/taler/wallet/transactions/TransactionDetailScreen.kt | 14+-------------
Mwallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt | 8++++----
Mwallet/src/main/java/net/taler/wallet/transactions/Transactions.kt | 65++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mwallet/src/main/java/net/taler/wallet/transfer/ScreenTransfer.kt | 420+++++++++++++++++++++++++++++++++++++------------------------------------------
Mwallet/src/main/java/net/taler/wallet/transfer/TransferIBAN.kt | 2+-
Mwallet/src/main/java/net/taler/wallet/transfer/WireTransferDetailsScreen.kt | 149++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mwallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt | 148++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mwallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt | 29++++++++---------------------
Mwallet/src/main/res/values-de/strings.xml | 3++-
Mwallet/src/main/res/values/strings.xml | 1+
13 files changed, 560 insertions(+), 361 deletions(-)

diff --git a/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt @@ -84,7 +84,7 @@ fun ColumnScope.QrCodeUriComposable( ) { val context = LocalContext.current val qrCodeSize = getQrCodeSize() - val qrState by produceState<Bitmap?>(null) { + val qrState by produceState<Bitmap?>(null, qrData) { value = QrCodeManager.makeQrCode( qrData, qrCodeSize.value.toInt(), diff --git a/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt @@ -79,7 +79,10 @@ fun TransactionDepositComposable( style = MaterialTheme.typography.bodyLarge, ) + val qrCodes = t.kycAuthTransferInfo?.transferOptions + ?.firstOrNull()?.qrCodes ?: emptyList() DepositActions(t, + qrCodes = qrCodes, onWireTransfer = onWireTransfer, onShowQrCodes = onShowQrCodes) diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionActionsComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionActionsComposable.kt @@ -43,9 +43,9 @@ import net.taler.wallet.transfer.PaytoQrCode import net.taler.wallet.withdraw.QrCodeSpec @Composable -fun ColumnScope.WithdrawalActions( +fun ColumnScope.WithdrawalTransfers( tx: TransactionWithdrawal, - mainQrCode: QrCodeSpec? = null, + transferOption: TransferOption? = null, onConfirmBank: () -> Unit, onConfirmKyc: (url: String) -> Unit, onConfirmManual: () -> Unit, @@ -62,30 +62,39 @@ fun ColumnScope.WithdrawalActions( } } - ExchangeWaitReserve -> { - Text( - text = stringResource(R.string.withdraw_manual_instruction_manual), - modifier = Modifier.padding(16.dp), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - ) - - WireTransferStepsButton(onConfirmManual) - - if (mainQrCode != null) { - Text( - text = stringResource(R.string.withdraw_manual_instruction_qr), - modifier = Modifier.padding(16.dp), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - ) - - PaytoQrCode( - modifier = Modifier.padding(horizontal = 16.dp), - qrCode = mainQrCode, - ) - } else { - ShowQrCodesButton(onShowQrCodes) + ExchangeWaitReserve -> if (transferOption != null) { + when (val option = transferOption) { + is TransferOption.SwissQrBill, + is TransferOption.Payto -> { + Text( + text = stringResource(R.string.withdraw_manual_instruction_manual), + modifier = Modifier.padding(16.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + ) + + WireTransferStepsButton(onConfirmManual) + + if (option.qrCodes.size == 1) { + Text( + text = stringResource(R.string.withdraw_manual_instruction_qr), + modifier = Modifier.padding(16.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + ) + + PaytoQrCode( + modifier = Modifier.padding(horizontal = 16.dp), + qrCode = option.qrCodes[0], + ) + } else if (option.qrCodes.size > 1) { + ShowQrCodesButton(onShowQrCodes) + } + } + + is TransferOption.Uri -> { /* FIXME: implement */ } + + else -> error("assertion error") } } @@ -96,12 +105,26 @@ fun ColumnScope.WithdrawalActions( @Composable fun ColumnScope.DepositActions( tx: TransactionDeposit, + qrCodes: List<QrCodeSpec>, onWireTransfer: () -> Unit, onShowQrCodes: () -> Unit, ) { if (tx.txState.minor == KycAuthRequired) { WireTransferStepsButton(onWireTransfer) - ShowQrCodesButton(onShowQrCodes) + if (qrCodes.size == 1) { + Text( + text = stringResource(R.string.withdraw_manual_instruction_qr), + modifier = Modifier.padding(16.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + ) + PaytoQrCode( + modifier = Modifier.padding(horizontal = 16.dp), + qrCode = qrCodes[0], + ) + } else if (qrCodes.size > 1) { + ShowQrCodesButton(onShowQrCodes) + } } } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailScreen.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailScreen.kt @@ -91,7 +91,6 @@ fun TransactionDetailScreen( ) { val transactionManager = model.transactionManager val exchangeManager = model.exchangeManager - val withdrawManager = model.withdrawManager val devMode by model.devMode.observeAsState(false) val context = LocalContext.current var keepSelectedTx: Boolean by remember { mutableStateOf(false) } @@ -167,23 +166,12 @@ fun TransactionDetailScreen( is WalletDestination.TransactionWithdrawal -> { (t as? TransactionWithdrawal)?.let { tx -> - val qrCode = remember(tx) { - (tx.withdrawalDetails as? WithdrawalDetails.ManualTransfer)?.let { details -> - if (details.exchangeCreditAccountDetails?.size == 1) { - val account0 = details.exchangeCreditAccountDetails[0] - val qrCodes = withdrawManager.getQrCodesForPayto(account0.paytoUri) - if (qrCodes.size == 1) qrCodes[0] - else null - } else null - } - } - TransactionWithdrawalComposable( modifier = modifier, t = tx, devMode = devMode, spec = exchangeManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes), - qrCode = qrCode, + onSelectOption = { it?.let { transactionManager.selectTransferOption(it) } }, onConfirmKyc = { url -> launchInAppBrowser(context, url) }, diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt @@ -77,8 +77,10 @@ class TransactionManager( private val allTransactions = HashMap<ScopeInfo, List<Transaction>>() private val mTransactions = HashMap<ScopeInfo, MutableStateFlow<TransactionsResult>>() private val mSelectedTransaction = MutableStateFlow<Transaction?>(null) + private val mSelectedTransferOption = MutableStateFlow<TransferOption?>(null) val selectedTransaction = mSelectedTransaction.asStateFlow() + val selectedTransferOption = mSelectedTransferOption.asStateFlow() // This function must be called ONLY when scopeInfo / searchQuery change! // Use remember() {} in Compose to prevent multiple calls during recomposition @@ -186,10 +188,8 @@ class TransactionManager( } } - fun selectTransactionSync(transactionId: String, onSuccess: () -> Unit) = scope.launch { - if (selectTransaction(transactionId)) { - onSuccess() - } + fun selectTransferOption(option: TransferOption) { + mSelectedTransferOption.value = option } @UiThread diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -38,7 +38,6 @@ import kotlinx.serialization.json.JsonElement import net.taler.common.Amount import net.taler.common.Bech32 import net.taler.common.ContractProduct -import net.taler.common.ContractTerms import net.taler.common.Timestamp import net.taler.wallet.R import net.taler.wallet.main.TAG @@ -58,6 +57,7 @@ import net.taler.wallet.transactions.TransactionMajorState.None import net.taler.wallet.transactions.TransactionMajorState.Pending import net.taler.wallet.transactions.WithdrawalDetails.ManualTransfer import net.taler.wallet.transactions.WithdrawalDetails.TalerBankIntegrationApi +import net.taler.wallet.withdraw.QrCodeSpec import net.taler.wallet.withdraw.TransferData import java.util.UUID @@ -267,6 +267,23 @@ data class WithdrawalExchangeAccountDetails ( val bankLabel: String? = null, val priority: Int? = null, + + /** + * Error that happened when attempting to request the conversion rate. + */ + val conversionError: TalerErrorInfo? = null, + + /** + * Timestamp that indicates when the transfer options expire. + * + * If missing, options do not expire. + */ + val transferExpiry: Timestamp? = null, + + /** + * Options for transfering funds to the exchange for the withdrawal. + */ + val transferOptions: List<TransferOption> = emptyList(), ) { @Serializable enum class Status { @@ -280,6 +297,7 @@ data class WithdrawalExchangeAccountDetails ( fun getTransferDetails( amountRaw: Amount, amountEffective: Amount, + transferOption: TransferOption, ): TransferData? { val uri = paytoUri.trim().toUri() val transferAmount = (transferAmount @@ -287,7 +305,6 @@ data class WithdrawalExchangeAccountDetails ( ?.let { Amount.fromJSONString(it) } ?: amountEffective).withSpec(currencySpecification) return if ("bitcoin".equals(uri.authority, true)) { - // FIXME: use parsing logic from PaytoUriBitcoin.fromString() val msg = uri.getQueryParameter("message").orEmpty() val reg = "\\b([A-Z0-9]{52})\\b".toRegex().find(msg) val reserve = reg?.value @@ -303,18 +320,22 @@ data class WithdrawalExchangeAccountDetails ( amountEffective = amountEffective, transferAmount = transferAmount, withdrawalAccount = copy(paytoUri = uri.toString()), + transferOption = transferOption, ) } else if (uri.authority.equals("x-taler-bank", true)) { PaytoUriTalerBank.fromString(uri)?.let { data -> TransferData.Taler( account = data.account, receiverName = data.receiverName, - subject = uri.getQueryParameter("message") ?: return@let null, + subject = uri.getQueryParameter("message") + ?: uri.getQueryParameter("ch-qrr") + ?: return@let null, amountRaw = amountRaw, amountEffective = amountEffective, exchangeBaseUrl = data.host, transferAmount = transferAmount, - withdrawalAccount = copy(paytoUri = uri.toString()) + withdrawalAccount = copy(paytoUri = uri.toString()), + transferOption = transferOption, ) } } else if (uri.authority.equals("iban", true)) { @@ -324,11 +345,14 @@ data class WithdrawalExchangeAccountDetails ( receiverName = data.receiverName, receiverTown = data.receiverTown, receiverPostalCode = data.receiverPostalCode, - subject = uri.getQueryParameter("message") ?: return@let null, + subject = uri.getQueryParameter("message") + ?: uri.getQueryParameter("ch-qrr") + ?: return@let null, amountRaw = amountRaw, amountEffective = amountEffective, transferAmount = transferAmount, withdrawalAccount = copy(paytoUri = uri.toString()), + transferOption = transferOption, ) } } else if (uri.authority.equals("cyclos", true)) { @@ -343,6 +367,7 @@ data class WithdrawalExchangeAccountDetails ( transferAmount = transferAmount .withSpec(currencySpecification), withdrawalAccount = copy(paytoUri = uri.toString()), + transferOption = transferOption, ) } } else null @@ -382,6 +407,35 @@ sealed class AccountRestriction { } @Serializable +sealed class TransferOption { + abstract val qrCodes: List<QrCodeSpec> + + @Serializable + @SerialName("payto") + data class Payto( + val paytoUri: String, + override val qrCodes: List<QrCodeSpec>, + ): TransferOption() + + @Serializable + @SerialName("uri") + data class Uri( + val uri: String, + ): TransferOption() { + override val qrCodes: List<QrCodeSpec> = emptyList() + } + + @Serializable + @SerialName("ch-qr-bill") + data class SwissQrBill( + val paytoUri: String, + val qrReferenceNumber: String, + override val qrCodes: List<QrCodeSpec>, + ): TransferOption() + +} + +@Serializable @SerialName("payment") class TransactionPayment( override val transactionId: String, @@ -512,6 +566,7 @@ data class KycAuthTransferInfo( val debitPaytoUri: String, val accountPub: String, val creditPaytoUris: List<String>, + val transferOptions: List<TransferOption>, ) @Serializable diff --git a/wallet/src/main/java/net/taler/wallet/transfer/ScreenTransfer.kt b/wallet/src/main/java/net/taler/wallet/transfer/ScreenTransfer.kt @@ -31,18 +31,12 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon 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.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf -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 @@ -65,6 +59,7 @@ import net.taler.wallet.compose.ShareButton import net.taler.wallet.transactions.AccountRestriction import net.taler.wallet.transactions.AmountType import net.taler.wallet.transactions.TransactionAmountComposable +import net.taler.wallet.transactions.TransferOption import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails.Status.Ok import net.taler.wallet.withdraw.QrCodeSpec @@ -77,141 +72,111 @@ sealed class TransferContext { data class DepositKycAuth(val debitPaytoUri: String) : TransferContext() } +fun TransferOption.subjectLabelResId(): Int = when (this) { + is TransferOption.SwissQrBill -> R.string.withdraw_manual_ready_subject_ch_qr + else -> R.string.withdraw_manual_ready_subject +} + @Composable fun ScreenTransfer( modifier: Modifier = Modifier, - transfers: List<TransferData>, + transfer: TransferData, spec: CurrencySpecification?, showQrCodes: Boolean, - getQrCodes: (account: TransferData) -> List<QrCodeSpec>, bankAppClick: ((transfer: TransferData) -> Unit)?, shareClick: ((transfer: TransferData) -> Unit)?, devMode: Boolean = false, transferContext: TransferContext, ) { - // TODO: show some placeholder - if (transfers.isEmpty()) return - - val transfers = transfers.filter { - // TODO: in dev mode, show debug info when status is `Error' - it.withdrawalAccount.status == Ok - }.sortedByDescending { - it.withdrawalAccount.priority - } - - val defaultTransfer = transfers[0] - var selectedTransfer by remember { mutableStateOf(defaultTransfer) } - val qrCodes = remember(selectedTransfer) { getQrCodes(selectedTransfer) } + val qrCodes = transfer.transferOption.qrCodes val qrExpandedStates = remember(qrCodes) { - val map = mutableStateMapOf<QrCodeSpec, Boolean>() - qrCodes.forEach { - map[it] = false - } - map + mutableStateMapOf<QrCodeSpec, Boolean>() } - LaunchedEffect(Unit) { - getQrCodes(defaultTransfer) - } - - Column(modifier) { - if (transfers.size > 1) { - TransferAccountChooser( - accounts = transfers.map { it.withdrawalAccount }, - selectedAccount = selectedTransfer.withdrawalAccount, - onSelectAccount = { account -> - transfers.find { - it.withdrawalAccount.paytoUri == account.paytoUri - }?.let { selectedTransfer = it } - } + val scrollState = rememberScrollState() + Column( + modifier = modifier + .verticalScroll(scrollState), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (showQrCodes) { + Text( + text = stringResource(R.string.withdraw_manual_qr_intro), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .padding( + vertical = 8.dp, + horizontal = 16.dp, + ) ) - } - - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .verticalScroll(scrollState), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (showQrCodes) { - Text( - text = stringResource(R.string.withdraw_manual_qr_intro), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .padding( - vertical = 8.dp, - horizontal = 16.dp, - ) - ) - qrCodes.forEach { spec -> - PaytoQrCard( - expanded = qrExpandedStates[spec]!!, - setExpanded = { expanded -> - if (expanded) { // un-expand all others - qrExpandedStates.forEach { (k, _) -> - qrExpandedStates[k] = false - } + qrCodes.forEach { spec -> + PaytoQrCard( + expanded = qrExpandedStates[spec]!!, + setExpanded = { expanded -> + if (expanded) { // un-expand all others + qrExpandedStates.forEach { (k, _) -> + qrExpandedStates[k] = false } - // expand only toggled one - qrExpandedStates[spec] = expanded - }, - qrCode = spec, - ) - } - - BottomInsetsSpacer() - return + } + // expand only toggled one + qrExpandedStates[spec] = expanded + }, + qrCode = spec, + ) } - when (val transfer = selectedTransfer) { - is TransferData.Taler -> TransferTaler( - transfer = transfer, - transactionAmountEffective = transfer.amountEffective.withSpec(spec), - transferContext = transferContext, - ) + BottomInsetsSpacer() + return + } - is TransferData.IBAN -> TransferIBAN( - transfer = transfer, - transactionAmountEffective = transfer.amountEffective.withSpec(spec), - transferContext = transferContext, - ) + when (transfer) { + is TransferData.Taler -> TransferTaler( + transfer = transfer, + transactionAmountEffective = transfer.amountEffective.withSpec(spec), + transferContext = transferContext, + ) - is TransferData.Cyclos -> TransferCyclos( - transfer = transfer, - transactionAmountEffective = transfer.amountEffective.withSpec(spec), - ) + is TransferData.IBAN -> TransferIBAN( + transfer = transfer, + transactionAmountEffective = transfer.amountEffective.withSpec(spec), + transferContext = transferContext, + ) - is TransferData.Bitcoin -> TransferBitcoin( - transfer = transfer, - ) - } + is TransferData.Cyclos -> TransferCyclos( + transfer = transfer, + transactionAmountEffective = transfer.amountEffective.withSpec(spec), + ) - Spacer(Modifier.height(24.dp)) + is TransferData.Bitcoin -> TransferBitcoin( + transfer = transfer, + ) + } - if (devMode) { - val paytoUri = selectedTransfer.withdrawalAccount.paytoUri - if (bankAppClick != null && LocalContext.current.canAppHandleUri(paytoUri)) { - Button( - onClick = { bankAppClick(selectedTransfer) }, - modifier = Modifier - .padding(bottom = 16.dp), - ) { - Text(text = stringResource(R.string.withdraw_manual_ready_bank_button)) - } - } + Spacer(Modifier.height(24.dp)) - if (shareClick != null) { - ShareButton( - content = selectedTransfer.withdrawalAccount.paytoUri, - modifier = Modifier - .padding(bottom = 16.dp), - ) + if (devMode) { + val paytoUri = transfer.withdrawalAccount.paytoUri + if (bankAppClick != null && LocalContext.current.canAppHandleUri(paytoUri)) { + Button( + onClick = { bankAppClick(transfer) }, + modifier = Modifier + .padding(bottom = 16.dp), + ) { + Text(text = stringResource(R.string.withdraw_manual_ready_bank_button)) } } - BottomInsetsSpacer() + if (shareClick != null) { + ShareButton( + content = transfer.withdrawalAccount.paytoUri, + modifier = Modifier + .padding(bottom = 16.dp), + ) + } } + + BottomInsetsSpacer() } } @@ -311,125 +276,136 @@ fun WithdrawalAmountTransfer( } } -@Composable -fun TransferAccountChooser( - modifier: Modifier = Modifier, - accounts: List<WithdrawalExchangeAccountDetails>, - selectedAccount: WithdrawalExchangeAccountDetails, - onSelectAccount: (account: WithdrawalExchangeAccountDetails) -> Unit, -) { - val selectedIndex = accounts.indexOfFirst { - it.paytoUri == selectedAccount.paytoUri - } +private val qrCodes = listOf( + QrCodeSpec(EpcQr, "BCD\\n002\\n1\\nSCT\\n\\n\\nGENODEM1GLS/DE54430609674049078800\\n\\n\\nTaler MJ15S835A5ENQZGJX161TS7FND6Q5DSABS8FCHB8ECF9NT1J8GH0"), + QrCodeSpec(SPC, "BCD\\n002\\n1\\nSCT\\n\\n\\nGENODEM1GLS/DE54430609674049078800\\n\\n\\nTaler MJ15S835A5ENQZGJX161TS7FND6Q5DSABS8FCHB8ECF9NT1J8GH0"), +) - ScrollableTabRow( - selectedTabIndex = selectedIndex, - modifier = modifier, - edgePadding = 8.dp, - ) { - accounts.forEachIndexed { index, account -> - Tab( - selected = selectedAccount.paytoUri == account.paytoUri, - onClick = { onSelectAccount(account) }, - text = { - if (!account.bankLabel.isNullOrEmpty()) { - Text(account.bankLabel) - } else 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)) - }, - ) - } - } -} +private val transferOptions = listOf( + TransferOption.Payto( + "payto://", + qrCodes = listOf( + QrCodeSpec(EpcQr, "BCD\\n002\\n1\\nSCT\\n\\n\\nGENODEM1GLS/DE54430609674049078800\\n\\n\\nTaler MJ15S835A5ENQZGJX161TS7FND6Q5DSABS8FCHB8ECF9NT1J8GH0"), + QrCodeSpec(SPC, "BCD\\n002\\n1\\nSCT\\n\\n\\nGENODEM1GLS/DE54430609674049078800\\n\\n\\nTaler MJ15S835A5ENQZGJX161TS7FND6Q5DSABS8FCHB8ECF9NT1J8GH0"), + ), + ), +) @Preview @Composable -fun ScreenTransferPreview( +fun ScreenTransferIBANPreview( showQrCodes: Boolean = false, transferContext: TransferContext = TransferContext.ManualWithdrawal, ) { Surface { ScreenTransfer( - transfers = listOf( - TransferData.IBAN( - iban = "ASDQWEASDZXCASDQWE", - subject = "Taler Withdrawal P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG", - amountRaw = Amount("KUDOS", 10, 0), - amountEffective = Amount("KUDOS", 9, 5), + transfer = TransferData.IBAN( + iban = "ASDQWEASDZXCASDQWE", + subject = "Taler Withdrawal P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG", + amountRaw = Amount("KUDOS", 10, 0), + amountEffective = Amount("KUDOS", 9, 5), + transferAmount = Amount("KUDOS", 10, 0), + receiverTown = "Biel/Bienne", + receiverPostalCode = "2500", + transferOption = transferOptions.first(), + withdrawalAccount = WithdrawalExchangeAccountDetails( + paytoUri = "https://taler.net/kudos", transferAmount = Amount("KUDOS", 10, 0), - receiverTown = "Biel/Bienne", - receiverPostalCode = "2500", - withdrawalAccount = WithdrawalExchangeAccountDetails( - paytoUri = "https://taler.net/kudos", - transferAmount = Amount("KUDOS", 10, 0), - status = Ok, - currencySpecification = CurrencySpecification( - "KUDOS", - numFractionalInputDigits = 2, - numFractionalNormalDigits = 2, - numFractionalTrailingZeroDigits = 2, - altUnitNames = emptyMap(), + transferOptions = transferOptions, + status = Ok, + currencySpecification = CurrencySpecification( + "KUDOS", + numFractionalInputDigits = 2, + numFractionalNormalDigits = 2, + numFractionalTrailingZeroDigits = 2, + altUnitNames = emptyMap(), + ), + creditRestrictions = listOf( + AccountRestriction.RegexAccount( + paytoRegex = "CH", + humanHint = "Only Swiss bank accounts", + humanHintI18n = emptyMap(), ), - creditRestrictions = listOf( - AccountRestriction.RegexAccount( - paytoRegex = "CH", - humanHint = "Only Swiss bank accounts", - humanHintI18n = emptyMap(), - ), - AccountRestriction.RegexAccount( - paytoRegex = "DE", - humanHint = "Only European bank accounts", - humanHintI18n = emptyMap(), - ) + AccountRestriction.RegexAccount( + paytoRegex = "DE", + humanHint = "Only European bank accounts", + humanHintI18n = emptyMap(), ) - ), + ) ), - TransferData.Cyclos( - host = "demo.cyclos.org/abc", - account = "1234567890", - receiverName = "Taler Wire", - subject = "Taler Withdrawal P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG", - amountRaw = Amount("IU", 10, 0), - amountEffective = Amount("IU", 9, 5), + ), + spec = null, + bankAppClick = {}, + shareClick = {}, + showQrCodes = showQrCodes, + transferContext = transferContext, + ) + } +} + +@Preview +@Composable +fun ScreenTransferCyclosPreview( + showQrCodes: Boolean = false, + transferContext: TransferContext = TransferContext.ManualWithdrawal, +) { + Surface { + ScreenTransfer( + transfer = TransferData.Cyclos( + host = "demo.cyclos.org/abc", + account = "1234567890", + receiverName = "Taler Wire", + subject = "Taler Withdrawal P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG", + amountRaw = Amount("IU", 10, 0), + amountEffective = Amount("IU", 9, 5), + transferAmount = Amount("IU", 10, 0), + transferOption = transferOptions.first(), + withdrawalAccount = WithdrawalExchangeAccountDetails( + paytoUri = "https://taler.net/cyclos", transferAmount = Amount("IU", 10, 0), - withdrawalAccount = WithdrawalExchangeAccountDetails( - paytoUri = "https://taler.net/cyclos", - transferAmount = Amount("IU", 10, 0), - status = Ok, - ), + transferOptions = transferOptions, + status = Ok, ), - TransferData.Bitcoin( - account = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", - segwitAddresses = listOf( - "bc1qqleages8702xvg9qcyu02yclst24xurdrynvxq", - "bc1qsleagehks96u7jmqrzcf0fw80ea5g57qm3m84c" - ), - subject = "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00", - amountRaw = Amount(CURRENCY_BTC, 0, 14000000), - amountEffective = Amount(CURRENCY_BTC, 0, 14000000), - transferAmount = Amount("KUDOS", 10, 0), - withdrawalAccount = WithdrawalExchangeAccountDetails( - paytoUri = "https://taler.net/btc", - transferAmount = Amount("BTC", 0, 14000000), - status = Ok, - currencySpecification = CurrencySpecification( - "Bitcoin", - numFractionalInputDigits = 2, - numFractionalNormalDigits = 2, - numFractionalTrailingZeroDigits = 2, - altUnitNames = emptyMap(), - ), + ), + spec = null, + bankAppClick = {}, + shareClick = {}, + showQrCodes = showQrCodes, + transferContext = transferContext, + ) + } +} + +@Preview +@Composable +fun ScreenTransferBitcoinPreview( + showQrCodes: Boolean = false, + transferContext: TransferContext = TransferContext.ManualWithdrawal, +) { + Surface { + ScreenTransfer( + transfer = TransferData.Bitcoin( + account = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + segwitAddresses = listOf( + "bc1qqleages8702xvg9qcyu02yclst24xurdrynvxq", + "bc1qsleagehks96u7jmqrzcf0fw80ea5g57qm3m84c" + ), + subject = "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00", + amountRaw = Amount(CURRENCY_BTC, 0, 14000000), + amountEffective = Amount(CURRENCY_BTC, 0, 14000000), + transferAmount = Amount("KUDOS", 10, 0), + transferOption = transferOptions.first(), + withdrawalAccount = WithdrawalExchangeAccountDetails( + paytoUri = "https://taler.net/btc", + transferAmount = Amount("BTC", 0, 14000000), + transferOptions = transferOptions, + status = Ok, + currencySpecification = CurrencySpecification( + "Bitcoin", + numFractionalInputDigits = 2, + numFractionalNormalDigits = 2, + numFractionalTrailingZeroDigits = 2, + altUnitNames = emptyMap(), ), ), ), @@ -437,12 +413,6 @@ fun ScreenTransferPreview( bankAppClick = {}, shareClick = {}, showQrCodes = showQrCodes, - getQrCodes = { - listOf( - QrCodeSpec(EpcQr, "BCD\\n002\\n1\\nSCT\\n\\n\\nGENODEM1GLS/DE54430609674049078800\\n\\n\\nTaler MJ15S835A5ENQZGJX161TS7FND6Q5DSABS8FCHB8ECF9NT1J8GH0"), - QrCodeSpec(SPC, "BCD\\n002\\n1\\nSCT\\n\\n\\nGENODEM1GLS/DE54430609674049078800\\n\\n\\nTaler MJ15S835A5ENQZGJX161TS7FND6Q5DSABS8FCHB8ECF9NT1J8GH0") - ) - }, transferContext = transferContext, ) } @@ -451,11 +421,11 @@ fun ScreenTransferPreview( @Preview @Composable fun ScreenTransferKycAuthPreview() { - ScreenTransferPreview(transferContext = TransferContext.DepositKycAuth("CH120912")) + ScreenTransferIBANPreview(transferContext = TransferContext.DepositKycAuth("CH120912")) } @Preview @Composable fun ScreenTransferQRPreview() { - ScreenTransferPreview(true) + ScreenTransferIBANPreview(true) } \ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/transfer/TransferIBAN.kt b/wallet/src/main/java/net/taler/wallet/transfer/TransferIBAN.kt @@ -140,7 +140,7 @@ fun TransferIBAN( ) DetailRow( - stringResource(R.string.withdraw_manual_ready_subject), + stringResource(transfer.transferOption.subjectLabelResId()), transfer.subject, characterBreak = true, ) diff --git a/wallet/src/main/java/net/taler/wallet/transfer/WireTransferDetailsScreen.kt b/wallet/src/main/java/net/taler/wallet/transfer/WireTransferDetailsScreen.kt @@ -30,14 +30,16 @@ import androidx.compose.ui.res.stringResource import net.taler.lib.android.openUri import net.taler.lib.android.shareText import net.taler.wallet.R +import net.taler.wallet.compose.EmptyComposable import net.taler.wallet.compose.GlobalScaffold import net.taler.wallet.compose.collectAsStateLifecycleAware import net.taler.wallet.main.MainViewModel import net.taler.wallet.transactions.TransactionDeposit import net.taler.wallet.transactions.TransactionMajorState.Done import net.taler.wallet.transactions.TransactionWithdrawal -import net.taler.wallet.transactions.WithdrawalDetails +import net.taler.wallet.transactions.TransferOption import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails +import net.taler.wallet.withdraw.TransferData @Composable fun WireTransferDetailsScreen( @@ -48,10 +50,10 @@ fun WireTransferDetailsScreen( val context = LocalContext.current val sharePaymentTitle = stringResource(R.string.share_payment) val transactionManager = model.transactionManager - val withdrawManager = model.withdrawManager val exchangeManager = model.exchangeManager val selectedTx by transactionManager.selectedTransaction.collectAsStateLifecycleAware() + val selectedOption by transactionManager.selectedTransferOption.collectAsStateLifecycleAware() val devMode by model.devMode.observeAsState(false) LaunchedEffect(selectedTx) { @@ -66,64 +68,103 @@ fun WireTransferDetailsScreen( title = { Text(stringResource(R.string.wire_transfer)) }, onNavigateBack = onNavigateBack, ) { paddingValues -> - val transfers = remember(selectedTx) { - selectedTx?.let { tx -> - when (tx) { - is TransactionWithdrawal -> when (tx.withdrawalDetails) { - is WithdrawalDetails.ManualTransfer -> { - tx.withdrawalDetails.exchangeCreditAccountDetails - } - else -> null - } - is TransactionDeposit -> tx.kycAuthTransferInfo?.let { - it.creditPaytoUris.map { paytoUri -> - WithdrawalExchangeAccountDetails( - paytoUri = paytoUri, - status = WithdrawalExchangeAccountDetails.Status.Ok, - ) - } - } + val tx = selectedTx ?: return@GlobalScaffold + + val spec = tx.amountRaw.currency.let { currency -> + exchangeManager.getSpecForCurrency(currency, tx.scopes) + } + + val bankAppClick: (TransferData) -> Unit = { transfer -> + context.openUri(uri = transfer.withdrawalAccount.paytoUri, title = sharePaymentTitle) + } + + val shareClick: (TransferData) -> Unit = { transfer -> + context.shareText(text = transfer.withdrawalAccount.paytoUri) + } + + when (tx) { + is TransactionWithdrawal -> { + val option = selectedOption + val paytoUri = when (option) { + is TransferOption.Payto -> option.paytoUri + is TransferOption.SwissQrBill -> option.paytoUri else -> null - }?.map { - it.getTransferDetails( + } + if (paytoUri == null || option == null) return@GlobalScaffold + + val transferData = remember(option, tx) { + WithdrawalExchangeAccountDetails( + paytoUri = paytoUri, + status = WithdrawalExchangeAccountDetails.Status.Ok, + ).getTransferDetails( amountRaw = tx.amountRaw, - amountEffective = tx.amountEffective + amountEffective = tx.amountEffective, + transferOption = option, + ) + } + + if (transferData != null) { + ScreenTransfer( + modifier = Modifier.padding(paddingValues), + transfer = transferData, + spec = spec, + bankAppClick = bankAppClick, + shareClick = shareClick, + showQrCodes = showQrCodes, + devMode = devMode, + transferContext = TransferContext.ManualWithdrawal, ) + } else { + EmptyComposable(modifier = Modifier.padding(paddingValues)) } } - }?.filterNotNull() - - if (transfers != null) ScreenTransfer( - modifier = Modifier.padding(paddingValues), - transfers = transfers, - getQrCodes = { withdrawManager.getQrCodesForPayto(it.withdrawalAccount.paytoUri) }, - spec = selectedTx?.amountRaw?.currency?.let { - selectedTx?.scopes?.let { selectedScopes -> - exchangeManager.getSpecForCurrency(it, selectedScopes) - } ?: run { - exchangeManager.getSpecForCurrency(it) + + is TransactionDeposit -> { + val opt = tx.kycAuthTransferInfo?.transferOptions?.firstOrNull() + if (opt == null) { + EmptyComposable(modifier = Modifier.padding(paddingValues)) + return@GlobalScaffold + } + + val paytoUri = when (opt) { + is TransferOption.Payto -> opt.paytoUri + is TransferOption.SwissQrBill -> opt.paytoUri + is TransferOption.Uri -> { + EmptyComposable(modifier = Modifier.padding(paddingValues)) + return@GlobalScaffold + } + } + + val transferData = remember(opt, tx) { + WithdrawalExchangeAccountDetails( + paytoUri = paytoUri, + status = WithdrawalExchangeAccountDetails.Status.Ok, + ).getTransferDetails( + amountRaw = tx.amountRaw, + amountEffective = tx.amountEffective, + transferOption = opt, + ) + } + + if (transferData != null) { + ScreenTransfer( + modifier = Modifier.padding(paddingValues), + transfer = transferData, + spec = spec, + bankAppClick = bankAppClick, + shareClick = shareClick, + showQrCodes = showQrCodes, + devMode = devMode, + transferContext = TransferContext.DepositKycAuth( + tx.kycAuthTransferInfo?.debitPaytoUri ?: "", + ), + ) + } else { + EmptyComposable(modifier = Modifier.padding(paddingValues)) } - }, - bankAppClick = { transfer -> - context.openUri( - uri = transfer.withdrawalAccount.paytoUri, - title = sharePaymentTitle - ) - }, - shareClick = { transfer -> - context.shareText( - text = transfer.withdrawalAccount.paytoUri, - ) - }, - showQrCodes = showQrCodes, - devMode = devMode, - transferContext = when (val tx = selectedTx) { - is TransactionWithdrawal -> TransferContext.ManualWithdrawal - is TransactionDeposit -> TransferContext.DepositKycAuth( - tx.kycAuthTransferInfo?.debitPaytoUri ?: "" - ) - else -> return@GlobalScaffold } - ) + + else -> return@GlobalScaffold + } } } diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt b/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt @@ -17,14 +17,24 @@ package net.taler.wallet.withdraw import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PrimaryScrollableTabRow +import androidx.compose.material3.SecondaryScrollableTabRow import androidx.compose.material3.Surface +import androidx.compose.material3.Tab import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -55,20 +65,21 @@ import net.taler.wallet.transactions.TransactionMinorState import net.taler.wallet.transactions.TransactionState import net.taler.wallet.transactions.TransactionStateComposable import net.taler.wallet.transactions.TransactionWithdrawal +import net.taler.wallet.transactions.TransferOption import net.taler.wallet.transactions.TransitionsComposable -import net.taler.wallet.transactions.WithdrawalActions import net.taler.wallet.transactions.WithdrawalDetails.ManualTransfer import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails +import net.taler.wallet.transactions.WithdrawalTransfers @Composable fun TransactionWithdrawalComposable( t: TransactionWithdrawal, devMode: Boolean, - qrCode: QrCodeSpec?, spec: CurrencySpecification?, + onSelectOption: (option: TransferOption?) -> Unit, onConfirmKyc: (url: String) -> Unit, onConfirmBank: () -> Unit, - onConfirmManual: () -> Unit, + onConfirmManual: (option: TransferOption?) -> Unit, onShowQrCodes: () -> Unit, onTransition: (t: TransactionAction) -> Unit, modifier: Modifier = Modifier, @@ -80,6 +91,53 @@ fun TransactionWithdrawalComposable( .verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally, ) { + val accounts = (t.withdrawalDetails as? ManualTransfer) + ?.exchangeCreditAccountDetails + ?.let { details -> + details.filter { + it.status == WithdrawalExchangeAccountDetails.Status.Ok + }.sortedByDescending { + it.priority + } + } + + val defaultAccountIndex = 0 + var selectedAccountIndex by rememberSaveable { + mutableIntStateOf(defaultAccountIndex) } + val selectedAccount = accounts?.getOrNull(selectedAccountIndex) + val defaultOptionIndex = 0 + var selectedOptionIndex by rememberSaveable (selectedAccountIndex) { + mutableIntStateOf(defaultOptionIndex) } + val selectedOption = selectedAccount + ?.transferOptions[selectedOptionIndex] + + LaunchedEffect(selectedOption) { + selectedOption?.let { onSelectOption(it) } + } + + val showAccountChooser = accounts != null && accounts.size > 1 + val showOptionChooser = selectedAccount != null && selectedAccount.transferOptions.size > 1 + + if (showAccountChooser) { + TransferAccountChooser( + accounts = accounts.map { it }, + selectedIndex = selectedAccountIndex, + onSelectAccount = { selectedAccountIndex = it }, + ) + } + + if (showOptionChooser) { + TransferOptionChooser( + options = selectedAccount.transferOptions, + selectedIndex = selectedOptionIndex, + onSelectOption = { selectedOptionIndex = it }, + ) + } + + if (showAccountChooser || showOptionChooser) { + Spacer(Modifier.height(16.dp)) + } + TransactionStateComposable(state = t.txState, tx = t) Text( @@ -111,11 +169,12 @@ fun TransactionWithdrawalComposable( amountType = AmountType.Positive, ) - WithdrawalActions(t, - mainQrCode = qrCode, - onConfirmKyc = onConfirmKyc, + WithdrawalTransfers ( + t, + selectedOption, onConfirmBank = onConfirmBank, - onConfirmManual = onConfirmManual, + onConfirmKyc = onConfirmKyc, + onConfirmManual = { onConfirmManual(selectedOption) }, onShowQrCodes = onShowQrCodes, ) @@ -169,6 +228,77 @@ private val previewWithdrawalTx = TransactionWithdrawal( )) ) +@Composable +fun TransferAccountChooser( + modifier: Modifier = Modifier, + accounts: List<WithdrawalExchangeAccountDetails>, + selectedIndex: Int, + onSelectAccount: (index: Int) -> Unit, +) { + val selectedIndex = accounts.indexOfFirst { + it.paytoUri == accounts[selectedIndex].paytoUri + } + + PrimaryScrollableTabRow( + selectedTabIndex = selectedIndex, + modifier = modifier, + edgePadding = 8.dp, + ) { + accounts.forEachIndexed { index, account -> + Tab( + selected = accounts[selectedIndex].paytoUri == account.paytoUri, + onClick = { onSelectAccount(index) }, + text = { + if (!account.bankLabel.isNullOrEmpty()) { + Text(account.bankLabel) + } else 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)) + }, + ) + } + } +} + +@Composable +fun TransferOptionChooser( + modifier: Modifier = Modifier, + options: List<TransferOption>, + selectedIndex: Int, + onSelectOption: (index: Int) -> Unit, +) { + SecondaryScrollableTabRow ( + selectedTabIndex = selectedIndex, + modifier = modifier, + edgePadding = 8.dp, + ) { + options.forEachIndexed { index, option -> + Tab( + selected = index == selectedIndex, + onClick = { onSelectOption(index) }, + text = { + when(option) { + // FIXME: better i18n labels + is TransferOption.Payto -> Text("payto://") + is TransferOption.SwissQrBill -> Text("Swiss") + is TransferOption.Uri -> Text("link") + } + }, + ) + } + } +} + @Preview @Composable fun TransactionWithdrawalComposableSingleQrPreview() { @@ -176,8 +306,8 @@ fun TransactionWithdrawalComposableSingleQrPreview() { TransactionWithdrawalComposable( t = previewWithdrawalTx, devMode = true, - qrCode = QrCodeSpec(QrCodeSpec.Type.SPC, "something"), spec = null, + onSelectOption = {}, onConfirmKyc = {}, onConfirmBank = {}, onConfirmManual = {}, @@ -194,8 +324,8 @@ fun TransactionWithdrawalComposableMultiQrPreview() { TransactionWithdrawalComposable( t = previewWithdrawalTx, devMode = true, - qrCode = null, spec = null, + onSelectOption = {}, onConfirmKyc = {}, onConfirmBank = {}, onConfirmManual = {}, diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt @@ -36,9 +36,9 @@ import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.exchanges.ExchangeFees import net.taler.wallet.exchanges.ExchangeItem import net.taler.wallet.exchanges.ExchangeManager +import net.taler.wallet.transactions.TransferOption import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails import net.taler.wallet.withdraw.WithdrawStatus.Status.* -import kotlinx.coroutines.runBlocking import net.taler.common.CurrencySpecification import net.taler.wallet.R import net.taler.wallet.transactions.TransactionMajorState @@ -97,8 +97,7 @@ sealed class TransferData { abstract val amountEffective: Amount abstract val transferAmount: Amount abstract val withdrawalAccount: WithdrawalExchangeAccountDetails - - val currency get() = withdrawalAccount.transferAmount?.currency + abstract val transferOption: TransferOption data class Taler( override val subject: String, @@ -106,6 +105,7 @@ sealed class TransferData { override val amountEffective: Amount, override val transferAmount: Amount, override val withdrawalAccount: WithdrawalExchangeAccountDetails, + override val transferOption: TransferOption, val receiverName: String? = null, val account: String, val exchangeBaseUrl: String, @@ -117,6 +117,7 @@ sealed class TransferData { override val amountEffective: Amount, override val transferAmount: Amount, override val withdrawalAccount: WithdrawalExchangeAccountDetails, + override val transferOption: TransferOption, val receiverName: String? = null, val receiverPostalCode: String? = null, val receiverTown: String? = null, @@ -129,6 +130,7 @@ sealed class TransferData { override val amountEffective: Amount, override val transferAmount: Amount, override val withdrawalAccount: WithdrawalExchangeAccountDetails, + override val transferOption: TransferOption, val host: String, // host + fpath val account: String, // FIXME: account ID not used at all? val receiverName: String, // FIXME: actual ID used for payments? @@ -140,6 +142,7 @@ sealed class TransferData { override val amountEffective: Amount, override val transferAmount: Amount, override val withdrawalAccount: WithdrawalExchangeAccountDetails, + override val transferOption: TransferOption, val account: String, val segwitAddresses: List<String>, ): TransferData() @@ -228,11 +231,6 @@ data class AcceptManualWithdrawalResponse( ) @Serializable -data class GetQrCodesForPaytoResponse( - val codes: List<QrCodeSpec>, -) - -@Serializable data class QrCodeSpec( val type: Type = Type.Unknown, val qrContent: String, @@ -549,19 +547,6 @@ class WithdrawManager( } } - fun getQrCodesForPayto(uri: String): List<QrCodeSpec> = runBlocking { - var codes = emptyList<QrCodeSpec>() - api.request("getQrCodesForPayto", GetQrCodesForPaytoResponse.serializer()) { - put("paytoUri", uri) - }.onError { error -> - handleError("getQrCodesForPayto", error) - }.onSuccess { response -> - codes = response.codes - } - - return@runBlocking codes - } - private fun handleError(operation: String, error: TalerErrorInfo) { Log.e(TAG, "Error $operation $error") _withdrawStatus.update { value -> @@ -578,9 +563,11 @@ class WithdrawManager( transactionId = response.transactionId, withdrawalTransfers = response.withdrawalAccountsList.mapNotNull { val details = status.amountInfo ?: return@mapNotNull null + val opt = it.transferOptions.firstOrNull() ?: return@mapNotNull null it.getTransferDetails( amountRaw = details.amountRaw, amountEffective = details.amountEffective, + transferOption = opt, ) }, ) diff --git a/wallet/src/main/res/values-de/strings.xml b/wallet/src/main/res/values-de/strings.xml @@ -106,7 +106,8 @@ <string name="withdraw_manual_ready_iban">IBAN</string> <string name="withdraw_manual_ready_intro">Sie müssen %1$s von Ihrem regulären Bankkonto an den Zahlungsdienst überweisen, um %2$s als digitales Bargeld in diese Wallet zu erhalten.</string> <string name="withdraw_manual_ready_details_intro">Überweisungsanleitung</string> - <string name="withdraw_manual_ready_subject">Buchungsvermerk</string> + <string name="withdraw_manual_ready_subject">Verwendungszweck</string> + <string name="withdraw_manual_ready_subject_qr">QR-Referenz</string> <string name="withdraw_manual_ready_bank_button">In Banking-App öffnen</string> <string name="withdraw_manual_ready_warning">Dies ist obligatorisch, da Ihr Geld sonst nicht in dieser Wallet ankommt.</string> <string name="reload">Aktualisieren</string> diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml @@ -390,6 +390,7 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish <string name="withdraw_manual_ready_postal_code">Postal code</string> <string name="withdraw_manual_ready_receiver">Recipient</string> <string name="withdraw_manual_ready_subject">Subject</string> + <string name="withdraw_manual_ready_subject_ch_qr">QR reference</string> <string name="withdraw_manual_ready_town">Town</string> <string name="withdraw_manual_ready_warning">This is mandatory, otherwise your money will not arrive in this wallet.</string> <string name="withdraw_manual_step">&lt;b&gt;Step %1$s:&lt;/b&gt; %2$s</string>