taler-android

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

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 }