diff options
author | Torsten Grote <t@grobox.de> | 2020-09-28 12:12:20 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-09-28 14:27:47 -0300 |
commit | 0936fc851232c8c6c41ce4a0c479ba7a1e452137 (patch) | |
tree | 8e69b1a58319a275901456056558b28643cb8063 | |
parent | 841d87d5f9f05bc4a5e050948af2577cd87efade (diff) | |
download | taler-android-0936fc851232c8c6c41ce4a0c479ba7a1e452137.tar.gz taler-android-0936fc851232c8c6c41ce4a0c479ba7a1e452137.tar.bz2 taler-android-0936fc851232c8c6c41ce4a0c479ba7a1e452137.zip |
[wallet] implement exchange selection for withdrawals
-rw-r--r-- | .idea/codeStyles/Project.xml | 1 | ||||
-rw-r--r-- | build.gradle | 2 | ||||
-rw-r--r-- | taler-kotlin-android/src/main/java/net/taler/common/Event.kt | 4 | ||||
-rw-r--r-- | wallet/build.gradle | 2 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt | 17 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt | 146 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt | 20 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt | 135 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt | 88 | ||||
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt | 52 | ||||
-rw-r--r-- | wallet/src/main/res/layout/fragment_exchange_fees.xml (renamed from wallet/src/main/res/layout/fragment_select_exchange.xml) | 0 | ||||
-rw-r--r-- | wallet/src/main/res/layout/fragment_prompt_withdraw.xml | 2 | ||||
-rw-r--r-- | wallet/src/main/res/navigation/nav_graph.xml | 4 | ||||
-rw-r--r-- | wallet/src/main/res/values/strings.xml | 1 |
14 files changed, 298 insertions, 176 deletions
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index b23c749..587f132 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -17,6 +17,7 @@ </option> <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" /> <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" /> + <option name="ALLOW_TRAILING_COMMA" value="true" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> </JetCodeStyleSettings> <codeStyleSettings language="XML"> diff --git a/build.gradle b/build.gradle index 8973530..7f12ed4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.4.0' + ext.kotlin_version = '1.4.10' ext.ktor_version = "1.4.0" ext.nav_version = "2.3.0" ext.material_version = "1.2.1" diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Event.kt b/taler-kotlin-android/src/main/java/net/taler/common/Event.kt index 779247f..752e20e 100644 --- a/taler-kotlin-android/src/main/java/net/taler/common/Event.kt +++ b/taler-kotlin-android/src/main/java/net/taler/common/Event.kt @@ -34,6 +34,10 @@ open class Event<out T>(private val content: T) { return if (isConsumed.compareAndSet(false, true)) content else null } + fun getEvenIfConsumedAlready(): T { + return content + } + } fun <T> T.toEvent() = Event(this) diff --git a/wallet/build.gradle b/wallet/build.gradle index 02123ee..48e1749 100644 --- a/wallet/build.gradle +++ b/wallet/build.gradle @@ -139,7 +139,7 @@ dependencies { implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1' // Markdown rendering - final def markwon_version = '4.5.1' + final def markwon_version = '4.6.0' implementation "io.noties.markwon:core:$markwon_version" implementation "io.noties.markwon:ext-tables:$markwon_version" implementation "io.noties.markwon:recycler:$markwon_version" diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt index 17ac50f..e315632 100644 --- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt @@ -18,6 +18,8 @@ package net.taler.wallet.exchanges import android.view.LayoutInflater import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.ImageButton import android.widget.TextView @@ -39,11 +41,14 @@ data class ExchangeItem( } interface ExchangeClickListener { + fun onExchangeSelected(item: ExchangeItem) fun onManualWithdraw(item: ExchangeItem) } -internal class ExchangeAdapter(private val listener: ExchangeClickListener) : - Adapter<ExchangeItemViewHolder>() { +internal class ExchangeAdapter( + private val selectOnly: Boolean, + private val listener: ExchangeClickListener, +) : Adapter<ExchangeItemViewHolder>() { private val items = ArrayList<ExchangeItem>() @@ -74,6 +79,14 @@ internal class ExchangeAdapter(private val listener: ExchangeClickListener) : fun bind(item: ExchangeItem) { urlView.text = item.name currencyView.text = context.getString(R.string.exchange_list_currency, item.currency) + if (selectOnly) { + itemView.setOnClickListener { listener.onExchangeSelected(item) } + overflowIcon.visibility = GONE + } else { + itemView.setOnClickListener(null) + itemView.isClickable = false + overflowIcon.visibility = VISIBLE + } overflowIcon.setOnClickListener { openMenu(overflowIcon, item) } } diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt new file mode 100644 index 0000000..c59fffe --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt @@ -0,0 +1,146 @@ +/* + * 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 <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.exchanges + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.ContextCompat.getColor +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import net.taler.common.toRelativeTime +import net.taler.common.toShortDate +import net.taler.lib.common.Amount +import net.taler.wallet.MainViewModel +import net.taler.wallet.R +import net.taler.wallet.databinding.FragmentExchangeFeesBinding +import net.taler.wallet.exchanges.CoinFeeAdapter.CoinFeeViewHolder +import net.taler.wallet.exchanges.WireFeeAdapter.WireFeeViewHolder + +class ExchangeFeesFragment : Fragment() { + + private val model: MainViewModel by activityViewModels() + private val withdrawManager by lazy { model.withdrawManager } + + private lateinit var ui: FragmentExchangeFeesBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + ui = FragmentExchangeFeesBinding.inflate(inflater, container, false) + return ui.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val fees = withdrawManager.exchangeFees ?: throw IllegalStateException() + if (fees.withdrawFee.isZero()) { + ui.withdrawFeeLabel.visibility = GONE + ui.withdrawFeeView.visibility = GONE + } else ui.withdrawFeeView.setAmount(fees.withdrawFee) + if (fees.overhead.isZero()) { + ui.overheadLabel.visibility = GONE + ui.overheadView.visibility = GONE + } else ui.overheadView.setAmount(fees.overhead) + ui.expirationView.text = fees.earliestDepositExpiration.ms.toRelativeTime(requireContext()) + ui.coinFeesList.adapter = CoinFeeAdapter(fees.coinFees) + ui.wireFeesList.adapter = WireFeeAdapter(fees.wireFees) + } + + private fun TextView.setAmount(amount: Amount) { + if (amount.isZero()) text = amount.toString() + else { + text = getString(R.string.amount_negative, amount) + setTextColor(getColor(context, R.color.red)) + } + } + +} + +private class CoinFeeAdapter(private val items: List<CoinFee>) : Adapter<CoinFeeViewHolder>() { + override fun getItemCount() = items.size + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoinFeeViewHolder { + val v = + LayoutInflater.from(parent.context).inflate(R.layout.list_item_coin_fee, parent, false) + return CoinFeeViewHolder(v) + } + + override fun onBindViewHolder(holder: CoinFeeViewHolder, position: Int) { + holder.bind(items[position]) + } + + private class CoinFeeViewHolder(private val v: View) : ViewHolder(v) { + private val res = v.context.resources + private val coinView: TextView = v.findViewById(R.id.coinView) + private val withdrawFeeView: TextView = v.findViewById(R.id.withdrawFeeView) + private val depositFeeView: TextView = v.findViewById(R.id.depositFeeView) + private val refreshFeeView: TextView = v.findViewById(R.id.refreshFeeView) + private val refundFeeView: TextView = v.findViewById(R.id.refundFeeView) + fun bind(item: CoinFee) { + coinView.text = res.getQuantityString( + R.plurals.exchange_fee_coin, + item.quantity, + item.coin, + item.quantity + ) + withdrawFeeView.text = + v.context.getString(R.string.exchange_fee_withdraw_fee, item.feeWithdraw) + depositFeeView.text = + v.context.getString(R.string.exchange_fee_deposit_fee, item.feeDeposit) + refreshFeeView.text = + v.context.getString(R.string.exchange_fee_refresh_fee, item.feeRefresh) + refundFeeView.text = + v.context.getString(R.string.exchange_fee_refund_fee, item.feeRefresh) + } + } +} + +private class WireFeeAdapter(private val items: List<WireFee>) : Adapter<WireFeeViewHolder>() { + override fun getItemCount() = items.size + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WireFeeViewHolder { + val v = + LayoutInflater.from(parent.context).inflate(R.layout.list_item_wire_fee, parent, false) + return WireFeeViewHolder(v) + } + + override fun onBindViewHolder(holder: WireFeeViewHolder, position: Int) { + holder.bind(items[position]) + } + + private class WireFeeViewHolder(private val v: View) : ViewHolder(v) { + private val validityView: TextView = v.findViewById(R.id.validityView) + private val wireFeeView: TextView = v.findViewById(R.id.wireFeeView) + private val closingFeeView: TextView = v.findViewById(R.id.closingFeeView) + fun bind(item: WireFee) { + validityView.text = v.context.getString( + R.string.exchange_fee_wire_fee_timespan, + item.start.ms.toShortDate(v.context), + item.end.ms.toShortDate(v.context) + ) + wireFeeView.text = + v.context.getString(R.string.exchange_fee_wire_fee_wire_fee, item.wireFee) + closingFeeView.text = + v.context.getString(R.string.exchange_fee_wire_fee_closing_fee, item.closingFee) + } + } +} diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt index 86b2519..9a96b59 100644 --- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt @@ -34,17 +34,19 @@ import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.databinding.FragmentExchangeListBinding -class ExchangeListFragment : Fragment(), ExchangeClickListener { +open class ExchangeListFragment : Fragment(), ExchangeClickListener { - private val model: MainViewModel by activityViewModels() + protected val model: MainViewModel by activityViewModels() private val exchangeManager by lazy { model.exchangeManager } - private lateinit var ui: FragmentExchangeListBinding - private val exchangeAdapter by lazy { ExchangeAdapter(this) } + protected lateinit var ui: FragmentExchangeListBinding + protected open val isSelectOnly = false + private val exchangeAdapter by lazy { ExchangeAdapter(isSelectOnly, this) } override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, ): View? { ui = FragmentExchangeListBinding.inflate(inflater, container, false) return ui.root @@ -70,7 +72,7 @@ class ExchangeListFragment : Fragment(), ExchangeClickListener { }) } - private fun onExchangeUpdate(exchanges: List<ExchangeItem>) { + protected open fun onExchangeUpdate(exchanges: List<ExchangeItem>) { exchangeAdapter.update(exchanges) if (exchanges.isEmpty()) { ui.emptyState.fadeIn() @@ -85,6 +87,10 @@ class ExchangeListFragment : Fragment(), ExchangeClickListener { Toast.makeText(requireContext(), R.string.exchange_add_error, LENGTH_LONG).show() } + override fun onExchangeSelected(item: ExchangeItem) { + throw AssertionError("must not get triggered here") + } + override fun onManualWithdraw(item: ExchangeItem) { exchangeManager.withdrawalExchange = item findNavController().navigate(R.id.action_nav_settings_exchanges_to_nav_exchange_manual_withdrawal) diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt index a95a51c..61e0db5 100644 --- a/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt @@ -16,130 +16,33 @@ package net.taler.wallet.exchanges -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.View.GONE -import android.view.ViewGroup -import android.widget.TextView -import androidx.core.content.ContextCompat.getColor -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.recyclerview.widget.RecyclerView.Adapter -import androidx.recyclerview.widget.RecyclerView.ViewHolder -import net.taler.common.toRelativeTime -import net.taler.common.toShortDate -import net.taler.lib.common.Amount -import net.taler.wallet.MainViewModel -import net.taler.wallet.R -import net.taler.wallet.databinding.FragmentSelectExchangeBinding -import net.taler.wallet.exchanges.CoinFeeAdapter.CoinFeeViewHolder -import net.taler.wallet.exchanges.WireFeeAdapter.WireFeeViewHolder +import androidx.navigation.fragment.findNavController +import net.taler.common.fadeOut -class SelectExchangeFragment : Fragment() { +class SelectExchangeFragment : ExchangeListFragment() { - private val model: MainViewModel by activityViewModels() private val withdrawManager by lazy { model.withdrawManager } - private lateinit var ui: FragmentSelectExchangeBinding - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - ui = FragmentSelectExchangeBinding.inflate(inflater, container, false) - return ui.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val fees = withdrawManager.exchangeFees ?: throw IllegalStateException() - if (fees.withdrawFee.isZero()) { - ui.withdrawFeeLabel.visibility = GONE - ui.withdrawFeeView.visibility = GONE - } else ui.withdrawFeeView.setAmount(fees.withdrawFee) - if (fees.overhead.isZero()) { - ui.overheadLabel.visibility = GONE - ui.overheadView.visibility = GONE - } else ui.overheadView.setAmount(fees.overhead) - ui.expirationView.text = fees.earliestDepositExpiration.ms.toRelativeTime(requireContext()) - ui.coinFeesList.adapter = CoinFeeAdapter(fees.coinFees) - ui.wireFeesList.adapter = WireFeeAdapter(fees.wireFees) - } - - private fun TextView.setAmount(amount: Amount) { - if (amount.isZero()) text = amount.toString() - else { - text = getString(R.string.amount_negative, amount) - setTextColor(getColor(context, R.color.red)) - } + override val isSelectOnly = true + private val exchangeSelection by lazy { + requireNotNull(withdrawManager.exchangeSelection.value?.getEvenIfConsumedAlready()) } -} - -private class CoinFeeAdapter(private val items: List<CoinFee>) : Adapter<CoinFeeViewHolder>() { - override fun getItemCount() = items.size - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoinFeeViewHolder { - val v = - LayoutInflater.from(parent.context).inflate(R.layout.list_item_coin_fee, parent, false) - return CoinFeeViewHolder(v) + override fun onExchangeUpdate(exchanges: List<ExchangeItem>) { + ui.progressBar.fadeOut() + super.onExchangeUpdate(exchanges.filter { exchangeItem -> + exchangeItem.currency == exchangeSelection.amount.currency + }) } - override fun onBindViewHolder(holder: CoinFeeViewHolder, position: Int) { - holder.bind(items[position]) + override fun onExchangeSelected(item: ExchangeItem) { + withdrawManager.getWithdrawalDetails( + exchangeBaseUrl = item.exchangeBaseUrl, + amount = exchangeSelection.amount, + showTosImmediately = true, + uri = exchangeSelection.talerWithdrawUri, + ) + findNavController().navigateUp() } - private class CoinFeeViewHolder(private val v: View) : ViewHolder(v) { - private val res = v.context.resources - private val coinView: TextView = v.findViewById(R.id.coinView) - private val withdrawFeeView: TextView = v.findViewById(R.id.withdrawFeeView) - private val depositFeeView: TextView = v.findViewById(R.id.depositFeeView) - private val refreshFeeView: TextView = v.findViewById(R.id.refreshFeeView) - private val refundFeeView: TextView = v.findViewById(R.id.refundFeeView) - fun bind(item: CoinFee) { - coinView.text = res.getQuantityString( - R.plurals.exchange_fee_coin, - item.quantity, - item.coin, - item.quantity - ) - withdrawFeeView.text = - v.context.getString(R.string.exchange_fee_withdraw_fee, item.feeWithdraw) - depositFeeView.text = - v.context.getString(R.string.exchange_fee_deposit_fee, item.feeDeposit) - refreshFeeView.text = - v.context.getString(R.string.exchange_fee_refresh_fee, item.feeRefresh) - refundFeeView.text = - v.context.getString(R.string.exchange_fee_refund_fee, item.feeRefresh) - } - } -} - -private class WireFeeAdapter(private val items: List<WireFee>) : Adapter<WireFeeViewHolder>() { - override fun getItemCount() = items.size - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WireFeeViewHolder { - val v = - LayoutInflater.from(parent.context).inflate(R.layout.list_item_wire_fee, parent, false) - return WireFeeViewHolder(v) - } - - override fun onBindViewHolder(holder: WireFeeViewHolder, position: Int) { - holder.bind(items[position]) - } - - private class WireFeeViewHolder(private val v: View) : ViewHolder(v) { - private val validityView: TextView = v.findViewById(R.id.validityView) - private val wireFeeView: TextView = v.findViewById(R.id.wireFeeView) - private val closingFeeView: TextView = v.findViewById(R.id.closingFeeView) - fun bind(item: WireFee) { - validityView.text = v.context.getString( - R.string.exchange_fee_wire_fee_timespan, - item.start.ms.toShortDate(v.context), - item.end.ms.toShortDate(v.context) - ) - wireFeeView.text = - v.context.getString(R.string.exchange_fee_wire_fee_wire_fee, item.wireFee) - closingFeeView.text = - v.context.getString(R.string.exchange_fee_wire_fee_closing_fee, item.closingFee) - } - } } 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 0c7687c..38e09fa 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt @@ -20,13 +20,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast -import android.widget.Toast.LENGTH_SHORT import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG +import net.taler.common.EventObserver import net.taler.common.fadeIn import net.taler.common.fadeOut import net.taler.lib.common.Amount @@ -35,6 +34,7 @@ import net.taler.wallet.R import net.taler.wallet.cleanExchange import net.taler.wallet.databinding.FragmentPromptWithdrawBinding 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 @@ -46,8 +46,9 @@ class PromptWithdrawFragment : Fragment() { private lateinit var ui: FragmentPromptWithdrawBinding override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, ): View? { ui = FragmentPromptWithdrawBinding.inflate(inflater, container, false) return ui.root @@ -59,21 +60,26 @@ class PromptWithdrawFragment : Fragment() { withdrawManager.withdrawStatus.observe(viewLifecycleOwner, { showWithdrawStatus(it) }) + withdrawManager.exchangeSelection.observe(viewLifecycleOwner, EventObserver { + findNavController().navigate(R.id.action_promptWithdraw_to_selectExchangeFragment) + }) } private fun showWithdrawStatus(status: WithdrawStatus?): Any = when (status) { - is WithdrawStatus.ReceivedDetails -> { - showContent(status.amountRaw, status.amountEffective, status.exchangeBaseUrl) - ui.confirmWithdrawButton.apply { - text = getString(R.string.withdraw_button_confirm) - setOnClickListener { - it.fadeOut() - ui.confirmProgressBar.fadeIn() - withdrawManager.acceptWithdrawal() - } - isEnabled = true + null -> model.showProgressBar.value = false + is Loading -> model.showProgressBar.value = true + is WithdrawStatus.NeedsExchange -> { + model.showProgressBar.value = false + val exchangeSelection = status.exchangeSelection.getIfNotConsumed() + if (exchangeSelection == null) { // already consumed + findNavController().popBackStack() + } else { + withdrawManager.selectExchange(exchangeSelection) } } + is TosReviewRequired -> onTosReviewRequired(status) + is ReceivedDetails -> onReceivedDetails(status) + is Withdrawing -> model.showProgressBar.value = true is WithdrawStatus.Success -> { model.showProgressBar.value = false withdrawManager.withdrawStatus.value = null @@ -81,14 +87,18 @@ class PromptWithdrawFragment : Fragment() { model.showTransactions(status.currency) Snackbar.make(requireView(), R.string.withdraw_initiated, LENGTH_LONG).show() } - is Loading -> { - model.showProgressBar.value = true - } - is Withdrawing -> { - model.showProgressBar.value = true + is WithdrawStatus.Error -> { + model.showProgressBar.value = false + findNavController().navigate(R.id.action_promptWithdraw_to_errorFragment) } - is TosReviewRequired -> { - showContent(status.amountRaw, status.amountEffective, status.exchangeBaseUrl) + } + + private fun onTosReviewRequired(s: TosReviewRequired) { + model.showProgressBar.value = false + if (s.showImmediately.getIfNotConsumed() == true) { + findNavController().navigate(R.id.action_promptWithdraw_to_reviewExchangeTOS) + } else { + showContent(s.amountRaw, s.amountEffective, s.exchangeBaseUrl, s.talerWithdrawUri) ui.confirmWithdrawButton.apply { text = getString(R.string.withdraw_button_tos) setOnClickListener { @@ -97,14 +107,27 @@ class PromptWithdrawFragment : Fragment() { isEnabled = true } } - is WithdrawStatus.Error -> { - model.showProgressBar.value = false - findNavController().navigate(R.id.action_promptWithdraw_to_errorFragment) + } + + private fun onReceivedDetails(s: ReceivedDetails) { + showContent(s.amountRaw, s.amountEffective, s.exchangeBaseUrl, s.talerWithdrawUri) + ui.confirmWithdrawButton.apply { + text = getString(R.string.withdraw_button_confirm) + setOnClickListener { + it.fadeOut() + ui.confirmProgressBar.fadeIn() + withdrawManager.acceptWithdrawal() + } + isEnabled = true } - null -> model.showProgressBar.value = false } - private fun showContent(amountRaw: Amount, amountEffective: Amount, exchange: String) { + private fun showContent( + amountRaw: Amount, + amountEffective: Amount, + exchange: String, + uri: String?, + ) { model.showProgressBar.value = false ui.progressBar.fadeOut() @@ -117,15 +140,20 @@ class PromptWithdrawFragment : Fragment() { ui.chosenAmountView.fadeIn() ui.feeLabel.fadeIn() - ui.feeView.text = getString(R.string.amount_negative, (amountRaw - amountEffective).toString()) + ui.feeView.text = + getString(R.string.amount_negative, (amountRaw - amountEffective).toString()) ui.feeView.fadeIn() ui.exchangeIntroView.fadeIn() ui.withdrawExchangeUrl.text = cleanExchange(exchange) ui.withdrawExchangeUrl.fadeIn() - ui.selectExchangeButton.fadeIn() - ui.selectExchangeButton.setOnClickListener { - Toast.makeText(context, "Not yet implemented", LENGTH_SHORT).show() + + if (uri != null) { // no Uri for manual withdrawals + ui.selectExchangeButton.fadeIn() + ui.selectExchangeButton.setOnClickListener { + val exchangeSelection = ExchangeSelection(amountRaw, uri) + withdrawManager.selectExchange(exchangeSelection) + } } ui.withdrawCard.fadeIn() 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 25c5b72..5e11c04 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt @@ -18,10 +18,13 @@ package net.taler.wallet.withdraw import android.util.Log import androidx.annotation.UiThread +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.serialization.Serializable +import net.taler.common.Event +import net.taler.common.toEvent import net.taler.lib.common.Amount import net.taler.wallet.TAG import net.taler.wallet.backend.TalerErrorInfo @@ -32,20 +35,23 @@ import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails sealed class WithdrawStatus { data class Loading(val talerWithdrawUri: String? = null) : WithdrawStatus() + data class NeedsExchange(val exchangeSelection: Event<ExchangeSelection>) : WithdrawStatus() + data class TosReviewRequired( val talerWithdrawUri: String? = null, val exchangeBaseUrl: String, val amountRaw: Amount, val amountEffective: Amount, val tosText: String, - val tosEtag: String + val tosEtag: String, + val showImmediately: Event<Boolean>, ) : WithdrawStatus() data class ReceivedDetails( val talerWithdrawUri: String? = null, val exchangeBaseUrl: String, val amountRaw: Amount, - val amountEffective: Amount + val amountEffective: Amount, ) : WithdrawStatus() object Withdrawing : WithdrawStatus() @@ -57,24 +63,31 @@ sealed class WithdrawStatus { data class WithdrawalDetailsForUri( val amount: Amount, val defaultExchangeBaseUrl: String?, - val possibleExchanges: List<ExchangeItem> + val possibleExchanges: List<ExchangeItem>, ) @Serializable data class WithdrawalDetails( val tosAccepted: Boolean, val amountRaw: Amount, - val amountEffective: Amount + val amountEffective: Amount, +) + +data class ExchangeSelection( + val amount: Amount, + val talerWithdrawUri: String, ) class WithdrawManager( private val api: WalletBackendApi, - private val scope: CoroutineScope + private val scope: CoroutineScope, ) { val withdrawStatus = MutableLiveData<WithdrawStatus>() val testWithdrawalInProgress = MutableLiveData(false) + private val _exchangeSelection = MutableLiveData<Event<ExchangeSelection>>() + val exchangeSelection: LiveData<Event<ExchangeSelection>> = _exchangeSelection var exchangeFees: ExchangeFees? = null private set @@ -87,6 +100,11 @@ 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()) { @@ -95,11 +113,10 @@ class WithdrawManager( handleError("getWithdrawalDetailsForUri", error) }.onSuccess { details -> if (details.defaultExchangeBaseUrl == null) { - // TODO go to exchange selection screen instead - val chosenExchange = details.possibleExchanges[0].exchangeBaseUrl - getWithdrawalDetails(chosenExchange, details.amount, uri) + val exchangeSelection = ExchangeSelection(details.amount, uri) + withdrawStatus.value = WithdrawStatus.NeedsExchange(exchangeSelection.toEvent()) } else { - getWithdrawalDetails(details.defaultExchangeBaseUrl, details.amount, uri) + getWithdrawalDetails(details.defaultExchangeBaseUrl, details.amount, false, uri) } } } @@ -107,7 +124,8 @@ class WithdrawManager( fun getWithdrawalDetails( exchangeBaseUrl: String, amount: Amount, - uri: String? = null + showTosImmediately: Boolean = false, + uri: String? = null, ) = scope.launch { withdrawStatus.value = WithdrawStatus.Loading(uri) api.request("getWithdrawalDetailsForAmount", WithdrawalDetails.serializer()) { @@ -121,16 +139,17 @@ class WithdrawManager( talerWithdrawUri = uri, exchangeBaseUrl = exchangeBaseUrl, amountRaw = details.amountRaw, - amountEffective = details.amountEffective + amountEffective = details.amountEffective, ) - } else getExchangeTos(exchangeBaseUrl, details, uri) + } else getExchangeTos(exchangeBaseUrl, details, showTosImmediately, uri) } } private fun getExchangeTos( exchangeBaseUrl: String, details: WithdrawalDetails, - uri: String? + showImmediately: Boolean, + uri: String?, ) = scope.launch { api.request("getExchangeTos", TosResponse.serializer()) { put("exchangeBaseUrl", exchangeBaseUrl) @@ -143,7 +162,8 @@ class WithdrawManager( amountRaw = details.amountRaw, amountEffective = details.amountEffective, tosText = it.tos, - tosEtag = it.currentEtag + tosEtag = it.currentEtag, + showImmediately = showImmediately.toEvent(), ) } } @@ -163,7 +183,7 @@ class WithdrawManager( talerWithdrawUri = s.talerWithdrawUri, exchangeBaseUrl = s.exchangeBaseUrl, amountRaw = s.amountRaw, - amountEffective = s.amountEffective + amountEffective = s.amountEffective, ) } } @@ -181,7 +201,7 @@ class WithdrawManager( api.request<Unit>(operation) { put("exchangeBaseUrl", status.exchangeBaseUrl) if (status.talerWithdrawUri == null) { - put("amount", status.amountRaw) + put("amount", status.amountRaw.toJSONString()) } else { put("talerWithdrawUri", status.talerWithdrawUri) } diff --git a/wallet/src/main/res/layout/fragment_select_exchange.xml b/wallet/src/main/res/layout/fragment_exchange_fees.xml index 6f8814f..6f8814f 100644 --- a/wallet/src/main/res/layout/fragment_select_exchange.xml +++ b/wallet/src/main/res/layout/fragment_exchange_fees.xml diff --git a/wallet/src/main/res/layout/fragment_prompt_withdraw.xml b/wallet/src/main/res/layout/fragment_prompt_withdraw.xml index 6bca4ef..421911a 100644 --- a/wallet/src/main/res/layout/fragment_prompt_withdraw.xml +++ b/wallet/src/main/res/layout/fragment_prompt_withdraw.xml @@ -169,7 +169,7 @@ android:contentDescription="@string/nav_exchange_fees" android:src="@drawable/ic_edit" android:tint="?attr/colorOnPrimary" - android:visibility="invisible" + android:visibility="gone" app:layout_constraintBottom_toBottomOf="@+id/withdrawExchangeUrl" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/withdrawExchangeUrl" diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml index d8ce5b2..cf98b95 100644 --- a/wallet/src/main/res/navigation/nav_graph.xml +++ b/wallet/src/main/res/navigation/nav_graph.xml @@ -152,8 +152,8 @@ <fragment android:id="@+id/selectExchangeFragment" android:name="net.taler.wallet.exchanges.SelectExchangeFragment" - android:label="@string/nav_exchange_fees" - tools:layout="@layout/fragment_select_exchange" /> + android:label="@string/nav_exchange_select" + tools:layout="@layout/fragment_exchange_list" /> <fragment android:id="@+id/nav_pending_operations" diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml index 24db2b0..2e32c88 100644 --- a/wallet/src/main/res/values/strings.xml +++ b/wallet/src/main/res/values/strings.xml @@ -39,6 +39,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="nav_prompt_withdraw">Withdraw Digital Cash</string> <string name="nav_exchange_tos">Exchange\'s Terms of Service</string> + <string name="nav_exchange_select">Select Exchange</string> <string name="nav_exchange_fees">Exchange Fees</string> <string name="nav_error">Error</string> |