taler-android

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

commit a3b83e0778437653a0d3e35f772127875435ce50
parent ab5819ac0b003ba33e251991585b112f3a6f0350
Author: Iván Ávalos <avalos@disroot.org>
Date:   Fri, 30 May 2025 22:59:28 +0200

[wallet] WIP: refactor cta-wire-transfer-details and implement state pending(kyc-auth)

Diffstat:
Mwallet/src/main/java/net/taler/wallet/transactions/ActionButtonComposable.kt | 3++-
Mwallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt | 45++++++++++++++++++++++-----------------------
Mwallet/src/main/java/net/taler/wallet/transactions/TransactionStateComposable.kt | 3+++
Mwallet/src/main/java/net/taler/wallet/transactions/Transactions.kt | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/java/net/taler/wallet/transfer/PaytoQrCard.kt | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/java/net/taler/wallet/transfer/ScreenTransfer.kt | 428+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/java/net/taler/wallet/transfer/TransferBitcoin.kt | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/java/net/taler/wallet/transfer/TransferIBAN.kt | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/java/net/taler/wallet/transfer/TransferTaler.kt | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/java/net/taler/wallet/transfer/WireTransferDetailsFragment.kt | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt | 20++++++++++++++++++--
Dwallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt | 137-------------------------------------------------------------------------------
Dwallet/src/main/java/net/taler/wallet/withdraw/manual/PaytoQrCard.kt | 58----------------------------------------------------------
Dwallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt | 426-------------------------------------------------------------------------------
Dwallet/src/main/java/net/taler/wallet/withdraw/manual/TransferBitcoin.kt | 109-------------------------------------------------------------------------------
Dwallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt | 98-------------------------------------------------------------------------------
Dwallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt | 94-------------------------------------------------------------------------------
Mwallet/src/main/res/navigation/nav_graph.xml | 18+++---------------
Mwallet/src/main/res/values/strings.xml | 6+++++-
19 files changed, 1107 insertions(+), 964 deletions(-)

