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 }