taler-android

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

AccountsFragment.kt (14805B)


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