MainFragment.kt (16078B)
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.main 18 19 import android.os.Bundle 20 import android.view.LayoutInflater 21 import android.view.View 22 import android.view.ViewGroup 23 import android.widget.Toast 24 import androidx.activity.compose.BackHandler 25 import androidx.appcompat.app.AppCompatActivity 26 import androidx.compose.foundation.layout.PaddingValues 27 import androidx.compose.foundation.layout.WindowInsets 28 import androidx.compose.foundation.layout.WindowInsetsSides 29 import androidx.compose.foundation.layout.only 30 import androidx.compose.foundation.layout.padding 31 import androidx.compose.foundation.layout.systemBars 32 import androidx.compose.material.icons.Icons 33 import androidx.compose.material.icons.filled.BarChart 34 import androidx.compose.material.icons.filled.Settings 35 import androidx.compose.material3.ExperimentalMaterial3Api 36 import androidx.compose.material3.Icon 37 import androidx.compose.material3.NavigationBar 38 import androidx.compose.material3.NavigationBarItem 39 import androidx.compose.material3.Scaffold 40 import androidx.compose.material3.Text 41 import androidx.compose.material3.rememberModalBottomSheetState 42 import androidx.compose.runtime.Composable 43 import androidx.compose.runtime.LaunchedEffect 44 import androidx.compose.runtime.getValue 45 import androidx.compose.runtime.livedata.observeAsState 46 import androidx.compose.runtime.mutableStateOf 47 import androidx.compose.runtime.remember 48 import androidx.compose.runtime.saveable.rememberSaveable 49 import androidx.compose.runtime.setValue 50 import androidx.compose.ui.Modifier 51 import androidx.compose.ui.platform.ComposeView 52 import androidx.compose.ui.platform.LocalContext 53 import androidx.compose.ui.res.stringResource 54 import androidx.core.os.bundleOf 55 import androidx.fragment.app.Fragment 56 import androidx.fragment.app.activityViewModels 57 import androidx.fragment.compose.AndroidFragment 58 import androidx.fragment.compose.FragmentState 59 import androidx.fragment.compose.rememberFragmentState 60 import androidx.navigation.fragment.findNavController 61 import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG 62 import com.google.android.material.snackbar.Snackbar 63 import kotlinx.coroutines.flow.first 64 import net.taler.wallet.R 65 import net.taler.wallet.balances.BalanceState 66 import net.taler.wallet.compose.TalerSurface 67 import net.taler.wallet.compose.collectAsStateLifecycleAware 68 import net.taler.wallet.launchInAppBrowser 69 import net.taler.wallet.settings.SettingsFragment 70 import net.taler.wallet.transactions.Transaction 71 import net.taler.wallet.transactions.TransactionMajorState 72 import net.taler.wallet.transactions.TransactionPayment 73 import net.taler.wallet.transactions.TransactionPeerPullDebit 74 import net.taler.wallet.transactions.TransactionPeerPushCredit 75 import net.taler.wallet.transactions.TransactionState 76 import net.taler.wallet.transactions.TransactionStateFilter.Nonfinal 77 78 class MainFragment: Fragment() { 79 80 enum class Tab { ASSETS, SETTINGS } 81 82 private val model: MainViewModel by activityViewModels() 83 84 @OptIn(ExperimentalMaterial3Api::class) 85 override fun onCreateView( 86 inflater: LayoutInflater, 87 container: ViewGroup?, 88 savedInstanceState: Bundle? 89 ): View = ComposeView(requireContext()).apply { 90 setContent { 91 TalerSurface { 92 var tab by rememberSaveable { mutableStateOf(Tab.ASSETS) } 93 var showSheet by remember { mutableStateOf(false) } 94 val sheetState = rememberModalBottomSheetState() 95 96 val settingsFragmentState = rememberFragmentState() 97 98 val context = LocalContext.current 99 val online by model.networkManager.networkStatus.observeAsState(false) 100 val networkStatus by model.networkManager.networkStatus.observeAsState(false) 101 val balanceState by model.balanceManager.state.observeAsState(BalanceState.None) 102 val viewMode by model.viewMode.collectAsStateLifecycleAware() 103 val devMode by model.devMode.observeAsState(false) 104 val txResult by remember(viewMode) { 105 val v = viewMode as? ViewMode.Transactions 106 model.transactionManager.transactionsFlow(v?.selectedScope, stateFilter = v?.stateFilter) 107 }.collectAsStateLifecycleAware() 108 val actionButtonUsed by remember { model.settingsManager.getActionButtonUsed(context) }.collectAsStateLifecycleAware(true) 109 110 Scaffold( 111 bottomBar = { 112 NavigationBar { 113 NavigationBarItem( 114 icon = { Icon(Icons.Default.BarChart, contentDescription = null) }, 115 label = { Text(stringResource(R.string.balances_title)) }, 116 selected = tab == Tab.ASSETS, 117 onClick = { 118 tab = Tab.ASSETS 119 if (viewMode !is ViewMode.Assets) 120 model.showAssets() 121 } 122 ) 123 124 TalerActionButton( 125 demandAttention = !actionButtonUsed, 126 onShowSheet = { 127 showSheet = true 128 }, 129 onScanQr = { 130 onScanQr() 131 }, 132 ) 133 134 NavigationBarItem( 135 icon = { Icon(Icons.Default.Settings, contentDescription = null) }, 136 label = { Text(stringResource(R.string.menu_settings)) }, 137 selected = tab == Tab.SETTINGS, 138 onClick = { tab = Tab.SETTINGS }, 139 ) 140 } 141 }, 142 contentWindowInsets = WindowInsets.systemBars.only( 143 WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom 144 ) 145 ) { innerPadding -> 146 LaunchedEffect(Unit) { 147 val viewMode = model.settingsManager.getViewMode(context).first() 148 model.setViewMode(viewMode) 149 } 150 151 LaunchedEffect(tab, viewMode) { 152 setTitle(tab, viewMode) 153 } 154 155 BackHandler(viewMode !is ViewMode.Assets) { 156 model.showAssets() 157 } 158 159 LaunchedEffect(tab, balanceState, viewMode) { 160 (requireActivity() as AppCompatActivity).apply { 161 if (tab == Tab.ASSETS && viewMode is ViewMode.Assets && balanceState.showWelcome()) { 162 supportActionBar?.hide() 163 } else { 164 supportActionBar?.show() 165 } 166 } 167 } 168 169 when (tab) { 170 Tab.ASSETS -> MainComposable( 171 innerPadding = innerPadding, 172 state = balanceState, 173 txResult = txResult, 174 viewMode = viewMode, 175 devMode = devMode, 176 networkStatus = networkStatus, 177 onWithdrawMoneyClicked = { 178 val args = bundleOf( 179 "uri" to "taler://withdraw-exchange/exchange.taler-ops.ch/", 180 "from" to "Withdraw CHF button", 181 ) 182 findNavController().navigate(R.id.action_global_handleUri, args) 183 }, 184 onGetDemoMoneyClicked = { 185 model.withdrawManager.withdrawTestBalance() 186 Snackbar.make( 187 requireView(), 188 getString(R.string.settings_test_withdrawal), 189 LENGTH_LONG 190 ).show() 191 }, 192 onBalanceClicked = { 193 model.showTransactions(it.scopeInfo) 194 }, 195 onPendingClicked = { 196 model.showTransactions(it.scopeInfo, Nonfinal) 197 }, 198 onTransactionClicked = { tx -> 199 onTransactionClicked(tx) 200 }, 201 onTransactionsDelete = { txIds -> 202 model.transactionManager.deleteTransactions(txIds) { error -> 203 Toast.makeText(context, error.userFacingMsg, Toast.LENGTH_LONG) 204 .show() 205 } 206 }, 207 onShowBalancesClicked = { 208 model.showAssets() 209 }, 210 onStatementClicked = { 211 findNavController().navigate( 212 R.id.action_main_to_donauStatement, 213 bundleOf("host" to it), 214 ) 215 } 216 ) 217 Tab.SETTINGS -> SettingsView( 218 innerPadding = innerPadding, 219 settingsFragmentState = settingsFragmentState, 220 ) 221 } 222 } 223 224 val disableActions = remember(balanceState, online) { 225 !online || (balanceState as? BalanceState.Success)?.balances?.isEmpty() ?: true 226 } 227 228 val selectedScope = (viewMode as? ViewMode.Transactions)?.selectedScope 229 230 val selectedBalance = remember(balanceState, selectedScope) { 231 val balances = (balanceState as? BalanceState.Success)?.balances 232 selectedScope?.let { 233 balances?.find { it.scopeInfo == selectedScope } 234 } 235 } 236 237 TalerActionsModal( 238 showSheet = showSheet, 239 sheetState = sheetState, 240 selectedCurrency = selectedBalance?.currency, 241 showShopping = selectedBalance?.shoppingUrls?.isNotEmpty() == true, 242 onDismiss = { showSheet = false }, 243 disableActions = disableActions, 244 disablePeer = selectedBalance?.disablePeerPayments == true, 245 onSend = this@MainFragment::onSend, 246 onReceive = this@MainFragment::onReceive, 247 onScanQr = this@MainFragment::onScanQr, 248 onDeposit = this@MainFragment::onDeposit, 249 onWithdraw = this@MainFragment::onWithdraw, 250 onEnterUri = this@MainFragment::onEnterUri, 251 onShoppingDiscovery = { 252 selectedBalance?.shoppingUrls?.let { 253 onShoppingDiscovery(it) 254 } 255 } 256 ) 257 } 258 } 259 } 260 261 private fun onTransactionClicked(tx: Transaction) { 262 val showTxDetails = { 263 if (tx.detailPageNav != 0) { 264 model.transactionManager.selectTransaction(tx) 265 findNavController().navigate(tx.detailPageNav) 266 } 267 } 268 269 when (tx.txState) { 270 // unfinished transactions (dialog) 271 TransactionState(TransactionMajorState.Dialog) -> when (tx) { 272 is TransactionPayment -> { 273 model.paymentManager.preparePay(tx.transactionId) {} 274 findNavController().navigate(R.id.action_global_promptPayment) 275 } 276 277 is TransactionPeerPushCredit -> { 278 model.peerManager.preparePeerPushCredit(transactionId = tx.transactionId) 279 findNavController().navigate(R.id.action_global_promptPushPayment) 280 } 281 282 is TransactionPeerPullDebit -> { 283 model.peerManager.preparePeerPullDebit(transactionId = tx.transactionId) 284 findNavController().navigate(R.id.action_global_promptPullPayment) 285 } 286 287 else -> showTxDetails() 288 } 289 290 else -> showTxDetails() 291 } 292 } 293 294 override fun onStart() { 295 super.onStart() 296 model.balanceManager.loadAssets(model.viewMode.value is ViewMode.Assets) 297 } 298 299 override fun onDestroyView() { 300 super.onDestroyView() 301 (requireActivity() as AppCompatActivity).apply { 302 supportActionBar?.show() 303 } 304 } 305 306 private fun setTitle(tab: Tab, viewMode: ViewMode?) { 307 (requireActivity() as AppCompatActivity).apply { 308 supportActionBar?.title = when (tab) { 309 Tab.ASSETS -> when(viewMode) { 310 is ViewMode.Assets -> getString(R.string.balances_title) 311 is ViewMode.Transactions -> getString(R.string.transactions_title) 312 null -> getString(R.string.loading) 313 } 314 315 Tab.SETTINGS -> getString(R.string.menu_settings) 316 } 317 } 318 } 319 320 private fun onSend() { 321 model.settingsManager.saveActionButtonUsed(requireContext()) 322 findNavController().navigate(R.id.action_global_outgoingPush) 323 } 324 325 private fun onReceive() { 326 model.settingsManager.saveActionButtonUsed(requireContext()) 327 findNavController().navigate(R.id.action_global_outgoingPull) 328 } 329 330 private fun onDeposit() { 331 model.settingsManager.saveActionButtonUsed(requireContext()) 332 findNavController().navigate(R.id.action_global_deposit) 333 } 334 335 private fun onWithdraw() { 336 model.settingsManager.saveActionButtonUsed(requireContext()) 337 model.withdrawManager.resetWithdrawal() 338 findNavController().navigate(R.id.action_global_promptWithdraw) 339 } 340 341 private fun onScanQr() { 342 model.settingsManager.saveActionButtonUsed(requireContext()) 343 model.scanCode() 344 } 345 346 private fun onEnterUri() { 347 model.settingsManager.saveActionButtonUsed(requireContext()) 348 findNavController().navigate(R.id.action_main_to_uriInput) 349 } 350 351 private fun onShoppingDiscovery(shoppingUrls: List<String>) { 352 model.settingsManager.saveActionButtonUsed(requireContext()) 353 if (shoppingUrls.size == 1) { 354 launchInAppBrowser(requireContext(), shoppingUrls[0]) 355 } else findNavController().navigate(R.id.action_main_to_exchangeShopping) 356 } 357 } 358 359 @Composable 360 fun SettingsView( 361 innerPadding: PaddingValues, 362 settingsFragmentState: FragmentState, 363 ) { 364 AndroidFragment( 365 SettingsFragment::class.java, 366 modifier = Modifier.padding(innerPadding), 367 fragmentState = settingsFragmentState, 368 ) 369 }