diff --git a/wallet/src/main/java/net/taler/wallet/transactions/ActionButtonComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/ActionButtonComposable.kt @@ -36,6 +36,7 @@ import net.taler.wallet.transactions.TransactionMajorState.Pending import net.taler.wallet.transactions.TransactionMinorState.BalanceKycRequired import net.taler.wallet.transactions.TransactionMinorState.BankConfirmTransfer import net.taler.wallet.transactions.TransactionMinorState.ExchangeWaitReserve +import net.taler.wallet.transactions.TransactionMinorState.KycAuthRequired import net.taler.wallet.transactions.TransactionMinorState.KycRequired import net.taler.wallet.transactions.TransactionMinorState.MergeKycRequired @@ -60,7 +61,7 @@ fun ActionButton( when (tx.txState.minor) { KycRequired, BalanceKycRequired, MergeKycRequired -> KycButton(modifier, tx, listener) BankConfirmTransfer -> ConfirmBankButton(modifier, tx, listener) - ExchangeWaitReserve -> ConfirmManualButton(modifier, tx, listener) + ExchangeWaitReserve, KycAuthRequired -> ConfirmManualButton(modifier, tx, listener) else -> {} } } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt @@ -32,7 +32,6 @@ import net.taler.common.showError import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.TAG -import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.launchInAppBrowser import net.taler.wallet.showError import net.taler.wallet.transactions.TransactionAction.Abort @@ -125,28 +124,28 @@ abstract class TransactionDetailFragment : Fragment(), ActionListener { } ActionListener.Type.CONFIRM_MANUAL, - ActionListener.Type.SHOW_WIRE_QR -> { - if (tx !is TransactionWithdrawal) return - if (tx.withdrawalDetails !is ManualTransfer) return - if (tx.withdrawalDetails.exchangeCreditAccountDetails.isNullOrEmpty()) return - if (tx.exchangeBaseUrl == null) return - - withdrawManager.viewManualWithdrawal( - transactionId = tx.transactionId, - exchangeBaseUrl = tx.exchangeBaseUrl, - amountRaw = tx.amountRaw, - amountEffective = tx.amountEffective, - withdrawalAccountList = tx.withdrawalDetails.exchangeCreditAccountDetails, - scopeInfo = transactionManager.selectedScope.value - ?: tx.exchangeBaseUrl.let { - ScopeInfo.Exchange(currency = tx.amountRaw.currency, url = it) - }, - ) - - findNavController().navigate( - R.id.action_nav_transactions_detail_withdrawal_to_nav_exchange_manual_withdrawal_success, - bundleOf("showQrCodes" to (type == ActionListener.Type.SHOW_WIRE_QR)) - ) + ActionListener.Type.SHOW_WIRE_QR -> lifecycleScope.launch { + when (tx) { + is TransactionWithdrawal -> { + if (tx.withdrawalDetails !is ManualTransfer) return@launch + if (tx.withdrawalDetails.exchangeCreditAccountDetails.isNullOrEmpty()) return@launch + findNavController().navigate( + R.id.nav_wire_transfer_details, + bundleOf("showQrCodes" to (type == ActionListener.Type.SHOW_WIRE_QR)) + ) + + } + + is TransactionDeposit -> { + if (tx.kycAuthTransferInfo == null) return@launch + findNavController().navigate( + R.id.nav_wire_transfer_details, + bundleOf("showQrCodes" to (type == ActionListener.Type.SHOW_WIRE_QR)) + ) + } + + else -> {} + } } } } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionStateComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionStateComposable.kt @@ -49,6 +49,7 @@ import net.taler.wallet.transactions.TransactionMajorState.Suspended import net.taler.wallet.transactions.TransactionMinorState.BalanceKycInit import net.taler.wallet.transactions.TransactionMinorState.BalanceKycRequired import net.taler.wallet.transactions.TransactionMinorState.BankConfirmTransfer +import net.taler.wallet.transactions.TransactionMinorState.KycAuthRequired import net.taler.wallet.transactions.TransactionMinorState.KycRequired import net.taler.wallet.transactions.TransactionMinorState.MergeKycRequired import net.taler.wallet.transactions.TransactionMinorState.Repurchase @@ -68,6 +69,7 @@ fun TransactionStateComposable( TransactionState(Pending, KycRequired) -> stringResource(R.string.transaction_state_pending_kyc_bank) TransactionState(Pending, BalanceKycRequired) -> stringResource(R.string.transaction_state_pending_kyc_bank) TransactionState(Pending, MergeKycRequired) -> stringResource(R.string.transaction_state_pending_kyc_bank) + TransactionState(Pending, KycAuthRequired) -> stringResource(R.string.transaction_state_pending_kyc_auth) TransactionState(Pending) -> stringResource(R.string.transaction_state_pending) TransactionState(Aborted) -> if (tx is TransactionWithdrawal && tx.withdrawalDetails is ManualTransfer) { stringResource( @@ -129,6 +131,7 @@ fun TransactionStateComposablePreview() { TransactionStateComposable(modifier, state = TransactionState(Pending, KycRequired)) TransactionStateComposable(modifier, state = TransactionState(Pending, BalanceKycRequired)) TransactionStateComposable(modifier, state = TransactionState(Pending, MergeKycRequired)) + TransactionStateComposable(modifier, state = TransactionState(Pending, KycAuthRequired)) TransactionStateComposable(modifier, state = TransactionState(Pending)) TransactionStateComposable(modifier, state = TransactionState(Aborted)) TransactionStateComposable(modifier, state = TransactionState(Aborting)) diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -22,6 +22,7 @@ import android.util.Log import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.annotation.StringRes +import androidx.core.net.toUri import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -35,6 +36,7 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonElement import net.taler.common.Amount +import net.taler.common.Bech32 import net.taler.common.ContractMerchant import net.taler.common.ContractProduct import net.taler.common.ContractTerms @@ -52,6 +54,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.TransferData import java.util.UUID @Serializable @@ -268,6 +271,54 @@ data class WithdrawalExchangeAccountDetails ( @SerialName("error") Error; } + + fun getTransferDetails( + amountRaw: Amount, + amountEffective: Amount, + ): TransferData? { + val uri = paytoUri.trim().toUri() + val transferAmount = (transferAmount + ?: uri.getQueryParameter("amount") + ?.let { Amount.fromJSONString(it) } + ?: amountEffective).withSpec(currencySpecification) + return if ("bitcoin".equals(uri.authority, true)) { + val msg = uri.getQueryParameter("message").orEmpty() + val reg = "\\b([A-Z0-9]{52})\\b".toRegex().find(msg) + val reserve = reg?.value ?: uri.getQueryParameter("subject")!! + val segwitAddresses = + Bech32.generateFakeSegwitAddress(reserve, uri.pathSegments.first()) + TransferData.Bitcoin( + account = uri.lastPathSegment!!, + segwitAddresses = segwitAddresses, + subject = reserve, + amountRaw = amountRaw, + amountEffective = amountEffective, + transferAmount = transferAmount, + withdrawalAccount = copy(paytoUri = uri.toString()), + ) + } else if (uri.authority.equals("x-taler-bank", true)) { + TransferData.Taler( + account = uri.lastPathSegment!!, + receiverName = uri.getQueryParameter("receiver-name"), + subject = uri.getQueryParameter("message") ?: "Error: No message in URI", + amountRaw = amountRaw, + amountEffective = amountEffective, + exchangeBaseUrl = uri.pathSegments[0] ?: return null, + transferAmount = transferAmount, + withdrawalAccount = copy(paytoUri = uri.toString()), + ) + } else if (uri.authority.equals("iban", true)) { + TransferData.IBAN( + iban = uri.lastPathSegment!!, + receiverName = uri.getQueryParameter("receiver-name"), + subject = uri.getQueryParameter("message") ?: "Error: No message in URI", + amountRaw = amountRaw, + amountEffective = amountEffective, + transferAmount = transferAmount, + withdrawalAccount = copy(paytoUri = uri.toString()), + ) + } else null + } } @Serializable diff --git a/wallet/src/main/java/net/taler/wallet/transfer/PaytoQrCard.kt b/wallet/src/main/java/net/taler/wallet/transfer/PaytoQrCard.kt @@ -0,0 +1,57 @@ +/* + * This file is part of GNU Taler + * (C) 2024 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.transfer + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import net.taler.wallet.R +import net.taler.wallet.compose.ExpandableCard +import net.taler.wallet.compose.QrCodeUriComposable +import net.taler.wallet.withdraw.QrCodeSpec +import net.taler.wallet.withdraw.QrCodeSpec.Type.EpcQr +import net.taler.wallet.withdraw.QrCodeSpec.Type.SPC + +@Composable +fun PaytoQrCard( + expanded: Boolean, + setExpanded: (expanded: Boolean) -> Unit, + qrCode: QrCodeSpec, +) { + val label = when(qrCode.type) { + EpcQr -> stringResource(R.string.withdraw_manual_qr_epc) + SPC -> stringResource(R.string.withdraw_manual_qr_spc) + else -> return + } + + // TODO: copy/share actions + ExpandableCard( + expanded = expanded, + setExpanded = setExpanded, + header = { + Text(label, style = MaterialTheme.typography.titleMedium) + }, + content = { + QrCodeUriComposable( + talerUri = qrCode.qrContent, + clipBoardLabel = label, + showContents = false, + ) + }, + ) +} +\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/transfer/ScreenTransfer.kt b/wallet/src/main/java/net/taler/wallet/transfer/ScreenTransfer.kt @@ -0,0 +1,427 @@ +/* + * This file is part of GNU Taler + * (C) 2025 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.transfer + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.layout.size +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.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 +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.text.style.LineBreak +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import net.taler.common.Amount +import net.taler.common.CurrencySpecification +import net.taler.wallet.CURRENCY_BTC +import net.taler.wallet.R +import net.taler.common.canAppHandleUri +import net.taler.common.copyToClipBoard +import net.taler.wallet.BottomInsetsSpacer +import net.taler.wallet.compose.ShareButton +import net.taler.wallet.transactions.AmountType +import net.taler.wallet.transactions.KycAuthTransferInfo +import net.taler.wallet.transactions.TransactionAmountComposable +import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails +import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails.Status.Ok +import net.taler.wallet.withdraw.QrCodeSpec +import net.taler.wallet.withdraw.QrCodeSpec.Type.EpcQr +import net.taler.wallet.withdraw.QrCodeSpec.Type.SPC +import net.taler.wallet.withdraw.TransferData + +enum class TransferContext { + ManualWithdrawal, + DepositKycAuth, +} + +@Composable +fun ScreenTransfer( + transfers: List<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 qrExpandedStates = remember(qrCodes) { + val map = mutableStateMapOf<QrCodeSpec, Boolean>() + qrCodes.forEach { + map[it] = false + } + map + } + + LaunchedEffect(Unit) { + getQrCodes(defaultTransfer) + } + + Column { + 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, + ) + ) + + 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 + } + + when (val transfer = selectedTransfer) { + is TransferData.Taler -> TransferTaler( + transfer = transfer, + transactionAmountEffective = transfer.amountEffective.withSpec(spec), + transferContext = transferContext, + ) + + is TransferData.IBAN -> TransferIBAN( + transfer = transfer, + transactionAmountEffective = transfer.amountEffective.withSpec(spec), + transferContext = transferContext, + ) + + is TransferData.Bitcoin -> TransferBitcoin( + transfer = transfer, + ) + } + + Spacer(Modifier.height(24.dp)) + + 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)) + } + } + + if (shareClick != null) { + ShareButton( + content = selectedTransfer.withdrawalAccount.paytoUri, + modifier = Modifier + .padding(bottom = 16.dp), + ) + } + } + + BottomInsetsSpacer() + } + } +} + +@Composable +fun TransferStep( + index: Int, + description: String, +) { + Text( + modifier = Modifier.padding( + top = 16.dp, + start = 6.dp, + end = 6.dp, + bottom = 6.dp, + ), + text = AnnotatedString.fromHtml( + stringResource( + R.string.withdraw_manual_step, + index, + description, + ) + ), + style = MaterialTheme.typography.bodyMedium, + ) +} + +@Composable +fun DetailRow( + label: String, + content: String, + copy: Boolean = true, + characterBreak: Boolean = false, +) { + val context = LocalContext.current + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(top = 16.dp, start = 6.dp, end = 6.dp), + text = label, + style = MaterialTheme.typography.bodyMedium, + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.padding( + top = 8.dp, + start = 6.dp, + end = 6.dp, + ).weight(1f), + text = content, + style = if (characterBreak) { + MaterialTheme.typography.bodyLarge.copy( + lineBreak = LineBreak.Heading, + ) + } else MaterialTheme.typography.bodyLarge, + fontFamily = if (copy) FontFamily.Monospace else FontFamily.Default, + textAlign = TextAlign.Center, + ) + + if (copy) { + TextButton( + onClick = { copyToClipBoard(context, label, content) }, + ) { + Icon( + Icons.Default.ContentCopy, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.copy)) + } + } + } + } +} + +@Composable +fun WithdrawalAmountTransfer( + conversionAmountRaw: Amount, +) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TransactionAmountComposable( + label = stringResource(R.string.amount_transfer), + amount = conversionAmountRaw, + amountType = AmountType.Neutral, + context = LocalContext.current, + copy = true, + ) + } +} + +@Composable +fun TransferAccountChooser( + modifier: Modifier = Modifier, + accounts: List<WithdrawalExchangeAccountDetails>, + selectedAccount: WithdrawalExchangeAccountDetails, + onSelectAccount: (account: WithdrawalExchangeAccountDetails) -> Unit, +) { + val selectedIndex = accounts.indexOfFirst { + it.paytoUri == selectedAccount.paytoUri + } + + ScrollableTabRow( + selectedTabIndex = selectedIndex, + modifier = modifier, + 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)) + }, + ) + } + } +} + +@Preview +@Composable +fun ScreenTransferPreview( + 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), + transferAmount = Amount("KUDOS", 10, 0), + 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(), + ), + ), + ), + 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, + 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, + ) + } +} + +@Preview +@Composable +fun ScreenTransferKycAuthPreview() { + ScreenTransferPreview(transferContext = TransferContext.DepositKycAuth) +} + +@Preview +@Composable +fun ScreenTransferQRPreview() { + ScreenTransferPreview(true) +} +\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/transfer/TransferBitcoin.kt b/wallet/src/main/java/net/taler/wallet/transfer/TransferBitcoin.kt @@ -0,0 +1,108 @@ +/* + * This file is part of GNU Taler + * (C) 2025 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.transfer + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import net.taler.common.Amount +import net.taler.wallet.R +import net.taler.wallet.compose.CopyToClipboardButton +import net.taler.wallet.withdraw.TransferData + +@Composable +fun TransferBitcoin( + transfer: TransferData.Bitcoin, +) { + Column( + modifier = Modifier.padding(all = 16.dp), + horizontalAlignment = CenterHorizontally, + ) { + Text( + text = stringResource(R.string.withdraw_manual_bitcoin_intro), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .padding(vertical = 8.dp) + ) + + BitcoinSegwitAddresses( + amount = transfer.amountRaw, + address = transfer.account, + segwitAddresses = transfer.segwitAddresses, + ) + + transfer.withdrawalAccount.transferAmount?.let { amount -> + WithdrawalAmountTransfer( + conversionAmountRaw = amount.withSpec( + transfer.withdrawalAccount.currencySpecification, + ), + ) + } + } +} + +@Composable +fun BitcoinSegwitAddresses(amount: Amount, address: String, segwitAddresses: List<String>) { + Column { + val allSegwitAddresses = listOf(address) + segwitAddresses + for (segwitAddress in allSegwitAddresses) { + Row(modifier = Modifier.padding(vertical = 8.dp)) { + Column(modifier = Modifier.weight(0.3f)) { + Text( + text = segwitAddress, + fontWeight = FontWeight.Normal, + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.bodySmall, + ) + Text( + text = if (segwitAddress == address) + amount.withCurrency("BTC").toString() + else SEGWIT_MIN.toString(), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + ) + } + } + } + + CopyToClipboardButton( + modifier = Modifier + .padding(top = 16.dp, start = 6.dp, end = 6.dp) + .align(CenterHorizontally), + label = "Bitcoin", + content = getCopyText(amount, address, segwitAddresses), + ) + } +} + +private val SEGWIT_MIN = Amount("BTC", 0, 294) + +private fun getCopyText(amount: Amount, addr: String, segwitAddresses: List<String>): String { + val sr = segwitAddresses.joinToString(separator = "\n") { s -> + "\n$s $SEGWIT_MIN\n" + } + return "$addr ${amount.withCurrency("BTC")}\n$sr" +} +\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/transfer/TransferIBAN.kt b/wallet/src/main/java/net/taler/wallet/transfer/TransferIBAN.kt @@ -0,0 +1,124 @@ +/* + * This file is part of GNU Taler + * (C) 2025 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.transfer + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +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.stringResource +import androidx.compose.ui.unit.dp +import net.taler.common.Amount +import net.taler.wallet.R +import net.taler.wallet.compose.WarningLabel +import net.taler.wallet.withdraw.TransferData +import net.taler.wallet.transfer.TransferContext.* + +@Composable +fun TransferIBAN( + transfer: TransferData.IBAN, + transactionAmountEffective: Amount, + transferContext: TransferContext, +) { + Column( + modifier = Modifier.padding(all = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = when(transferContext) { + ManualWithdrawal -> stringResource( + R.string.withdraw_manual_ready_intro, + transfer.transferAmount, + transactionAmountEffective, + ) + + DepositKycAuth -> stringResource( + R.string.send_deposit_kyc_auth_intro_bank, + transfer.transferAmount, + transfer.iban, + ) + }, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .padding(vertical = 8.dp) + ) + + if (transferContext == DepositKycAuth) { + WarningLabel( + modifier = Modifier.padding( + horizontal = 8.dp, + vertical = 16.dp, + ), + label = stringResource(R.string.send_deposit_kyc_auth_warning_account), + ) + } + + HorizontalDivider( + modifier = Modifier.padding(vertical = 6.dp) + ) + + TransferStep(1, stringResource(R.string.withdraw_manual_step_subject)) + + DetailRow( + stringResource(R.string.withdraw_manual_ready_subject), + transfer.subject, + characterBreak = true, + ) + + WarningLabel( + modifier = Modifier.padding( + horizontal = 8.dp, + vertical = 16.dp, + ), + label = when (transferContext) { + ManualWithdrawal -> stringResource(R.string.withdraw_manual_ready_warning) + DepositKycAuth -> stringResource(R.string.send_deposit_kyc_auth_warning_subject) + }, + ) + + TransferStep(2, stringResource(R.string.withdraw_manual_step_iban)) + + transfer.receiverName?.let { + DetailRow(stringResource(R.string.withdraw_manual_ready_receiver), it) + } + + DetailRow(stringResource(R.string.withdraw_manual_ready_iban), transfer.iban) + + WithdrawalAmountTransfer( + conversionAmountRaw = transfer.transferAmount, + ) + + TransferStep(3, + when (transferContext) { + ManualWithdrawal -> stringResource( + R.string.withdraw_manual_step_finish, + transfer.transferAmount, + ) + + DepositKycAuth -> stringResource( + R.string.send_deposit_kyc_auth_step_finish, + transfer.transferAmount, + transfer.iban, + ) + } + ) + } +} +\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/transfer/TransferTaler.kt b/wallet/src/main/java/net/taler/wallet/transfer/TransferTaler.kt @@ -0,0 +1,109 @@ +/* + * This file is part of GNU Taler + * (C) 2025 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.transfer + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +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.stringResource +import androidx.compose.ui.unit.dp +import net.taler.common.Amount +import net.taler.wallet.R +import net.taler.wallet.cleanExchange +import net.taler.wallet.compose.WarningLabel +import net.taler.wallet.transactions.TransactionInfoComposable +import net.taler.wallet.withdraw.TransferData +import net.taler.wallet.transfer.TransferContext.* + +@Composable +fun TransferTaler( + transfer: TransferData.Taler, + transactionAmountEffective: Amount, + transferContext: TransferContext, +) { + Column( + modifier = Modifier.padding(all = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = when (transferContext) { + ManualWithdrawal -> stringResource( + R.string.withdraw_manual_ready_intro, + transfer.transferAmount, + transactionAmountEffective, + ) + + DepositKycAuth -> stringResource( + R.string.send_deposit_kyc_auth_intro_bank, + transfer.transferAmount, + transfer.account, + ) + }, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .padding(vertical = 8.dp) + ) + + if (transferContext == DepositKycAuth) { + WarningLabel( + modifier = Modifier.padding( + horizontal = 8.dp, + vertical = 16.dp, + ), + label = stringResource(R.string.send_deposit_kyc_auth_warning_account), + ) + } + + HorizontalDivider( + modifier = Modifier.padding(vertical = 6.dp) + ) + + DetailRow( + stringResource(R.string.withdraw_manual_ready_subject), + transfer.subject, + characterBreak = true, + ) + + WarningLabel( + modifier = Modifier.padding( + horizontal = 8.dp, + vertical = 16.dp, + ), + label = when (transferContext) { + ManualWithdrawal -> stringResource(R.string.withdraw_manual_ready_warning) + DepositKycAuth -> stringResource(R.string.send_deposit_kyc_auth_warning_subject) + }, + ) + + transfer.receiverName?.let { + DetailRow(stringResource(R.string.withdraw_manual_ready_receiver), it) + } + + DetailRow(stringResource(R.string.withdraw_manual_ready_account), transfer.account) + + DetailRow(stringResource(R.string.withdraw_exchange), cleanExchange(transfer.exchangeBaseUrl)) + + WithdrawalAmountTransfer( + conversionAmountRaw = transfer.transferAmount, + ) + } +} +\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/transfer/WireTransferDetailsFragment.kt b/wallet/src/main/java/net/taler/wallet/transfer/WireTransferDetailsFragment.kt @@ -0,0 +1,173 @@ +/* + * This file is part of GNU Taler + * (C) 2022 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.transfer + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.NavOptions +import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.launch +import net.taler.common.openUri +import net.taler.common.shareText +import net.taler.wallet.MainViewModel +import net.taler.wallet.R +import net.taler.wallet.compose.TalerSurface +import net.taler.wallet.compose.collectAsStateLifecycleAware +import net.taler.wallet.transactions.Transaction +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.WithdrawalExchangeAccountDetails +import net.taler.wallet.transfer.ScreenTransfer +import net.taler.wallet.withdraw.TransferData + +class WireTransferDetailsFragment : Fragment() { + private val model: MainViewModel by activityViewModels() + private val withdrawManager by lazy { model.withdrawManager } + private val transactionManager by lazy { model.transactionManager } + private val balanceManager by lazy { model.balanceManager } + + private var navigating: Boolean = false + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?, + ): View = ComposeView(requireContext()).apply { + val showQrCodes = arguments?.getBoolean("showQrCodes") == true + setContent { + TalerSurface { + val selectedTx by transactionManager.selectedTransaction.collectAsStateLifecycleAware() + val devMode by model.devMode.observeAsState() + + // TODO: move this code somewhere else + // TODO: better error handling + 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, + ) + } + } + + else -> null + }?.map { + it.getTransferDetails( + amountRaw = tx.amountRaw, + amountEffective = tx.amountEffective + ) + } + } + }?.filterNotNull() ?: return@TalerSurface + + ScreenTransfer( + transfers = transfers, + getQrCodes = { withdrawManager.getQrCodesForPayto(it.withdrawalAccount.paytoUri) }, + spec = selectedTx?.amountRaw?.currency?.let { + selectedTx?.scopes?.let { selectedScopes -> + balanceManager.getSpecForCurrency(it, selectedScopes) + } ?: run { + balanceManager.getSpecForCurrency(it) + } + }, + bankAppClick = { onBankAppClick(it) }, + shareClick = { onShareClick(it) }, + showQrCodes = showQrCodes, + devMode = devMode == true, + transferContext = when(selectedTx) { + is TransactionWithdrawal -> TransferContext.ManualWithdrawal + is TransactionDeposit -> TransferContext.DepositKycAuth + else -> return@TalerSurface + } + ) + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + model.withdrawManager.withdrawStatus.collect { status -> + // Set action bar subtitle and unset on exit + if (status.withdrawalTransfers.size > 1) { + (requireActivity() as? AppCompatActivity)?.apply { + supportActionBar?.subtitle = getString(R.string.withdraw_subtitle) + } + } + } + } + } + + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + model.transactionManager.selectedTransaction.collect { tx -> + if (tx?.txState?.major == Done) { + if (navigating) return@collect + findNavController().popBackStack() + navigating = true + } + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + (requireActivity() as? AppCompatActivity)?.apply { + supportActionBar?.subtitle = null + } + } + + private fun onBankAppClick(transfer: TransferData) { + requireContext().openUri( + uri = transfer.withdrawalAccount.paytoUri, + title = requireContext().getString(R.string.share_payment) + ) + } + + private fun onShareClick(transfer: TransferData) { + requireContext().shareText( + text = transfer.withdrawalAccount.paytoUri, + ) + } +} diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt @@ -42,6 +42,7 @@ import net.taler.wallet.exchanges.ExchangeTosStatus import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails import net.taler.wallet.withdraw.WithdrawStatus.Status.* import androidx.core.net.toUri +import kotlinx.coroutines.runBlocking import net.taler.wallet.transactions.TransactionMajorState import net.taler.wallet.transactions.TransactionManager @@ -92,6 +93,7 @@ sealed class TransferData { abstract val subject: String abstract val amountRaw: Amount abstract val amountEffective: Amount + abstract val transferAmount: Amount abstract val withdrawalAccount: WithdrawalExchangeAccountDetails val currency get() = withdrawalAccount.transferAmount?.currency @@ -100,15 +102,18 @@ sealed class TransferData { override val subject: String, override val amountRaw: Amount, override val amountEffective: Amount, + override val transferAmount: Amount, override val withdrawalAccount: WithdrawalExchangeAccountDetails, val receiverName: String? = null, val account: String, + val exchangeBaseUrl: String, ): TransferData() data class IBAN( override val subject: String, override val amountRaw: Amount, override val amountEffective: Amount, + override val transferAmount: Amount, override val withdrawalAccount: WithdrawalExchangeAccountDetails, val receiverName: String? = null, val iban: String, @@ -118,6 +123,7 @@ sealed class TransferData { override val subject: String, override val amountRaw: Amount, override val amountEffective: Amount, + override val transferAmount: Amount, override val withdrawalAccount: WithdrawalExchangeAccountDetails, val account: String, val segwitAddresses: List<String>, @@ -488,7 +494,7 @@ class WithdrawManager( } } - fun getQrCodesForPayto(uri: String) = scope.launch { + fun getQrCodesForPayto(uri: String): List<QrCodeSpec> = runBlocking { var codes = emptyList<QrCodeSpec>() api.request("getQrCodesForPayto", GetQrCodesForPaytoResponse.serializer()) { put("paytoUri", uri) @@ -498,7 +504,7 @@ class WithdrawManager( codes = response.codes } - qrCodes.value = codes + return@runBlocking codes } private fun handleError(operation: String, error: TalerErrorInfo) { @@ -562,6 +568,9 @@ class WithdrawManager( subject = reserve, amountRaw = details.amountRaw, amountEffective = details.amountEffective, + transferAmount = it.transferAmount + ?.withSpec(it.currencySpecification) + ?: details.amountEffective, withdrawalAccount = it.copy(paytoUri = uri.toString()), ) } else if (uri.authority.equals("x-taler-bank", true)) { @@ -571,6 +580,10 @@ class WithdrawManager( subject = uri.getQueryParameter("message") ?: "Error: No message in URI", amountRaw = details.amountRaw, amountEffective = details.amountEffective, + exchangeBaseUrl = uri.host!!, + transferAmount = it.transferAmount + ?.withSpec(it.currencySpecification) + ?: details.amountEffective, withdrawalAccount = it.copy(paytoUri = uri.toString()), ) } else if (uri.authority.equals("iban", true)) { @@ -580,6 +593,9 @@ class WithdrawManager( subject = uri.getQueryParameter("message") ?: "Error: No message in URI", amountRaw = details.amountRaw, amountEffective = details.amountEffective, + transferAmount = it.transferAmount + ?.withSpec(it.currencySpecification) + ?: details.amountEffective, withdrawalAccount = it.copy(paytoUri = uri.toString()), ) } else null 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 @@ -1,137 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2022 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet.withdraw.manual - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.NavOptions -import androidx.navigation.fragment.findNavController -import kotlinx.coroutines.launch -import net.taler.common.openUri -import net.taler.common.shareText -import net.taler.wallet.MainViewModel -import net.taler.wallet.R -import net.taler.wallet.compose.TalerSurface -import net.taler.wallet.compose.collectAsStateLifecycleAware -import net.taler.wallet.transactions.Transaction -import net.taler.wallet.transactions.TransactionMajorState.Done -import net.taler.wallet.withdraw.TransferData - -class ManualWithdrawSuccessFragment : Fragment() { - private val model: MainViewModel by activityViewModels() - private val withdrawManager by lazy { model.withdrawManager } - private val transactionManager by lazy { model.transactionManager } - private val balanceManager by lazy { model.balanceManager } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?, - ): View = ComposeView(requireContext()).apply { - val showQrCodes = arguments?.getBoolean("showQrCodes") == true - setContent { - TalerSurface { - val status by withdrawManager.withdrawStatus.collectAsStateLifecycleAware() - val selectedTx by transactionManager.selectedTransaction.collectAsStateLifecycleAware() - val qrCodes by withdrawManager.qrCodes.observeAsState() - val devMode by model.devMode.observeAsState() - - ScreenTransfer( - status = status, - qrCodes = qrCodes ?: emptyList(), - getQrCodes = { withdrawManager.getQrCodesForPayto(it.paytoUri) }, - spec = status.amountInfo?.amountRaw?.currency?.let { - selectedTx?.scopes?.let { selectedScopes -> - balanceManager.getSpecForCurrency(it, selectedScopes) - } ?: run { - balanceManager.getSpecForCurrency(it) - } - }, - bankAppClick = { onBankAppClick(it) }, - shareClick = { onShareClick(it) }, - showQrCodes = showQrCodes, - devMode = devMode == true, - ) - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - model.withdrawManager.withdrawStatus.collect { status -> - // Set action bar subtitle and unset on exit - if (status.withdrawalTransfers.size > 1) { - (requireActivity() as? AppCompatActivity)?.apply { - supportActionBar?.subtitle = getString(R.string.withdraw_subtitle) - } - } - } - } - } - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - model.transactionManager.selectedTransaction.collect { tx -> - if (tx?.txState?.major == Done) { - navigateToDetails(tx) - } - } - } - } - } - - override fun onDestroy() { - super.onDestroy() - (requireActivity() as? AppCompatActivity)?.apply { - supportActionBar?.subtitle = null - } - } - - private fun navigateToDetails(tx: Transaction) { - val options = NavOptions.Builder() - .setPopUpTo(R.id.nav_main, false) - .build() - findNavController() - .navigate(tx.detailPageNav, null, options) - } - - private fun onBankAppClick(transfer: TransferData) { - requireContext().openUri( - uri = transfer.withdrawalAccount.paytoUri, - title = requireContext().getString(R.string.share_payment) - ) - } - - private fun onShareClick(transfer: TransferData) { - requireContext().shareText( - text = transfer.withdrawalAccount.paytoUri, - ) - } -} diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/PaytoQrCard.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/PaytoQrCard.kt @@ -1,57 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2024 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.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import net.taler.wallet.R -import net.taler.wallet.compose.ExpandableCard -import net.taler.wallet.compose.QrCodeUriComposable -import net.taler.wallet.withdraw.QrCodeSpec -import net.taler.wallet.withdraw.QrCodeSpec.Type.EpcQr -import net.taler.wallet.withdraw.QrCodeSpec.Type.SPC - -@Composable -fun PaytoQrCard( - expanded: Boolean, - setExpanded: (expanded: Boolean) -> Unit, - qrCode: QrCodeSpec, -) { - val label = when(qrCode.type) { - EpcQr -> stringResource(R.string.withdraw_manual_qr_epc) - SPC -> stringResource(R.string.withdraw_manual_qr_spc) - else -> return - } - - // TODO: copy/share actions - ExpandableCard( - expanded = expanded, - setExpanded = setExpanded, - header = { - Text(label, style = MaterialTheme.typography.titleMedium) - }, - content = { - QrCodeUriComposable( - talerUri = qrCode.qrContent, - clipBoardLabel = label, - showContents = false, - ) - }, - ) -} -\ No newline at end of file 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 @@ -1,425 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2023 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet.withdraw.manual - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -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.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 -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.fromHtml -import androidx.compose.ui.text.style.LineBreak -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import net.taler.common.Amount -import net.taler.common.CurrencySpecification -import net.taler.wallet.CURRENCY_BTC -import net.taler.wallet.R -import net.taler.common.canAppHandleUri -import net.taler.common.copyToClipBoard -import net.taler.wallet.BottomInsetsSpacer -import net.taler.wallet.balances.ScopeInfo -import net.taler.wallet.compose.ShareButton -import net.taler.wallet.transactions.AmountType -import net.taler.wallet.transactions.TransactionAmountComposable -import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails -import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails.Status.Ok -import net.taler.wallet.withdraw.QrCodeSpec -import net.taler.wallet.withdraw.QrCodeSpec.Type.EpcQr -import net.taler.wallet.withdraw.QrCodeSpec.Type.SPC -import net.taler.wallet.withdraw.TransferData -import net.taler.wallet.withdraw.WithdrawStatus -import net.taler.wallet.withdraw.WithdrawalDetailsForAmount - -@Composable -fun ScreenTransfer( - status: WithdrawStatus, - qrCodes: List<QrCodeSpec>, - spec: CurrencySpecification?, - showQrCodes: Boolean, - getQrCodes: (account: WithdrawalExchangeAccountDetails) -> Unit, - bankAppClick: ((transfer: TransferData) -> Unit)?, - shareClick: ((transfer: TransferData) -> Unit)?, - devMode: Boolean = false, -) { - // TODO: show some placeholder - if (status.withdrawalTransfers.isEmpty()) return - - val transfers = status.withdrawalTransfers.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 qrExpandedStates = remember(qrCodes) { - val map = mutableStateMapOf<QrCodeSpec, Boolean>() - qrCodes.forEach { - map[it] = false - } - map - } - - LaunchedEffect(Unit) { - getQrCodes(defaultTransfer.withdrawalAccount) - } - - Column { - if (status.withdrawalTransfers.size > 1) { - TransferAccountChooser( - accounts = transfers.map { it.withdrawalAccount }, - selectedAccount = selectedTransfer.withdrawalAccount, - onSelectAccount = { account -> - status.withdrawalTransfers.find { - it.withdrawalAccount.paytoUri == account.paytoUri - }?.let { - selectedTransfer = it - getQrCodes(it.withdrawalAccount) - } - } - ) - } - - 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 - } - } - // expand only toggled one - qrExpandedStates[spec] = expanded - }, - qrCode = spec, - ) - } - - BottomInsetsSpacer() - return - } - - when (val transfer = selectedTransfer) { - is TransferData.Taler -> TransferTaler( - transfer = transfer, - exchangeBaseUrl = status.exchangeBaseUrl!!, - transactionAmountEffective = status.amountInfo!!.amountEffective.withSpec(spec), - ) - - is TransferData.IBAN -> TransferIBAN( - transfer = transfer, - transactionAmountEffective = status.amountInfo!!.amountEffective.withSpec(spec), - ) - - is TransferData.Bitcoin -> TransferBitcoin( - transfer = transfer, - ) - } - - Spacer(Modifier.height(24.dp)) - - 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)) - } - } - - if (shareClick != null) { - ShareButton( - content = selectedTransfer.withdrawalAccount.paytoUri, - modifier = Modifier - .padding(bottom = 16.dp), - ) - } - } - - BottomInsetsSpacer() - } - } -} - -@Composable -fun TransferStep( - index: Int, - description: String, -) { - Text( - modifier = Modifier.padding( - top = 16.dp, - start = 6.dp, - end = 6.dp, - bottom = 6.dp, - ), - text = AnnotatedString.fromHtml( - stringResource( - R.string.withdraw_manual_step, - index, - description, - ) - ), - style = MaterialTheme.typography.bodyMedium, - ) -} - -@Composable -fun DetailRow( - label: String, - content: String, - copy: Boolean = true, - characterBreak: Boolean = false, -) { - val context = LocalContext.current - - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - modifier = Modifier.padding(top = 16.dp, start = 6.dp, end = 6.dp), - text = label, - style = MaterialTheme.typography.bodyMedium, - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.padding( - top = 8.dp, - start = 6.dp, - end = 6.dp, - ).weight(1f), - text = content, - style = if (characterBreak) { - MaterialTheme.typography.bodyLarge.copy( - lineBreak = LineBreak.Heading, - ) - } else MaterialTheme.typography.bodyLarge, - fontFamily = if (copy) FontFamily.Monospace else FontFamily.Default, - textAlign = TextAlign.Center, - ) - - if (copy) { - TextButton( - onClick = { copyToClipBoard(context, label, content) }, - ) { - Icon( - Icons.Default.ContentCopy, - contentDescription = null, - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - Text(stringResource(R.string.copy)) - } - } - } - } -} - -@Composable -fun WithdrawalAmountTransfer( - conversionAmountRaw: Amount, -) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - TransactionAmountComposable( - label = stringResource(R.string.amount_transfer), - amount = conversionAmountRaw, - amountType = AmountType.Neutral, - context = LocalContext.current, - copy = true, - ) - } -} - -@Composable -fun TransferAccountChooser( - modifier: Modifier = Modifier, - accounts: List<WithdrawalExchangeAccountDetails>, - selectedAccount: WithdrawalExchangeAccountDetails, - onSelectAccount: (account: WithdrawalExchangeAccountDetails) -> Unit, -) { - val selectedIndex = accounts.indexOfFirst { - it.paytoUri == selectedAccount.paytoUri - } - - ScrollableTabRow( - selectedTabIndex = selectedIndex, - modifier = modifier, - 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)) - }, - ) - } - } -} - -@Preview -@Composable -fun ScreenTransferPreview( - showQrCodes: Boolean = false, -) { - Surface { - ScreenTransfer( - status = WithdrawStatus( - transactionId = "", - amountInfo = WithdrawalDetailsForAmount( - amountRaw = Amount.fromJSONString("KUDOS:10"), - amountEffective = Amount.fromJSONString("KUDOS:9.5"), - scopeInfo = ScopeInfo.Global("KUDOS"), - tosAccepted = true, - withdrawalAccountsList = listOf(), - ), - exchangeBaseUrl = "test.exchange.taler.net", - withdrawalTransfers = listOf( - TransferData.IBAN( - iban = "ASDQWEASDZXCASDQWE", - subject = "Taler Withdrawal P2T19EXRBY4B145JRNZ8CQTD7TCS03JE9VZRCEVKVWCP930P56WG", - amountRaw = Amount("KUDOS", 10, 0), - amountEffective = Amount("KUDOS", 9, 5), - withdrawalAccount = WithdrawalExchangeAccountDetails( - paytoUri = "https://taler.net/kudos", - transferAmount = Amount("KUDOS", 10, 0), - status = Ok, - currencySpecification = CurrencySpecification( - "KUDOS", - numFractionalInputDigits = 2, - numFractionalNormalDigits = 2, - numFractionalTrailingZeroDigits = 2, - altUnitNames = emptyMap(), - ), - ), - ), - TransferData.Bitcoin( - account = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", - segwitAddresses = listOf( - "bc1qqleages8702xvg9qcyu02yclst24xurdrynvxq", - "bc1qsleagehks96u7jmqrzcf0fw80ea5g57qm3m84c" - ), - subject = "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00", - amountRaw = Amount(CURRENCY_BTC, 0, 14000000), - amountEffective = Amount(CURRENCY_BTC, 0, 14000000), - withdrawalAccount = WithdrawalExchangeAccountDetails( - paytoUri = "https://taler.net/btc", - transferAmount = Amount("BTC", 0, 14000000), - status = Ok, - currencySpecification = CurrencySpecification( - "Bitcoin", - numFractionalInputDigits = 2, - numFractionalNormalDigits = 2, - numFractionalTrailingZeroDigits = 2, - altUnitNames = emptyMap(), - ), - ), - ) - ), - ), - spec = null, - bankAppClick = {}, - shareClick = {}, - 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") - ), - showQrCodes = showQrCodes, - getQrCodes = {}, - ) - } -} - -@Preview -@Composable -fun ScreenTransferQRPreview() { - ScreenTransferPreview(true) -} -\ 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 @@ -1,108 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2023 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet.withdraw.manual - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment.Companion.CenterHorizontally -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import net.taler.common.Amount -import net.taler.wallet.R -import net.taler.wallet.compose.CopyToClipboardButton -import net.taler.wallet.withdraw.TransferData - -@Composable -fun TransferBitcoin( - transfer: TransferData.Bitcoin, -) { - Column( - modifier = Modifier.padding(all = 16.dp), - horizontalAlignment = CenterHorizontally, - ) { - Text( - text = stringResource(R.string.withdraw_manual_bitcoin_intro), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .padding(vertical = 8.dp) - ) - - BitcoinSegwitAddresses( - amount = transfer.amountRaw, - address = transfer.account, - segwitAddresses = transfer.segwitAddresses, - ) - - transfer.withdrawalAccount.transferAmount?.let { amount -> - WithdrawalAmountTransfer( - conversionAmountRaw = amount.withSpec( - transfer.withdrawalAccount.currencySpecification, - ), - ) - } - } -} - -@Composable -fun BitcoinSegwitAddresses(amount: Amount, address: String, segwitAddresses: List<String>) { - Column { - val allSegwitAddresses = listOf(address) + segwitAddresses - for (segwitAddress in allSegwitAddresses) { - Row(modifier = Modifier.padding(vertical = 8.dp)) { - Column(modifier = Modifier.weight(0.3f)) { - Text( - text = segwitAddress, - fontWeight = FontWeight.Normal, - fontFamily = FontFamily.Monospace, - style = MaterialTheme.typography.bodySmall, - ) - Text( - text = if (segwitAddress == address) - amount.withCurrency("BTC").toString() - else SEGWIT_MIN.toString(), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Bold, - ) - } - } - } - - CopyToClipboardButton( - modifier = Modifier - .padding(top = 16.dp, start = 6.dp, end = 6.dp) - .align(CenterHorizontally), - label = "Bitcoin", - content = getCopyText(amount, address, segwitAddresses), - ) - } -} - -private val SEGWIT_MIN = Amount("BTC", 0, 294) - -private fun getCopyText(amount: Amount, addr: String, segwitAddresses: List<String>): String { - val sr = segwitAddresses.joinToString(separator = "\n") { s -> - "\n$s ${SEGWIT_MIN}\n" - } - return "$addr ${amount.withCurrency("BTC")}\n$sr" -} -\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt @@ -1,97 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2023 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet.withdraw.manual - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.HorizontalDivider -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.stringResource -import androidx.compose.ui.unit.dp -import net.taler.common.Amount -import net.taler.wallet.R -import net.taler.wallet.compose.WarningLabel -import net.taler.wallet.withdraw.TransferData - -@Composable -fun TransferIBAN( - transfer: TransferData.IBAN, - transactionAmountEffective: Amount, -) { - val transferAmount = transfer - .withdrawalAccount - .transferAmount - ?.withSpec(transfer.withdrawalAccount.currencySpecification) - ?: transfer.amountRaw - - Column( - modifier = Modifier.padding(all = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResource( - R.string.withdraw_manual_ready_intro, - transferAmount, - transactionAmountEffective, - ), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .padding(vertical = 8.dp) - ) - - HorizontalDivider( - modifier = Modifier.padding(vertical = 6.dp) - ) - - TransferStep(1, stringResource(R.string.withdraw_manual_step_subject)) - - DetailRow( - stringResource(R.string.withdraw_manual_ready_subject), - transfer.subject, - characterBreak = true, - ) - - WarningLabel( - modifier = Modifier.padding( - horizontal = 8.dp, - vertical = 16.dp, - ), - label = stringResource(R.string.withdraw_manual_ready_warning), - ) - - TransferStep(2, stringResource(R.string.withdraw_manual_step_iban)) - - transfer.receiverName?.let { - DetailRow(stringResource(R.string.withdraw_manual_ready_receiver), it) - } - - DetailRow(stringResource(R.string.withdraw_manual_ready_iban), transfer.iban) - - WithdrawalAmountTransfer( - conversionAmountRaw = transferAmount, - ) - - TransferStep(3, stringResource( - R.string.withdraw_manual_step_finish, - transferAmount, - )) - } -} -\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt @@ -1,93 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2024 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet.withdraw.manual - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import net.taler.common.Amount -import net.taler.wallet.R -import net.taler.wallet.cleanExchange -import net.taler.wallet.compose.WarningLabel -import net.taler.wallet.transactions.TransactionInfoComposable -import net.taler.wallet.withdraw.TransferData - -@Composable -fun TransferTaler( - transfer: TransferData.Taler, - exchangeBaseUrl: String, - transactionAmountEffective: Amount, -) { - val transferAmount = transfer - .withdrawalAccount - .transferAmount - ?.withSpec(transfer.withdrawalAccount.currencySpecification) - ?: transfer.amountRaw - - Column( - modifier = Modifier.padding(all = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResource( - R.string.withdraw_manual_ready_intro, - transferAmount, - transactionAmountEffective, - ), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .padding(vertical = 8.dp) - ) - - WarningLabel( - modifier = Modifier.padding(8.dp), - label = stringResource(R.string.withdraw_manual_ready_warning), - ) - - DetailRow( - stringResource(R.string.withdraw_manual_ready_subject), - transfer.subject, - characterBreak = true, - ) - - WarningLabel( - modifier = Modifier.padding(8.dp), - label = stringResource(R.string.withdraw_manual_ready_warning), - ) - - transfer.receiverName?.let { - DetailRow(stringResource(R.string.withdraw_manual_ready_receiver), it) - } - - DetailRow(stringResource(R.string.withdraw_manual_ready_account), transfer.account) - - TransactionInfoComposable( - label = stringResource(R.string.withdraw_exchange), - info = cleanExchange(exchangeBaseUrl), - ) - - WithdrawalAmountTransfer( - conversionAmountRaw = transferAmount, - ) - } -} -\ No newline at end of file diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml @@ -101,13 +101,9 @@ android:label="@string/exchange_list_title" /> <fragment - android:id="@+id/nav_exchange_manual_withdrawal_success" - android:name="net.taler.wallet.withdraw.manual.ManualWithdrawSuccessFragment" + android:id="@+id/nav_wire_transfer_details" + android:name="net.taler.wallet.transfer.WireTransferDetailsFragment" android:label="@string/withdraw_manual_ready_details_intro"> - <action - android:id="@+id/action_nav_exchange_manual_withdrawal_success_to_nav_main" - app:destination="@id/nav_main" - app:popUpTo="@id/nav_main" /> </fragment> <fragment @@ -219,11 +215,7 @@ <fragment android:id="@+id/nav_transactions_detail_withdrawal" android:name="net.taler.wallet.transactions.TransactionWithdrawalFragment" - android:label="@string/withdraw_title"> - <action - android:id="@+id/action_nav_transactions_detail_withdrawal_to_nav_exchange_manual_withdrawal_success" - app:destination="@id/nav_exchange_manual_withdrawal_success" /> - </fragment> + android:label="@string/withdraw_title" /> <fragment android:id="@+id/nav_transactions_detail_payment" @@ -289,10 +281,6 @@ app:destination="@id/nav_main" app:popUpTo="@id/nav_main" /> <action - android:id="@+id/action_promptWithdraw_to_nav_exchange_manual_withdrawal_success" - app:destination="@id/nav_exchange_manual_withdrawal_success" - app:popUpTo="@id/nav_main" /> - <action android:id="@+id/action_promptWithdraw_to_nav_transactions_detail_withdrawal" app:destination="@id/nav_transactions_detail_withdrawal" app:popUpTo="@id/nav_main" /> diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml @@ -155,6 +155,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="transaction_state_pending">This transaction is pending</string> <string name="transaction_state_pending_bank">Waiting for authorization in the bank</string> <string name="transaction_state_pending_kyc_bank">This transaction would exceed a limit set by the payment service provider. To increase the limit, follow the instructions linked below.</string> + <string name="transaction_state_pending_kyc_auth">You need to verify having control over the bank account for the deposit. To continue, follow the instructions linked below.</string> <string name="transaction_state_suspended">This transaction is suspended</string> <string name="transactions_abort">Abort</string> <string name="transactions_abort_dialog_message">Are you sure you want to abort this transaction? Funds still in transit might get lost.</string> @@ -253,6 +254,10 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="send_deposit_known_bank_accounts">Known bank accounts</string> <string name="send_deposit_known_bank_account_delete">Delete</string> <string name="send_deposit_known_bank_accounts_empty">No bank accounts saved in the wallet</string> + <string name="send_deposit_kyc_auth_intro_bank">You need to transfer %1$s from the bank account %2$s to the payment service to verify having control over it.</string> + <string name="send_deposit_kyc_auth_step_finish">Finish the wire transfer of %1$s in your banking app or website to verify having control over the bank account %2$s. Depending on your bank the transfer can take from minutes to two working days, please be patient.</string> + <string name="send_deposit_kyc_auth_warning_account">Don\'t use a different bank account, or the verification will fail.</string> + <string name="send_deposit_kyc_auth_warning_subject">This is mandatory, otherwise the verification will fail.</string> <string name="send_deposit_name">Account holder</string> <string name="send_deposit_no_methods_error">No supported wire methods</string> <string name="send_deposit_select_account_title">Select bank account</string> @@ -298,7 +303,6 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="withdraw_manual_ready_details_intro">Wire transfer instructions</string> <string name="withdraw_manual_ready_details_qr">Wire transfer QR codes</string> <string name="withdraw_manual_ready_iban">IBAN</string> -<!-- <string name="withdraw_manual_ready_intro">To complete the process you need to wire %s to the provider\'s bank account</string>--> <string name="withdraw_manual_ready_intro">You need to transfer %1$s from your regular bank account to the payment service to receive %2$s as electronic cash in this wallet.</string> <string name="withdraw_manual_ready_receiver">Recipient</string> <string name="withdraw_manual_ready_subject">Subject</string>