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 }