taler-android

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

PromptWithdrawFragment.kt (11944B)


      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.withdraw
     18 
     19 import android.os.Bundle
     20 import android.view.LayoutInflater
     21 import android.view.View
     22 import android.view.ViewGroup
     23 import androidx.appcompat.app.AppCompatActivity
     24 import androidx.compose.runtime.LaunchedEffect
     25 import androidx.compose.runtime.getValue
     26 import androidx.compose.runtime.livedata.observeAsState
     27 import androidx.compose.runtime.remember
     28 import androidx.compose.ui.platform.ComposeView
     29 import androidx.core.os.bundleOf
     30 import androidx.fragment.app.Fragment
     31 import androidx.fragment.app.activityViewModels
     32 import androidx.lifecycle.Lifecycle
     33 import androidx.lifecycle.lifecycleScope
     34 import androidx.lifecycle.map
     35 import androidx.lifecycle.repeatOnLifecycle
     36 import androidx.navigation.fragment.findNavController
     37 import com.google.android.material.snackbar.Snackbar
     38 import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
     39 import kotlinx.coroutines.launch
     40 import net.taler.common.Amount
     41 import net.taler.common.EventObserver
     42 import net.taler.wallet.main.MainViewModel
     43 import net.taler.wallet.R
     44 import net.taler.wallet.main.ViewMode
     45 import net.taler.wallet.compose.AmountScope
     46 import net.taler.wallet.compose.LoadingScreen
     47 import net.taler.wallet.compose.TalerSurface
     48 import net.taler.wallet.compose.collectAsStateLifecycleAware
     49 import net.taler.wallet.exchanges.ExchangeItem
     50 import net.taler.wallet.exchanges.SelectExchangeDialogFragment
     51 import net.taler.wallet.withdraw.WithdrawStatus.Status.AlreadyConfirmed
     52 import net.taler.wallet.withdraw.WithdrawStatus.Status.Confirming
     53 import net.taler.wallet.withdraw.WithdrawStatus.Status.Error
     54 import net.taler.wallet.withdraw.WithdrawStatus.Status.InfoReceived
     55 import net.taler.wallet.withdraw.WithdrawStatus.Status.Loading
     56 import net.taler.wallet.withdraw.WithdrawStatus.Status.ManualTransferRequired
     57 import net.taler.wallet.withdraw.WithdrawStatus.Status.None
     58 import net.taler.wallet.withdraw.WithdrawStatus.Status.Success
     59 import net.taler.wallet.withdraw.WithdrawStatus.Status.TosReviewRequired
     60 import net.taler.wallet.withdraw.WithdrawStatus.Status.Updating
     61 
     62 class PromptWithdrawFragment: Fragment() {
     63     private val model: MainViewModel by activityViewModels()
     64     private val withdrawManager by lazy { model.withdrawManager }
     65     private val transactionManager by lazy { model.transactionManager }
     66     private val exchangeManager by lazy { model.exchangeManager }
     67     private val balanceManager by lazy { model.balanceManager }
     68 
     69     private val selectExchangeDialog = SelectExchangeDialogFragment()
     70 
     71     private var editableCurrency: Boolean = true
     72     private var navigating: Boolean = false
     73 
     74     override fun onCreateView(
     75         inflater: LayoutInflater,
     76         container: ViewGroup?,
     77         savedInstanceState: Bundle?
     78     ) = ComposeView(requireContext()).apply {
     79         val withdrawUri = arguments?.getString("withdrawUri")
     80         val withdrawExchangeUri = arguments?.getString("withdrawExchangeUri")
     81         val exchangeBaseUrl = arguments?.getString("exchangeBaseUrl")
     82         val amount = arguments?.getString("amount")?.let { Amount.fromJSONString(it) }
     83         editableCurrency = arguments?.getBoolean("editableCurrency") ?: true
     84 
     85         setContent {
     86             val status by withdrawManager.withdrawStatus.collectAsStateLifecycleAware()
     87             val viewMode by model.viewMode.collectAsStateLifecycleAware()
     88             val devMode by model.devMode.observeAsState()
     89             val selectedScope = remember (viewMode) {
     90                 (viewMode as? ViewMode.Transactions)?.selectedScope
     91             }
     92 
     93             val scopes by balanceManager.balances
     94                 .map { bl -> bl.map { it.scopeInfo } }
     95                 .map { bl ->
     96                     val scope = status.amountInfo?.scopeInfo
     97                     if (scope != null && !bl.contains(scope)) { bl + scope } else bl
     98                 }.observeAsState(emptyList())
     99 
    100             val initialAmount = status.selectedAmount
    101                 ?: selectedScope?.let { Amount.zero(it.currency) }
    102                 ?: scopes.firstOrNull()?.let { Amount.zero(it.currency) }
    103 
    104             val initialScope = status.selectedScope
    105                 ?: selectedScope
    106                 ?: scopes.firstOrNull()
    107 
    108             LaunchedEffect(status.status) {
    109                 if (status.status == None) {
    110                     if (withdrawUri != null) {
    111                         // get withdrawal details for taler://withdraw URI
    112                         withdrawManager.prepareBankIntegratedWithdrawal(withdrawUri, context, loading = true)
    113                     } else if (withdrawExchangeUri != null) {
    114                         // get withdrawal details for taler://withdraw-exchange URI
    115                         withdrawManager.prepareManualWithdrawal(withdrawExchangeUri)
    116                     } else if (exchangeBaseUrl != null) {
    117                         withdrawManager.getWithdrawalDetailsForExchange(exchangeBaseUrl, loading = true)
    118                     } else if (initialAmount != null) {
    119                         withdrawManager.getWithdrawalDetailsForAmount(
    120                             amount = initialAmount,
    121                             scopeInfo = initialScope,
    122                         )
    123                     }
    124                 }
    125             }
    126 
    127             LaunchedEffect(status.selectedSpec, amount) {
    128                 (requireActivity() as AppCompatActivity).apply {
    129                     supportActionBar?.title = status.selectedSpec?.symbol?.let { symbol ->
    130                         getString(R.string.nav_prompt_withdraw_currency, symbol)
    131                     } ?: amount?.currency?.let { currency ->
    132                         getString(R.string.nav_prompt_withdraw_currency, currency)
    133                     } ?: getString(R.string.nav_prompt_withdraw)
    134                 }
    135             }
    136 
    137             TalerSurface {
    138                 // FIXME: hack to prevent initialAmount from changing after preparing withdrawal.
    139                 //   WithdrawalShowInfo cannot detect changes other than null -> not null,
    140                 //   otherwise there would be an input loop.
    141                 if (status.selectedAmount == null && status.selectedScope == null) {
    142                     LoadingScreen()
    143                     return@TalerSurface
    144                 }
    145 
    146                 status.let { s ->
    147                     when (s.status) {
    148                         Confirming, AlreadyConfirmed -> LoadingScreen()
    149 
    150                         None, Loading, Error, InfoReceived, TosReviewRequired, Updating -> {
    151                             WithdrawalShowInfo(
    152                                 status = s,
    153                                 devMode = devMode ?: false,
    154                                 initialAmountScope = initialAmount?.let { amount ->
    155                                     initialScope?.let { scope ->
    156                                         AmountScope(amount, scope)
    157                                     }
    158                                 },
    159                                 editableScope = editableCurrency,
    160                                 scopes = scopes,
    161                                 onSelectExchange = { selectExchange() },
    162                                 onSelectAmount = { amount, scope ->
    163                                     withdrawManager.getWithdrawalDetailsForAmount(
    164                                         amount = amount,
    165                                         scopeInfo = scope,
    166                                         // only show loading screen when switching currencies
    167                                         loading = scope != status.selectedScope,
    168                                     )
    169                                 },
    170                                 onTosReview = {
    171                                     // TODO: rewrite ToS review screen in compose
    172                                     if (s.exchangeBaseUrl != null) {
    173                                         val args = bundleOf("exchangeBaseUrl" to s.exchangeBaseUrl)
    174                                         findNavController().navigate(R.id.action_global_reviewExchangeTOS, args)
    175                                     }
    176                                 },
    177                                 onConfirm = { age ->
    178                                     status.selectedScope?.let { model.selectScope(it) }
    179                                     withdrawManager.acceptWithdrawal(age)
    180                                 },
    181                             )
    182                         }
    183                         else -> {}
    184                     }
    185                 }
    186             }
    187         }
    188     }
    189 
    190     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    191         super.onViewCreated(view, savedInstanceState)
    192 
    193         lifecycleScope.launch {
    194             repeatOnLifecycle(Lifecycle.State.STARTED) {
    195                 withdrawManager.withdrawStatus.collect { status ->
    196                     if (status.exchangeBaseUrl == null
    197                         && selectExchangeDialog.dialog?.isShowing != true) {
    198                         selectExchange()
    199                     }
    200 
    201                     when (status.status) {
    202                         Success, ManualTransferRequired, AlreadyConfirmed -> lifecycleScope.launch {
    203                             Snackbar.make(
    204                                 requireView(),
    205                                 if (status.status == AlreadyConfirmed) {
    206                                     R.string.withdraw_error_already_confirmed
    207                                 } else {
    208                                     R.string.withdraw_initiated
    209                                 },
    210                                 LENGTH_LONG,
    211                             ).show()
    212 
    213                             status.transactionId?.let {
    214                                 if (!navigating) {
    215                                     navigating = true
    216                                 } else return@let
    217 
    218                                 if (transactionManager.selectTransaction(it)) {
    219                                     status.amountInfo?.scopeInfo?.let { s -> model.selectScope(s) }
    220                                     findNavController().navigate(R.id.action_global_transactionWithdrawal)
    221                                 } else {
    222                                     findNavController().navigate(R.id.action_global_main)
    223                                 }
    224                             }
    225                         }
    226 
    227                         else -> {}
    228                     }
    229                 }
    230             }
    231         }
    232 
    233         selectExchangeDialog.exchangeSelection.observe(viewLifecycleOwner, EventObserver {
    234             onExchangeSelected(it)
    235         })
    236 
    237         exchangeManager.exchanges.observe(viewLifecycleOwner) { exchanges ->
    238             // detect ToS acceptation
    239             withdrawManager.refreshTosStatus(exchanges)
    240         }
    241     }
    242 
    243     private fun selectExchange() {
    244         val exchanges = withdrawManager.withdrawStatus.value.uriInfo?.possibleExchanges ?: return
    245         selectExchangeDialog.setExchanges(exchanges)
    246         if (selectExchangeDialog.isAdded) {
    247             selectExchangeDialog.show(parentFragmentManager, "SELECT_EXCHANGE")
    248         }
    249     }
    250 
    251     private fun onExchangeSelected(exchange: ExchangeItem) {
    252         withdrawManager.getWithdrawalDetailsForExchange(
    253             exchangeBaseUrl = exchange.exchangeBaseUrl,
    254         )
    255     }
    256 }