taler-android

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

AddAccountComposable.kt (12921B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2023 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.accounts
     18 
     19 import androidx.compose.foundation.layout.Spacer
     20 import androidx.compose.foundation.layout.fillMaxSize
     21 import androidx.compose.foundation.layout.fillMaxWidth
     22 import androidx.compose.foundation.layout.imePadding
     23 import androidx.compose.foundation.layout.padding
     24 import androidx.compose.foundation.layout.size
     25 import androidx.compose.foundation.lazy.LazyColumn
     26 import androidx.compose.foundation.text.KeyboardOptions
     27 import androidx.compose.material.icons.Icons
     28 import androidx.compose.material.icons.filled.Add
     29 import androidx.compose.material.icons.filled.Edit
     30 import androidx.compose.material3.Button
     31 import androidx.compose.material3.ButtonDefaults
     32 import androidx.compose.material3.Icon
     33 import androidx.compose.material3.OutlinedTextField
     34 import androidx.compose.material3.ScrollableTabRow
     35 import androidx.compose.material3.Surface
     36 import androidx.compose.material3.Tab
     37 import androidx.compose.material3.Text
     38 import androidx.compose.runtime.Composable
     39 import androidx.compose.runtime.getValue
     40 import androidx.compose.runtime.mutableStateOf
     41 import androidx.compose.runtime.remember
     42 import androidx.compose.runtime.saveable.rememberSaveable
     43 import androidx.compose.runtime.setValue
     44 import androidx.compose.ui.Alignment.Companion.CenterHorizontally
     45 import androidx.compose.ui.Modifier
     46 import androidx.compose.ui.platform.LocalFocusManager
     47 import androidx.compose.ui.res.stringResource
     48 import androidx.compose.ui.text.input.ImeAction
     49 import androidx.compose.ui.tooling.preview.Preview
     50 import androidx.compose.ui.unit.dp
     51 import net.taler.wallet.BottomInsetsSpacer
     52 import net.taler.wallet.R
     53 import net.taler.wallet.backend.TalerErrorInfo
     54 import net.taler.wallet.compose.ErrorComposable
     55 import net.taler.wallet.compose.WarningLabel
     56 import net.taler.wallet.deposit.AddAccountBitcoin
     57 import net.taler.wallet.deposit.AddAccountIBAN
     58 import net.taler.wallet.deposit.AddAccountTaler
     59 import net.taler.wallet.deposit.GetDepositWireTypesResponse
     60 import net.taler.wallet.deposit.WireType
     61 import net.taler.wallet.deposit.WireTypeDetails
     62 import net.taler.wallet.deposit.getBitcoinPayto
     63 import net.taler.wallet.deposit.getIbanPayto
     64 import net.taler.wallet.deposit.getTalerPayto
     65 import net.taler.wallet.useDebounce
     66 
     67 @Composable
     68 fun AddAccountComposable(
     69     modifier: Modifier = Modifier,
     70     presetAccount: KnownBankAccountInfo? = null,
     71     depositWireTypes: GetDepositWireTypesResponse,
     72     validateIban: suspend (iban: String) -> Boolean,
     73     onSubmit: (paytoUri: String, label: String) -> Unit,
     74     onClose: () -> Unit,
     75 ) {
     76     val focusManager = LocalFocusManager.current
     77     val supportedWireTypes = remember(depositWireTypes) { depositWireTypes.wireTypes }
     78     val talerBankHostnames = remember(depositWireTypes) { depositWireTypes.hostNames }
     79     val presetPaytoUri = remember(presetAccount) { presetAccount?.let { PaytoUri.parse(it.paytoUri) } }
     80 
     81     if (supportedWireTypes.isEmpty()) {
     82         return ErrorComposable(
     83             error = TalerErrorInfo.makeCustomError(
     84                 stringResource(R.string.send_deposit_no_methods_error)),
     85             modifier = Modifier.fillMaxSize(),
     86             devMode = false,
     87             onClose = onClose,
     88         )
     89     }
     90 
     91     var selectedWireType by remember(supportedWireTypes, presetAccount) {
     92         if (presetAccount == null) {
     93             return@remember mutableStateOf(supportedWireTypes.firstOrNull())
     94         }
     95 
     96         val parsed = PaytoUri.parse(presetAccount.paytoUri)
     97         mutableStateOf(when (parsed) {
     98             is PaytoUriIban -> WireType.IBAN
     99             is PaytoUriTalerBank -> WireType.TalerBank
    100             is PaytoUriBitcoin -> WireType.Bitcoin
    101             else -> supportedWireTypes.firstOrNull()
    102         })
    103     }
    104 
    105     // payto:// stuff
    106     var formError by rememberSaveable { mutableStateOf(false) } // TODO: do an initial validation!
    107     var ibanError by rememberSaveable(presetPaytoUri) { mutableStateOf(presetPaytoUri == null) }
    108     var formAlias by rememberSaveable(presetAccount) { mutableStateOf(presetAccount?.label ?: "") }
    109     var ibanName by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriIban)?.receiverName ?: "") }
    110     var ibanTown by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriIban)?.receiverTown) }
    111     var ibanZip by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriIban)?.receiverPostalCode) }
    112     var ibanIban by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriIban)?.iban ?: "") }
    113     var talerName by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriTalerBank)?.receiverName ?: "") }
    114     var talerHost by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriTalerBank)?.host ?: talerBankHostnames.firstOrNull() ?: "") }
    115     var talerAccount by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriTalerBank)?.account ?: "") }
    116     var bitcoinAddress by rememberSaveable(presetPaytoUri) { mutableStateOf("") } // TODO: fill-in bitcoin address
    117 
    118     val paytoUri = when(selectedWireType) {
    119         WireType.IBAN -> getIbanPayto(
    120             ibanName.trim(),
    121             ibanZip?.trim(),
    122             ibanTown?.trim(),
    123             ibanIban.trim()
    124         )
    125         WireType.TalerBank -> getTalerPayto(talerName.trim(), talerHost.trim(), talerAccount.trim())
    126         WireType.Bitcoin -> getBitcoinPayto(bitcoinAddress.trim())
    127         else -> null
    128     }
    129 
    130     // form validation
    131     paytoUri.useDebounce {
    132         formError = when (selectedWireType) {
    133             WireType.IBAN -> {
    134                 val valid = validateIban(ibanIban)
    135                 ibanError = !valid || ibanIban.isBlank()
    136                 !valid || ibanName.isBlank()
    137             }
    138 
    139             WireType.TalerBank -> {
    140                 talerName.isBlank()
    141                         || talerHost.isBlank()
    142                         || talerAccount.isBlank()
    143             }
    144 
    145             WireType.Bitcoin -> {
    146                 bitcoinAddress.isBlank()
    147             }
    148 
    149             else -> true
    150         }
    151     }
    152 
    153     LazyColumn(
    154         modifier = modifier
    155             .fillMaxSize()
    156             .imePadding(),
    157         horizontalAlignment = CenterHorizontally,
    158     ) {
    159         // Do not show chooser when editing account
    160         if (presetAccount == null
    161             && selectedWireType != null
    162             && supportedWireTypes.size > 1) {
    163             item {
    164                 MakeDepositWireTypeChooser(
    165                     supportedWireTypes = supportedWireTypes,
    166                     selectedWireType = selectedWireType!!,
    167                     onSelectWireType = {
    168                         selectedWireType = it
    169                     }
    170                 )
    171             }
    172         }
    173 
    174         item {
    175             WarningLabel(
    176                 modifier = Modifier.padding(16.dp),
    177                 label = stringResource(R.string.send_deposit_account_warning),
    178             )
    179         }
    180 
    181         when(selectedWireType) {
    182             WireType.IBAN -> item {
    183                 AddAccountIBAN(
    184                     name = ibanName,
    185                     town = ibanTown,
    186                     zip = ibanZip,
    187                     iban = ibanIban,
    188                     ibanError = ibanError,
    189                     onFormEdited = { name, town, zip, iban ->
    190                         ibanName = name
    191                         ibanTown = town
    192                         ibanZip = zip
    193                         ibanIban = iban
    194                     }
    195                 )
    196             }
    197 
    198             WireType.TalerBank -> item {
    199                 AddAccountTaler(
    200                     name = talerName,
    201                     host = talerHost,
    202                     account = talerAccount,
    203                     supportedHosts = talerBankHostnames,
    204                     onFormEdited = { name, host, account ->
    205                         talerName = name
    206                         talerHost = host
    207                         talerAccount = account
    208                     }
    209                 )
    210             }
    211 
    212             WireType.Bitcoin -> item {
    213                 AddAccountBitcoin(
    214                     bitcoinAddress = bitcoinAddress,
    215                     onFormEdited = { address ->
    216                         bitcoinAddress = address
    217                     }
    218                 )
    219             }
    220 
    221             else -> {}
    222         }
    223 
    224         item {
    225             OutlinedTextField(
    226                 modifier = Modifier
    227                     .padding(horizontal = 16.dp)
    228                     .fillMaxWidth(),
    229                 value = formAlias,
    230                 onValueChange = {
    231                     formAlias = it
    232                 },
    233                 label = {
    234                     Text(stringResource(R.string.send_deposit_account_note))
    235                 },
    236                 singleLine = true,
    237                 isError = formAlias.isBlank(),
    238                 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
    239             )
    240         }
    241 
    242         item {
    243             Button(
    244                 modifier = Modifier.padding(16.dp),
    245                 enabled = !formError && formAlias.isNotBlank(),
    246                 onClick = {
    247                     focusManager.clearFocus()
    248                     if (paytoUri != null && formAlias.isNotEmpty()) {
    249                         onSubmit(paytoUri, formAlias)
    250                     }
    251                 },
    252             ) {
    253                 Icon(
    254                     if (presetAccount == null) {
    255                         Icons.Default.Add
    256                     } else {
    257                         Icons.Default.Edit
    258                     },
    259                     contentDescription = null,
    260                     modifier = Modifier.size(ButtonDefaults.IconSize),
    261                 )
    262 
    263                 Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    264 
    265                 if (presetAccount == null) {
    266                     Text(stringResource(R.string.send_deposit_account_add))
    267                 } else {
    268                     Text(stringResource(R.string.send_deposit_account_edit))
    269                 }
    270             }
    271         }
    272 
    273         item {
    274             BottomInsetsSpacer()
    275         }
    276     }
    277 }
    278 
    279 @Composable
    280 fun MakeDepositWireTypeChooser(
    281     modifier: Modifier = Modifier,
    282     supportedWireTypes: List<WireType>,
    283     selectedWireType: WireType,
    284     onSelectWireType: (wireType: WireType) -> Unit,
    285 ) {
    286     val selectedIndex = supportedWireTypes.indexOfFirst {
    287         it == selectedWireType
    288     }
    289 
    290     ScrollableTabRow(
    291         selectedTabIndex = selectedIndex,
    292         modifier = modifier,
    293         edgePadding = 8.dp,
    294     ) {
    295         supportedWireTypes.forEach { wireType ->
    296             if (wireType != WireType.Unknown) {
    297                 Tab(
    298                     selected = selectedWireType == wireType,
    299                     onClick = { onSelectWireType(wireType) },
    300                     text = {
    301                         Text(when(wireType) {
    302                             WireType.IBAN -> stringResource(R.string.send_deposit_iban)
    303                             WireType.TalerBank -> stringResource(R.string.send_deposit_taler)
    304                             WireType.Bitcoin -> stringResource(R.string.send_deposit_bitcoin)
    305                             else -> error("unknown method")
    306                         })
    307                     }
    308                 )
    309             }
    310         }
    311     }
    312 }
    313 
    314 @Preview
    315 @Composable
    316 fun PreviewAddAccountComposable() {
    317     Surface {
    318         AddAccountComposable(
    319             modifier = Modifier,
    320             depositWireTypes = GetDepositWireTypesResponse(
    321                 wireTypeDetails = listOf(
    322                     WireTypeDetails(
    323                         paymentTargetType = WireType.IBAN,
    324                         talerBankHostnames = listOf("bank.test.taler.net")
    325                     ),
    326                     WireTypeDetails(
    327                         paymentTargetType = WireType.TalerBank,
    328                         talerBankHostnames = listOf("bank.test.taler.net")
    329                     ),
    330                     WireTypeDetails(
    331                         paymentTargetType = WireType.Bitcoin,
    332                         talerBankHostnames = emptyList(),
    333                     )
    334                 ),
    335             ),
    336             validateIban = { true },
    337             onSubmit = { _, _ -> },
    338             onClose = {},
    339         )
    340     }
    341 }