From da098a6bd5c7cdc4722a24ac28e0a87334341bbb Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Wed, 31 Jan 2024 13:16:19 -0600 Subject: [wallet] Replace exchange selection fragment with dialog decoupled from withdrawals --- .../exchanges/SelectExchangeDialogFragment.kt | 111 +++++++++++++++++++++ .../wallet/exchanges/SelectExchangeFragment.kt | 55 ---------- .../wallet/withdraw/PromptWithdrawFragment.kt | 64 ++++++++++-- .../net/taler/wallet/withdraw/WithdrawManager.kt | 26 ++--- wallet/src/main/res/navigation/nav_graph.xml | 9 -- 5 files changed, 174 insertions(+), 91 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeDialogFragment.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeDialogFragment.kt new file mode 100644 index 0000000..2da7618 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeDialogFragment.kt @@ -0,0 +1,111 @@ +/* + * This file is part of GNU Taler + * (C) 2024 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.wallet.exchanges + +import android.app.Dialog +import android.os.Bundle +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asFlow +import com.google.accompanist.themeadapter.material3.Mdc3Theme +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import net.taler.common.Event +import net.taler.common.toEvent +import net.taler.wallet.R +import net.taler.wallet.cleanExchange +import net.taler.wallet.compose.collectAsStateLifecycleAware + +class SelectExchangeDialogFragment: DialogFragment() { + private var exchangeList = MutableLiveData>() + + private var mExchangeSelection = MutableLiveData>() + val exchangeSelection: LiveData> = mExchangeSelection + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = ComposeView(requireContext()).apply { + setContent { + val exchanges = exchangeList.asFlow().collectAsStateLifecycleAware(initial = emptyList()) + SelectExchangeComposable(exchanges.value) { + onExchangeSelected(it) + } + } + } + + return MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3) + .setIcon(R.drawable.ic_account_balance) + .setTitle(R.string.exchange_list_add) + .setView(view) + .setNegativeButton(R.string.cancel) { _, _ -> + dismiss() + } + .create() + } + + fun setExchanges(exchanges: List) { + exchangeList.value = exchanges + } + + private fun onExchangeSelected(exchange: ExchangeItem) { + mExchangeSelection.value = exchange.toEvent() + dismiss() + } +} + +@Composable +fun SelectExchangeComposable( + exchanges: List, + onExchangeSelected: (exchange: ExchangeItem) -> Unit, +) { + Mdc3Theme { + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + items(exchanges) { + ExchangeItemComposable(it) { + onExchangeSelected(it) + } + } + } + } +} + +@Composable +fun ExchangeItemComposable(exchange: ExchangeItem, onSelected: () -> Unit) { + ListItem( + modifier = Modifier.clickable { onSelected() }, + headlineContent = { Text(cleanExchange(exchange.exchangeBaseUrl)) }, + supportingContent = exchange.currency?.let { + { Text(stringResource(R.string.exchange_list_currency, it)) } + }, + colors = ListItemDefaults.colors( + containerColor = Color.Transparent, + ) + ) +} \ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt deleted file mode 100644 index 732e0c5..0000000 --- a/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see - */ - -package net.taler.wallet.exchanges - -import androidx.navigation.fragment.findNavController -import net.taler.common.fadeOut -import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails - -class SelectExchangeFragment : ExchangeListFragment() { - - private val withdrawManager by lazy { model.withdrawManager } - - override val isSelectOnly = true - private val exchangeSelection by lazy { - requireNotNull(withdrawManager.exchangeSelection.value?.getEvenIfConsumedAlready()) - } - - override fun onExchangeUpdate(exchanges: List) { - ui.progressBar.fadeOut() - val status = withdrawManager.withdrawStatus.value - if (status is ReceivedDetails) { - super.onExchangeUpdate(status.possibleExchanges) - } else { - findNavController().navigateUp() - } - } - - override fun onExchangeSelected(item: ExchangeItem) { - val status = withdrawManager.withdrawStatus.value - val exchanges = (status as? ReceivedDetails)?.possibleExchanges ?: emptyList() - withdrawManager.getWithdrawalDetails( - exchangeBaseUrl = item.exchangeBaseUrl, - amount = exchangeSelection.amount, - showTosImmediately = true, - uri = exchangeSelection.talerWithdrawUri, - possibleExchanges = exchanges, - ) - findNavController().navigateUp() - } - -} diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt index caad7b6..16b956b 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt @@ -36,10 +36,13 @@ import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.cleanExchange import net.taler.wallet.databinding.FragmentPromptWithdrawBinding +import net.taler.wallet.exchanges.ExchangeItem +import net.taler.wallet.exchanges.SelectExchangeDialogFragment import net.taler.wallet.withdraw.WithdrawStatus.Loading import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails import net.taler.wallet.withdraw.WithdrawStatus.TosReviewRequired import net.taler.wallet.withdraw.WithdrawStatus.Withdrawing +import net.taler.wallet.withdraw.WithdrawStatus.NeedsExchange class PromptWithdrawFragment : Fragment() { @@ -47,6 +50,8 @@ class PromptWithdrawFragment : Fragment() { private val withdrawManager by lazy { model.withdrawManager } private val transactionManager by lazy { model.transactionManager } + private val selectExchangeDialog = SelectExchangeDialogFragment() + private lateinit var ui: FragmentPromptWithdrawBinding override fun onCreateView( @@ -64,22 +69,20 @@ class PromptWithdrawFragment : Fragment() { withdrawManager.withdrawStatus.observe(viewLifecycleOwner) { showWithdrawStatus(it) } - withdrawManager.exchangeSelection.observe(viewLifecycleOwner, EventObserver { - findNavController().navigate(R.id.action_promptWithdraw_to_selectExchangeFragment) + + selectExchangeDialog.exchangeSelection.observe(viewLifecycleOwner, EventObserver { + onExchangeSelected(it) }) } private fun showWithdrawStatus(status: WithdrawStatus?): Any = when (status) { null -> model.showProgressBar.value = false is Loading -> model.showProgressBar.value = true - is WithdrawStatus.NeedsExchange -> { + is NeedsExchange -> { model.showProgressBar.value = false - val exchangeSelection = status.exchangeSelection.getIfNotConsumed() - if (exchangeSelection == null) { // already consumed - findNavController().popBackStack() - } else { - withdrawManager.selectExchange(exchangeSelection) - } + if (selectExchangeDialog.dialog?.isShowing != true) { + selectExchange() + } else {} } is TosReviewRequired -> onTosReviewRequired(status) is ReceivedDetails -> onReceivedDetails(status) @@ -178,8 +181,7 @@ class PromptWithdrawFragment : Fragment() { if (uri != null) { // no Uri for manual withdrawals ui.selectExchangeButton.fadeIn() ui.selectExchangeButton.setOnClickListener { - val exchangeSelection = ExchangeSelection(amountRaw, uri) - withdrawManager.selectExchange(exchangeSelection) + selectExchange() } } @@ -195,4 +197,44 @@ class PromptWithdrawFragment : Fragment() { ui.withdrawCard.fadeIn() } + private fun selectExchange() { + val exchanges = when (val status = withdrawManager.withdrawStatus.value) { + is ReceivedDetails -> status.possibleExchanges + is NeedsExchange -> status.possibleExchanges + is TosReviewRequired -> status.possibleExchanges + else -> return + } + selectExchangeDialog.setExchanges(exchanges) + selectExchangeDialog.show(parentFragmentManager, "SELECT_EXCHANGE") + } + + private fun onExchangeSelected(exchange: ExchangeItem) { + val status = withdrawManager.withdrawStatus.value + val amount = when (status) { + is ReceivedDetails -> status.amountRaw + is NeedsExchange -> status.amount + is TosReviewRequired -> status.amountRaw + else -> return + } + val uri = when (status) { + is ReceivedDetails -> status.talerWithdrawUri + is NeedsExchange -> status.talerWithdrawUri + is TosReviewRequired -> status.talerWithdrawUri + else -> return + } + val exchanges = when (status) { + is ReceivedDetails -> status.possibleExchanges + is NeedsExchange -> status.possibleExchanges + is TosReviewRequired -> status.possibleExchanges + else -> return + } + + withdrawManager.getWithdrawalDetails( + exchangeBaseUrl = exchange.exchangeBaseUrl, + amount = amount, + showTosImmediately = false, + uri = uri, + possibleExchanges = exchanges, + ) + } } diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt index 34ae69f..6e8eafd 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt @@ -20,7 +20,6 @@ import android.net.Uri import android.util.Log import androidx.annotation.UiThread import androidx.annotation.WorkerThread -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -40,7 +39,11 @@ import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails sealed class WithdrawStatus { data class Loading(val talerWithdrawUri: String? = null) : WithdrawStatus() - data class NeedsExchange(val exchangeSelection: Event) : WithdrawStatus() + data class NeedsExchange( + val talerWithdrawUri: String, + val amount: Amount, + val possibleExchanges: List, + ) : WithdrawStatus() data class TosReviewRequired( val talerWithdrawUri: String? = null, @@ -146,11 +149,6 @@ data class AcceptManualWithdrawalResponse( val transactionId: String, ) -data class ExchangeSelection( - val amount: Amount, - val talerWithdrawUri: String, -) - class WithdrawManager( private val api: WalletBackendApi, private val scope: CoroutineScope, @@ -159,8 +157,6 @@ class WithdrawManager( val withdrawStatus = MutableLiveData() val testWithdrawalStatus = MutableLiveData() - private val _exchangeSelection = MutableLiveData>() - val exchangeSelection: LiveData> = _exchangeSelection var exchangeFees: ExchangeFees? = null private set @@ -173,11 +169,6 @@ class WithdrawManager( } } - @UiThread - fun selectExchange(selection: ExchangeSelection) { - _exchangeSelection.value = selection.toEvent() - } - fun getWithdrawalDetails(uri: String) = scope.launch { withdrawStatus.value = WithdrawStatus.Loading(uri) api.request("getWithdrawalDetailsForUri", WithdrawalDetailsForUri.serializer()) { @@ -186,8 +177,11 @@ class WithdrawManager( handleError("getWithdrawalDetailsForUri", error) }.onSuccess { details -> if (details.defaultExchangeBaseUrl == null) { - val exchangeSelection = ExchangeSelection(details.amount, uri) - withdrawStatus.value = WithdrawStatus.NeedsExchange(exchangeSelection.toEvent()) + withdrawStatus.value = WithdrawStatus.NeedsExchange( + talerWithdrawUri = uri, + amount = details.amount, + possibleExchanges = details.possibleExchanges, + ) } else getWithdrawalDetails( exchangeBaseUrl = details.defaultExchangeBaseUrl, amount = details.amount, diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml index 8c1010c..24def7f 100644 --- a/wallet/src/main/res/navigation/nav_graph.xml +++ b/wallet/src/main/res/navigation/nav_graph.xml @@ -300,9 +300,6 @@ android:name="net.taler.wallet.withdraw.PromptWithdrawFragment" android:label="@string/nav_prompt_withdraw" tools:layout="@layout/fragment_prompt_withdraw"> - @@ -334,12 +331,6 @@ app:destination="@id/promptWithdraw" app:popUpTo="@id/nav_main" /> - -