taler-android

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

BankAccountsScreen.kt (12960B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2026 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.Image
     20 import androidx.compose.foundation.clickable
     21 import androidx.compose.foundation.layout.fillMaxSize
     22 import androidx.compose.foundation.layout.navigationBarsPadding
     23 import androidx.compose.foundation.layout.padding
     24 import androidx.compose.foundation.lazy.LazyColumn
     25 import androidx.compose.foundation.lazy.items
     26 import androidx.compose.material.icons.Icons
     27 import androidx.compose.material.icons.filled.AccountBalance
     28 import androidx.compose.material.icons.filled.Add
     29 import androidx.compose.material.icons.filled.CurrencyBitcoin
     30 import androidx.compose.material.icons.filled.Delete
     31 import androidx.compose.material3.AlertDialog
     32 import androidx.compose.material3.ExperimentalMaterial3Api
     33 import androidx.compose.material3.FloatingActionButton
     34 import androidx.compose.material3.Icon
     35 import androidx.compose.material3.IconButton
     36 import androidx.compose.material3.ListItem
     37 import androidx.compose.material3.MaterialTheme
     38 import androidx.compose.material3.PlainTooltip
     39 import androidx.compose.material3.Text
     40 import androidx.compose.material3.TextButton
     41 import androidx.compose.material3.TooltipBox
     42 import androidx.compose.material3.TooltipDefaults
     43 import androidx.compose.material3.rememberTooltipState
     44 import androidx.compose.runtime.Composable
     45 import androidx.compose.runtime.LaunchedEffect
     46 import androidx.compose.runtime.collectAsState
     47 import androidx.compose.runtime.getValue
     48 import androidx.compose.runtime.livedata.observeAsState
     49 import androidx.compose.runtime.mutableStateOf
     50 import androidx.compose.runtime.remember
     51 import androidx.compose.runtime.setValue
     52 import androidx.compose.ui.Modifier
     53 import androidx.compose.ui.graphics.ColorFilter
     54 import androidx.compose.ui.res.painterResource
     55 import androidx.compose.ui.res.stringResource
     56 import androidx.compose.ui.text.font.FontWeight
     57 import androidx.compose.ui.tooling.preview.Preview
     58 import net.taler.wallet.NavigateCallback
     59 import net.taler.wallet.R
     60 import net.taler.wallet.WalletDestination
     61 import net.taler.wallet.accounts.ListBankAccountsResult.Error
     62 import net.taler.wallet.accounts.ListBankAccountsResult.None
     63 import net.taler.wallet.accounts.ListBankAccountsResult.Success
     64 import net.taler.wallet.compose.Avatar
     65 import net.taler.wallet.compose.EmptyComposable
     66 import net.taler.wallet.compose.ErrorComposable
     67 import net.taler.wallet.compose.GlobalScaffold
     68 import net.taler.wallet.compose.LoadingScreen
     69 import net.taler.wallet.compose.TalerSurface
     70 import net.taler.wallet.main.MainViewModel
     71 
     72 @OptIn(ExperimentalMaterial3Api::class)
     73 @Composable
     74 fun BankAccountsScreen(
     75     model: MainViewModel,
     76     currency: String?,
     77     onNavigate: NavigateCallback,
     78     onNavigateBack: () -> Unit,
     79 ) {
     80     val accounts by model.accountManager.bankAccounts.collectAsState()
     81     val devMode by model.devMode.observeAsState(false)
     82 
     83     LaunchedEffect(currency) {
     84         model.accountManager.listBankAccounts(currency)
     85     }
     86 
     87     TalerSurface {
     88         GlobalScaffold(
     89             model = model,
     90             modifier = Modifier.fillMaxSize(),
     91             title = { Text(stringResource(R.string.settings_bank_accounts)) },
     92             onNavigateBack = onNavigateBack,
     93             floatingActionButton = {
     94                 val tooltipState = rememberTooltipState()
     95                 TooltipBox(
     96                     positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
     97                     tooltip = { PlainTooltip { Text(stringResource(R.string.send_deposit_account_add)) } },
     98                     state = tooltipState,
     99                 ) {
    100                     FloatingActionButton(
    101                         modifier = Modifier.navigationBarsPadding(),
    102                         onClick = { onNavigate(WalletDestination.AddBankAccount(), false) },
    103                     ) {
    104                         Icon(Icons.Default.Add, contentDescription = null)
    105                     }
    106                 }
    107             },
    108         ) { paddingValues ->
    109             when (val acc = accounts) {
    110                 is None -> LoadingScreen(
    111                     modifier = Modifier
    112                         .fillMaxSize()
    113                         .padding(paddingValues)
    114                 )
    115                 is Success -> BankAccountsList(
    116                     modifier = Modifier.padding(paddingValues),
    117                     accounts = acc.accounts,
    118                     onEdit = { account ->
    119                         onNavigate(WalletDestination.AddBankAccount(account.bankAccountId), false)
    120                     },
    121                     onForget = { account ->
    122                         model.accountManager.forgetBankAccount(account.bankAccountId) {
    123                             // TODO: show error
    124                         }
    125                     },
    126                 )
    127                 is Error -> ErrorComposable(
    128                     acc.error,
    129                     devMode = devMode,
    130                     modifier = Modifier
    131                         .fillMaxSize()
    132                         .padding(paddingValues)
    133                 )
    134             }
    135         }
    136     }
    137 }
    138 
    139 @Composable
    140 fun BankAccountsList(
    141     accounts: List<KnownBankAccountInfo>,
    142     onEdit: (account: KnownBankAccountInfo) -> Unit,
    143     onForget: (account: KnownBankAccountInfo) -> Unit,
    144     modifier: Modifier = Modifier,
    145 ) {
    146     var showDeleteDialog by remember { mutableStateOf(false) }
    147     var accountToDelete by remember { mutableStateOf<KnownBankAccountInfo?>(null) }
    148 
    149     if (showDeleteDialog) AlertDialog(
    150         title = { Text(stringResource(R.string.send_deposit_account_forget_dialog_title)) },
    151         text = { Text(stringResource(R.string.send_deposit_account_forget_dialog_message)) },
    152         onDismissRequest = { showDeleteDialog = false },
    153         confirmButton = {
    154             TextButton(onClick = {
    155                 accountToDelete?.let(onForget)
    156                 accountToDelete = null
    157                 showDeleteDialog = false
    158             }) {
    159                 Text(stringResource(R.string.transactions_delete))
    160             }
    161         },
    162         dismissButton = {
    163             TextButton(onClick = {
    164                 showDeleteDialog = false
    165             }) {
    166                 Text(stringResource(R.string.cancel))
    167             }
    168         },
    169     )
    170 
    171     if (accounts.isEmpty()) {
    172         EmptyComposable(
    173             modifier = modifier,
    174             message = stringResource(R.string.send_deposit_known_bank_accounts_empty)
    175         )
    176         return
    177     }
    178 
    179     LazyColumn(
    180         modifier = modifier
    181     ) {
    182         items(accounts, key = { it.paytoUri }) { account ->
    183             BankAccountRow(account,
    184                 onForget = {
    185                     accountToDelete = account
    186                     showDeleteDialog = true
    187                 },
    188                 onClick = { onEdit(account) },
    189             )
    190 
    191         }
    192     }
    193 }
    194 
    195 @Composable
    196 fun BankAccountRow(
    197     account: KnownBankAccountInfo,
    198     showMenu: Boolean = true,
    199     onClick: (() -> Unit)? = null,
    200     onForget: (() -> Unit)? = null,
    201 ) {
    202     val paytoUri = remember(account.paytoUri) {
    203         PaytoUri.parse(account.paytoUri)
    204     }
    205 
    206     ListItem(
    207         modifier = Modifier.then(
    208             onClick?.let {
    209                 Modifier.clickable { onClick() }
    210             } ?: Modifier
    211         ),
    212         leadingContent = {
    213             Avatar {
    214                 when(paytoUri) {
    215                     is PaytoUriTalerBank -> Image(
    216                         painterResource(R.drawable.ic_actions),
    217                         contentDescription = null,
    218                         colorFilter = ColorFilter.tint(
    219                             color = MaterialTheme.colorScheme.onSecondaryContainer,
    220                         )
    221                     )
    222 
    223                     is PaytoUriBitcoin -> Icon(
    224                         Icons.Default.CurrencyBitcoin,
    225                         contentDescription = null,
    226                         tint = MaterialTheme.colorScheme.onSecondaryContainer,
    227                     )
    228 
    229                     is PaytoUriCyclos -> Text("Cy",
    230                         style = MaterialTheme.typography.labelLarge,
    231                         fontWeight = FontWeight.Bold,
    232                         color = MaterialTheme.colorScheme.onSecondaryContainer,
    233                     )
    234 
    235                     else -> Icon(
    236                         Icons.Default.AccountBalance,
    237                         contentDescription = null,
    238                         tint = MaterialTheme.colorScheme.onSecondaryContainer,
    239                     )
    240                 }
    241             }
    242         },
    243         overlineContent = {
    244             when(paytoUri) {
    245                 is PaytoUriIban -> Text(stringResource(R.string.send_deposit_iban))
    246                 is PaytoUriTalerBank -> Text(stringResource(R.string.send_deposit_taler))
    247                 is PaytoUriCyclos -> Text(stringResource(R.string.send_deposit_cyclos))
    248                 is PaytoUriBitcoin -> Text(stringResource(R.string.send_deposit_bitcoin))
    249                 else -> {}
    250             }
    251         },
    252         headlineContent = {
    253             Text(account.label
    254                 ?: stringResource(R.string.send_deposit_no_alias))
    255         },
    256         supportingContent = {
    257             when(paytoUri) {
    258                 is PaytoUriIban -> Text(paytoUri.iban)
    259                 is PaytoUriTalerBank -> Text(paytoUri.account)
    260                 is PaytoUriCyclos -> Text(paytoUri.receiverName)
    261                 is PaytoUriBitcoin -> {
    262                     Text(remember(paytoUri.segwitAddresses) {
    263                         paytoUri.segwitAddresses.joinToString(" ")
    264                     })
    265                 }
    266                 else -> {}
    267             }
    268         },
    269         trailingContent = {
    270             // TODO: turn into dropdown menu if more options are added
    271             if (showMenu) IconButton(onClick = { onForget?.let { it() } }) {
    272                 Icon(
    273                     Icons.Default.Delete,
    274                     contentDescription = stringResource(R.string.send_deposit_known_bank_account_delete),
    275                 )
    276             }
    277         }
    278     )
    279 }
    280 
    281 val previewKnownAccounts = listOf(
    282     KnownBankAccountInfo(
    283         bankAccountId = "acct:EHHRQMZNDNAW3KZMBW0ATTNHCT3WH3TNX3HNMS4MKGK10E1W0YNG",
    284         paytoUri = PaytoUriIban(
    285             iban = "DE7489694250801",
    286             targetPath = "",
    287             params = emptyMap(),
    288             receiverName = "John Doe",
    289             receiverPostalCode = "1234",
    290             receiverTown = "Texas",
    291         ).paytoUri,
    292         kycCompleted = true,
    293         currencies = listOf("KUDOS"),
    294         label = "GLS",
    295     ),
    296 
    297     KnownBankAccountInfo(
    298         bankAccountId = "acct:EHHRQMZNDNAW3KZMBW0ATTNHCT3WH3TNX3HNMS4MKGK10E1W0YNG",
    299         paytoUri = PaytoUriTalerBank(
    300             host = "bank.test.taler.net",
    301             account = "john123",
    302             targetPath = "",
    303             params = emptyMap(),
    304             receiverName = "John Doe",
    305         ).paytoUri,
    306         kycCompleted = true,
    307         currencies = listOf("TESTKUDOS"),
    308         label = "Main on test",
    309     ),
    310 
    311     KnownBankAccountInfo(
    312         bankAccountId = "acct:EHHRQMZNDNAW3KZMBW0ATTNHCT3WH3TNX3HNMS4MKGK10E1W0YNG",
    313         paytoUri = PaytoUriCyclos(
    314             host = "demo.cyclos.org",
    315             account = "john123",
    316             targetPath = "",
    317             params = emptyMap(),
    318             receiverName = "John Doe",
    319         ).paytoUri,
    320         kycCompleted = true,
    321         currencies = listOf("UI"),
    322         label = "Cyclos demo",
    323     ),
    324 
    325     KnownBankAccountInfo(
    326         bankAccountId = "acct:EHHRQMZNDNAW3KZMBW0ATTNHCT3WH3TNX3HNMS4MKGK10E1W0YNG",
    327         paytoUri = PaytoUriBitcoin(
    328             segwitAddresses = listOf("bc1qkrnmwd8t4yxzpha8gk3w8h8lyecfp2ra9yvgf9"),
    329             targetPath = "",
    330             params = emptyMap(),
    331             receiverName = "John Doe",
    332         ).paytoUri,
    333         kycCompleted = true,
    334         currencies = listOf("BTC"),
    335         label = "Android wallet",
    336     ),
    337 )
    338 
    339 @Preview
    340 @Composable
    341 fun KnownAccountsListPreview() {
    342     TalerSurface {
    343         BankAccountsList(
    344             accounts = previewKnownAccounts,
    345             onEdit = {},
    346             onForget = {},
    347         )
    348     }
    349 }
    350 
    351 @Preview
    352 @Composable
    353 fun KnownAccountsListEmptyPreview() {
    354     TalerSurface {
    355         BankAccountsList(
    356             accounts = listOf(),
    357             onEdit = {},
    358             onForget = {},
    359         )
    360     }
    361 }