taler-android

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

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 }