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 }