DepositAmountComposable.kt (11011B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2024 Taler Systems S.A. 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 package net.taler.wallet.deposit 18 19 import androidx.compose.animation.AnimatedVisibility 20 import androidx.compose.foundation.layout.Column 21 import androidx.compose.foundation.layout.fillMaxSize 22 import androidx.compose.foundation.layout.fillMaxWidth 23 import androidx.compose.foundation.layout.imePadding 24 import androidx.compose.foundation.layout.padding 25 import androidx.compose.foundation.rememberScrollState 26 import androidx.compose.foundation.verticalScroll 27 import androidx.compose.material3.Button 28 import androidx.compose.material3.HorizontalDivider 29 import androidx.compose.material3.Text 30 import androidx.compose.runtime.Composable 31 import androidx.compose.runtime.getValue 32 import androidx.compose.runtime.mutableStateOf 33 import androidx.compose.runtime.remember 34 import androidx.compose.runtime.setValue 35 import androidx.compose.ui.Alignment.Companion.CenterHorizontally 36 import androidx.compose.ui.Modifier 37 import androidx.compose.ui.platform.LocalFocusManager 38 import androidx.compose.ui.res.stringResource 39 import androidx.compose.ui.tooling.preview.Preview 40 import androidx.compose.ui.unit.dp 41 import net.taler.common.Amount 42 import net.taler.common.CurrencySpecification 43 import net.taler.wallet.BottomInsetsSpacer 44 import net.taler.wallet.R 45 import net.taler.wallet.accounts.BankAccountRow 46 import net.taler.wallet.accounts.KnownBankAccountInfo 47 import net.taler.wallet.backend.TalerErrorInfo 48 import net.taler.wallet.compose.AmountCurrencyField 49 import net.taler.wallet.compose.BottomButtonBox 50 import net.taler.wallet.compose.ErrorComposable 51 import net.taler.wallet.compose.TalerSurface 52 import net.taler.wallet.systemBarsPaddingBottom 53 import net.taler.wallet.transactions.AmountType.Negative 54 import net.taler.wallet.transactions.AmountType.Positive 55 import net.taler.wallet.transactions.TransactionAmountComposable 56 import net.taler.wallet.useDebounce 57 58 @Composable 59 fun DepositAmountComposable( 60 modifier: Modifier = Modifier, 61 state: DepositState.AccountSelected, 62 knownCurrencies: List<String>, 63 getCurrencySpec: (currency: String) -> CurrencySpecification?, 64 checkDeposit: suspend (amount: Amount) -> CheckDepositResult, 65 onMakeDeposit: (amount: Amount) -> Unit, 66 onClose: () -> Unit, 67 ) { 68 Column( 69 modifier 70 .fillMaxSize() 71 .imePadding(), 72 ) { 73 var checkResult by remember { mutableStateOf<CheckDepositResult>(CheckDepositResult.None) } 74 // TODO: handle unavailable scopes in UI (i.e. explain restrictions) 75 // if currencies is null, we assume any (known) currency is supported 76 val currencies = state.account.currencies?.distinct() ?: knownCurrencies 77 var amount by remember(currencies) { 78 mutableStateOf(currencies.firstOrNull()?.let { Amount.zero(it) }) } 79 80 Column( 81 modifier = Modifier 82 .weight(1f) 83 .verticalScroll(rememberScrollState()) 84 .fillMaxWidth(), 85 horizontalAlignment = CenterHorizontally, 86 ) { 87 88 BankAccountRow( 89 account = state.account, 90 showMenu = false, 91 ) 92 93 HorizontalDivider( 94 modifier = Modifier.padding(bottom = 16.dp), 95 ) 96 97 if (currencies.isEmpty() || amount == null) { 98 ErrorComposable( 99 error = TalerErrorInfo.makeCustomError( 100 stringResource(R.string.send_deposits_no_currencies_error)), 101 modifier = Modifier.fillMaxSize(), 102 devMode = false, 103 onClose = onClose, 104 ) 105 return 106 } 107 108 val spec = remember(amount) { getCurrencySpec(amount!!.currency) } 109 val maxDepositable = remember(amount) { state.maxDepositable[amount!!.currency] } 110 111 amount.useDebounce { 112 checkResult = checkDeposit(amount!!) 113 } 114 115 AnimatedVisibility(maxDepositable?.rawAmount != null) { 116 maxDepositable?.rawAmount?.let { 117 Text( 118 modifier = Modifier.padding( 119 start = 16.dp, 120 end = 16.dp, 121 bottom = 16.dp, 122 ), 123 text = if (maxDepositable.effectiveAmount == it) { 124 stringResource( 125 R.string.amount_available_transfer, 126 it.withSpec(spec), 127 ) 128 } else { 129 stringResource( 130 R.string.amount_available_transfer_fees, 131 it.withSpec(spec), 132 ) 133 }, 134 ) 135 } 136 } 137 138 AmountCurrencyField( 139 modifier = Modifier 140 .padding(horizontal = 16.dp) 141 .fillMaxWidth(), 142 amount = amount!!.withSpec(spec), 143 onAmountChanged = { amount = it }, 144 editableCurrency = true, 145 currencies = currencies, 146 isError = checkResult !is CheckDepositResult.Success, 147 label = { Text(stringResource(R.string.amount_deposit)) }, 148 supportingText = { 149 val res = checkResult 150 if (res is CheckDepositResult.InsufficientBalance) { 151 Text(stringResource(R.string.payment_balance_insufficient)) 152 } else if (res is CheckDepositResult.ExceedsLimit) { 153 Text(stringResource(R.string.amount_excess, 154 res.maxDepositAmountEffective)) 155 } 156 } 157 ) 158 159 AnimatedVisibility(visible = checkResult is CheckDepositResult.Success) { 160 val res = checkResult as? CheckDepositResult.Success ?: return@AnimatedVisibility 161 162 Column( 163 modifier = Modifier.fillMaxWidth(), 164 horizontalAlignment = CenterHorizontally, 165 ) { 166 val totalAmount = res.totalDepositCost 167 val effectiveAmount = res.effectiveDepositAmount 168 if (totalAmount > effectiveAmount) { 169 val fee = totalAmount - effectiveAmount 170 171 TransactionAmountComposable( 172 label = stringResource(R.string.amount_fee), 173 amount = fee.withSpec(amount?.spec), 174 amountType = Negative, 175 ) 176 177 TransactionAmountComposable( 178 label = stringResource(R.string.amount_send), 179 amount = effectiveAmount.withSpec(amount?.spec), 180 amountType = Positive, 181 ) 182 } 183 } 184 } 185 186 BottomInsetsSpacer() 187 } 188 189 BottomButtonBox(Modifier.fillMaxWidth()) { 190 val focusManager = LocalFocusManager.current 191 val amount = amount 192 Button( 193 modifier = Modifier 194 .systemBarsPaddingBottom(), 195 enabled = checkResult is CheckDepositResult.Success && amount != null, 196 onClick = { 197 focusManager.clearFocus() 198 onMakeDeposit(amount!!) 199 }, 200 ) { 201 Text(stringResource(R.string.send_deposit_create_button)) 202 } 203 } 204 } 205 } 206 207 @Preview 208 @Composable 209 fun DepositAmountComposablePreview() { 210 TalerSurface { 211 val state = DepositState.AccountSelected( 212 KnownBankAccountInfo( 213 bankAccountId = "acct:1234", 214 paytoUri = "payto://", 215 kycCompleted = false, 216 currencies = listOf("KUDOS", "TESTKUDOS"), 217 label = "Test accoul " 218 ), 219 maxDepositable = mapOf( 220 "CHF" to GetMaxDepositAmountResponse( 221 effectiveAmount = Amount.fromJSONString("CHF:100"), 222 rawAmount = Amount.fromJSONString("CHF:100"), 223 ), 224 "EUR" to GetMaxDepositAmountResponse( 225 effectiveAmount = Amount.fromJSONString("EUR:0"), 226 rawAmount = Amount.fromJSONString("EUR:0"), 227 ), 228 "MXN" to GetMaxDepositAmountResponse( 229 effectiveAmount = Amount.fromJSONString("MXN:1000"), 230 rawAmount = Amount.fromJSONString("MXN:1000"), 231 ), 232 "USD" to GetMaxDepositAmountResponse( 233 effectiveAmount = Amount.fromJSONString("USD:0"), 234 rawAmount = Amount.fromJSONString("USD:0"), 235 ), 236 ), 237 ) 238 DepositAmountComposable( 239 state = state, 240 knownCurrencies = listOf("CHF", "EUR", "MXN", "USD"), 241 checkDeposit = { CheckDepositResult.Success( 242 totalDepositCost = Amount.fromJSONString("KUDOS:10"), 243 effectiveDepositAmount = Amount.fromJSONString("KUDOS:12"), 244 ) }, 245 onMakeDeposit = {}, 246 getCurrencySpec = { null }, 247 onClose = {} 248 ) 249 } 250 } 251 252 @Preview 253 @Composable 254 fun DepositAmountComposableErrorPreview() { 255 TalerSurface { 256 val state = DepositState.AccountSelected( 257 KnownBankAccountInfo( 258 bankAccountId = "acct:1234", 259 paytoUri = "payto://", 260 kycCompleted = false, 261 currencies = listOf(), 262 label = "Test account" 263 ), 264 maxDepositable = mapOf(), 265 ) 266 DepositAmountComposable( 267 state = state, 268 knownCurrencies = listOf("CHF", "EUR", "MXN", "USD"), 269 checkDeposit = { CheckDepositResult.Success( 270 totalDepositCost = Amount.fromJSONString("KUDOS:10"), 271 effectiveDepositAmount = Amount.fromJSONString("KUDOS:12"), 272 ) }, 273 onMakeDeposit = {}, 274 getCurrencySpec = { null }, 275 onClose = {} 276 ) 277 } 278 }