From 874b52c6d5c0d8043f3250e2b80f5091c159ded1 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 8 Apr 2020 14:21:11 -0300 Subject: [wallet] add option to see exchange's fee structure --- .../main/java/net/taler/wallet/WalletViewModel.kt | 7 +- .../java/net/taler/wallet/withdraw/ExchangeFees.kt | 99 +++++++++++++++ .../wallet/withdraw/PromptWithdrawFragment.kt | 13 +- .../wallet/withdraw/SelectExchangeFragment.kt | 136 +++++++++++++++++++++ .../net/taler/wallet/withdraw/WithdrawManager.kt | 42 ++----- .../main/res/layout/fragment_prompt_withdraw.xml | 28 ++++- .../main/res/layout/fragment_select_exchange.xml | 135 ++++++++++++++++++++ wallet/src/main/res/layout/list_item_coin_fee.xml | 78 ++++++++++++ wallet/src/main/res/layout/list_item_wire_fee.xml | 57 +++++++++ wallet/src/main/res/navigation/nav_graph.xml | 8 ++ wallet/src/main/res/values/strings.xml | 18 +++ 11 files changed, 579 insertions(+), 42 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/withdraw/ExchangeFees.kt create mode 100644 wallet/src/main/java/net/taler/wallet/withdraw/SelectExchangeFragment.kt create mode 100644 wallet/src/main/res/layout/fragment_select_exchange.xml create mode 100644 wallet/src/main/res/layout/list_item_coin_fee.xml create mode 100644 wallet/src/main/res/layout/list_item_wire_fee.xml (limited to 'wallet') diff --git a/wallet/src/main/java/net/taler/wallet/WalletViewModel.kt b/wallet/src/main/java/net/taler/wallet/WalletViewModel.kt index c16b6fc..607ce15 100644 --- a/wallet/src/main/java/net/taler/wallet/WalletViewModel.kt +++ b/wallet/src/main/java/net/taler/wallet/WalletViewModel.kt @@ -48,9 +48,12 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { val showProgressBar = MutableLiveData() private val walletBackendApi = WalletBackendApi(app, { - loadBalances() + // nothing to do when we connect, balance will be requested by BalanceFragment in onStart() }) { payload -> - if (payload.getString("type") != "waiting-for-retry") { + if ( + payload.getString("type") != "waiting-for-retry" && // ignore ping + payload.optString("operation") != "init" // ignore init notification + ) { Log.i(TAG, "Received notification from wallet-core: ${payload.toString(2)}") loadBalances() } diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ExchangeFees.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ExchangeFees.kt new file mode 100644 index 0000000..4494e38 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/withdraw/ExchangeFees.kt @@ -0,0 +1,99 @@ +/* + * 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.withdraw + +import net.taler.common.Amount +import net.taler.common.Timestamp +import org.json.JSONObject + +data class CoinFee( + val coin: Amount, + val feeDeposit: Amount, + val feeRefresh: Amount, + val feeRefund: Amount, + val feeWithdraw: Amount +) + +data class CoinFees( + val quantity: Int, + val coinFee: CoinFee +) + +data class WireFee( + val start: Timestamp, + val end: Timestamp, + val wireFee: Amount, + val closingFee: Amount +) + +data class ExchangeFees( + val withdrawFee: Amount, + val overhead: Amount, + val earliestDepositExpiration: Timestamp, + val coinFees: List, + val wireFees: List +) { + companion object { + fun fromExchangeWithdrawDetailsJson(json: JSONObject): ExchangeFees { + val earliestDepositExpiration = + json.getJSONObject("earliestDepositExpiration").getLong("t_ms") + + val selectedDenoms = json.getJSONArray("selectedDenoms") + val coinFees = HashMap(selectedDenoms.length()) + for (i in 0 until selectedDenoms.length()) { + val denom = selectedDenoms.getJSONObject(i) + val coinFee = CoinFee( + coin = Amount.fromJsonObject(denom.getJSONObject("value")), + feeDeposit = Amount.fromJsonObject(denom.getJSONObject("feeDeposit")), + feeRefresh = Amount.fromJsonObject(denom.getJSONObject("feeRefresh")), + feeRefund = Amount.fromJsonObject(denom.getJSONObject("feeRefund")), + feeWithdraw = Amount.fromJsonObject(denom.getJSONObject("feeWithdraw")) + ) + coinFees[coinFee] = (coinFees[coinFee] ?: 0) + 1 + } + + val wireFeesJson = json.getJSONObject("wireFees") + val feesForType = wireFeesJson.getJSONObject("feesForType") + val bankFees = feesForType.getJSONArray("x-taler-bank") + val wireFees = ArrayList(bankFees.length()) + for (i in 0 until bankFees.length()) { + val fee = bankFees.getJSONObject(i) + val startStamp = + fee.getJSONObject("startStamp").getLong("t_ms") + val endStamp = + fee.getJSONObject("endStamp").getLong("t_ms") + val wireFee = WireFee( + start = Timestamp(startStamp), + end = Timestamp(endStamp), + wireFee = Amount.fromJsonObject(fee.getJSONObject("wireFee")), + closingFee = Amount.fromJsonObject(fee.getJSONObject("closingFee")) + ) + wireFees.add(wireFee) + } + + return ExchangeFees( + withdrawFee = Amount.fromJsonObject(json.getJSONObject("withdrawFee")), + overhead = Amount.fromJsonObject(json.getJSONObject("overhead")), + earliestDepositExpiration = Timestamp(earliestDepositExpiration), + coinFees = coinFees.map { (coinFee, quantity) -> + CoinFees(quantity, coinFee) + }, + wireFees = wireFees + ) + } + } +} 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 5d0fe63..56a2a8c 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt @@ -57,16 +57,13 @@ class PromptWithdrawFragment : Fragment() { private fun showWithdrawStatus(status: WithdrawStatus?): Any = when (status) { is WithdrawStatus.ReceivedDetails -> { - showContent(status.amount, status.fee, status.suggestedExchange) + showContent(status.amount, status.fee, status.exchange) confirmWithdrawButton.apply { text = getString(R.string.withdraw_button_confirm) setOnClickListener { it.fadeOut() confirmProgressBar.fadeIn() - withdrawManager.acceptWithdrawal( - status.talerWithdrawUri, - status.suggestedExchange - ) + withdrawManager.acceptWithdrawal(status.talerWithdrawUri, status.exchange) } isEnabled = true } @@ -83,7 +80,7 @@ class PromptWithdrawFragment : Fragment() { model.showProgressBar.value = true } is TermsOfServiceReviewRequired -> { - showContent(status.amount, status.fee, status.suggestedExchange) + showContent(status.amount, status.fee, status.exchange) confirmWithdrawButton.apply { text = getString(R.string.withdraw_button_tos) setOnClickListener { @@ -118,6 +115,10 @@ class PromptWithdrawFragment : Fragment() { exchangeIntroView.fadeIn() withdrawExchangeUrl.text = cleanExchange(exchange) withdrawExchangeUrl.fadeIn() + selectExchangeButton.fadeIn() + selectExchangeButton.setOnClickListener { + findNavController().navigate(R.id.action_promptWithdraw_to_selectExchangeFragment) + } withdrawCard.fadeIn() } diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/SelectExchangeFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/SelectExchangeFragment.kt new file mode 100644 index 0000000..78eba53 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/withdraw/SelectExchangeFragment.kt @@ -0,0 +1,136 @@ +/* + * 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.withdraw + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +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 kotlinx.android.synthetic.main.fragment_select_exchange.* +import net.taler.common.Amount +import net.taler.common.toRelativeTime +import net.taler.common.toShortDate +import net.taler.wallet.R +import net.taler.wallet.WalletViewModel +import net.taler.wallet.withdraw.CoinFeeAdapter.CoinFeeViewHolder +import net.taler.wallet.withdraw.WireFeeAdapter.WireFeeViewHolder + +class SelectExchangeFragment : Fragment() { + + private val model: WalletViewModel by activityViewModels() + private val withdrawManager by lazy { model.withdrawManager } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_select_exchange, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val fees = withdrawManager.exchangeFees ?: throw IllegalStateException() + withdrawFeeView.setAmount(fees.withdrawFee) + overheadView.setAmount(fees.overhead) + expirationView.text = fees.earliestDepositExpiration.ms.toRelativeTime(requireContext()) + coinFeesList.adapter = CoinFeeAdapter(fees.coinFees) + 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) : Adapter() { + 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: CoinFees) { + val fee = item.coinFee + coinView.text = res.getQuantityString( + R.plurals.exchange_fee_coin, + item.quantity, + fee.coin, + item.quantity + ) + withdrawFeeView.text = + v.context.getString(R.string.exchange_fee_withdraw_fee, fee.feeWithdraw) + depositFeeView.text = + v.context.getString(R.string.exchange_fee_deposit_fee, fee.feeDeposit) + refreshFeeView.text = + v.context.getString(R.string.exchange_fee_refresh_fee, fee.feeRefresh) + refundFeeView.text = + v.context.getString(R.string.exchange_fee_refund_fee, fee.feeRefresh) + } + } +} + +private class WireFeeAdapter(private val items: List) : Adapter() { + 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/WithdrawManager.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt index 26515a5..6bcd013 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt @@ -28,19 +28,18 @@ sealed class WithdrawStatus { data class Loading(val talerWithdrawUri: String) : WithdrawStatus() data class TermsOfServiceReviewRequired( val talerWithdrawUri: String, - val exchangeBaseUrl: String, + val exchange: String, val tosText: String, val tosEtag: String, val amount: Amount, - val fee: Amount, - val suggestedExchange: String + val fee: Amount ) : WithdrawStatus() data class ReceivedDetails( val talerWithdrawUri: String, + val exchange: String, val amount: Amount, - val fee: Amount, - val suggestedExchange: String + val fee: Amount ) : WithdrawStatus() data class Withdrawing(val talerWithdrawUri: String) : WithdrawStatus() @@ -54,7 +53,8 @@ class WithdrawManager(private val walletBackendApi: WalletBackendApi) { val withdrawStatus = MutableLiveData() val testWithdrawalInProgress = MutableLiveData(false) - private var currentWithdrawRequestId = 0 + var exchangeFees: ExchangeFees? = null + private set fun withdrawTestkudos() { testWithdrawalInProgress.value = true @@ -70,9 +70,6 @@ class WithdrawManager(private val walletBackendApi: WalletBackendApi) { } withdrawStatus.value = WithdrawStatus.Loading(talerWithdrawUri) - this.currentWithdrawRequestId++ - val myWithdrawRequestId = this.currentWithdrawRequestId - walletBackendApi.sendRequest("getWithdrawDetailsForUri", args) { isError, result -> if (isError) { Log.e(TAG, "Error getWithdrawDetailsForUri ${result.toString(4)}") @@ -80,11 +77,6 @@ class WithdrawManager(private val walletBackendApi: WalletBackendApi) { withdrawStatus.postValue(WithdrawStatus.Error(message)) return@sendRequest } - if (myWithdrawRequestId != this.currentWithdrawRequestId) { - val mismatch = "$myWithdrawRequestId != ${this.currentWithdrawRequestId}" - Log.w(TAG, "Got withdraw result for different request id $mismatch") - return@sendRequest - } Log.v(TAG, "got getWithdrawDetailsForUri result") val status = withdrawStatus.value if (status !is WithdrawStatus.Loading) { @@ -105,9 +97,6 @@ class WithdrawManager(private val walletBackendApi: WalletBackendApi) { put("selectedExchange", selectedExchange) } - currentWithdrawRequestId++ - val myWithdrawRequestId = currentWithdrawRequestId - walletBackendApi.sendRequest("getWithdrawDetailsForUri", args) { isError, result -> if (isError) { Log.e(TAG, "Error getWithdrawDetailsForUri ${result.toString(4)}") @@ -115,11 +104,6 @@ class WithdrawManager(private val walletBackendApi: WalletBackendApi) { withdrawStatus.postValue(WithdrawStatus.Error(message)) return@sendRequest } - if (myWithdrawRequestId != currentWithdrawRequestId) { - val mismatch = "$myWithdrawRequestId != $currentWithdrawRequestId" - Log.w(TAG, "Got withdraw result for different request id $mismatch") - return@sendRequest - } Log.v(TAG, "got getWithdrawDetailsForUri result (with exchange details)") val status = withdrawStatus.value if (status !is WithdrawStatus.Loading) { @@ -127,12 +111,13 @@ class WithdrawManager(private val walletBackendApi: WalletBackendApi) { return@sendRequest } val wi = result.getJSONObject("bankWithdrawDetails") - val suggestedExchange = wi.getString("suggestedExchange") val amount = Amount.fromJsonObject(wi.getJSONObject("amount")) val ei = result.getJSONObject("exchangeWithdrawDetails") val termsOfServiceAccepted = ei.getBoolean("termsOfServiceAccepted") + exchangeFees = ExchangeFees.fromExchangeWithdrawDetailsJson(ei) + val withdrawFee = Amount.fromJsonObject(ei.getJSONObject("withdrawFee")) val overhead = Amount.fromJsonObject(ei.getJSONObject("overhead")) val fee = withdrawFee + overhead @@ -145,16 +130,15 @@ class WithdrawManager(private val walletBackendApi: WalletBackendApi) { WithdrawStatus.TermsOfServiceReviewRequired( status.talerWithdrawUri, selectedExchange, tosText, tosEtag, - amount, fee, - suggestedExchange + amount, fee ) ) } else { withdrawStatus.postValue( ReceivedDetails( status.talerWithdrawUri, - amount, fee, - suggestedExchange + selectedExchange, amount, + fee ) ) } @@ -191,7 +175,7 @@ class WithdrawManager(private val walletBackendApi: WalletBackendApi) { check(s is WithdrawStatus.TermsOfServiceReviewRequired) val args = JSONObject().apply { - put("exchangeBaseUrl", s.exchangeBaseUrl) + put("exchangeBaseUrl", s.exchange) put("etag", s.tosEtag) } walletBackendApi.sendRequest("acceptExchangeTermsOfService", args) { isError, result -> @@ -199,7 +183,7 @@ class WithdrawManager(private val walletBackendApi: WalletBackendApi) { Log.e(TAG, "Error acceptExchangeTermsOfService ${result.toString(4)}") return@sendRequest } - val status = ReceivedDetails(s.talerWithdrawUri, s.amount, s.fee, s.suggestedExchange) + val status = ReceivedDetails(s.talerWithdrawUri, s.exchange, s.amount, s.fee) withdrawStatus.postValue(status) } } diff --git a/wallet/src/main/res/layout/fragment_prompt_withdraw.xml b/wallet/src/main/res/layout/fragment_prompt_withdraw.xml index 4372cba..c9c9402 100644 --- a/wallet/src/main/res/layout/fragment_prompt_withdraw.xml +++ b/wallet/src/main/res/layout/fragment_prompt_withdraw.xml @@ -64,7 +64,7 @@ android:layout_marginTop="32dp" android:layout_marginEnd="16dp" android:gravity="center" - android:text="Chosen Amount" + android:text="@string/amount_chosen" android:visibility="invisible" app:layout_constraintBottom_toTopOf="@+id/chosenAmountView" app:layout_constraintEnd_toEndOf="parent" @@ -144,18 +144,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wallet/src/main/res/layout/list_item_coin_fee.xml b/wallet/src/main/res/layout/list_item_coin_fee.xml new file mode 100644 index 0000000..daf2789 --- /dev/null +++ b/wallet/src/main/res/layout/list_item_coin_fee.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + diff --git a/wallet/src/main/res/layout/list_item_wire_fee.xml b/wallet/src/main/res/layout/list_item_wire_fee.xml new file mode 100644 index 0000000..92ede8b --- /dev/null +++ b/wallet/src/main/res/layout/list_item_wire_fee.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml index c39df94..f6d8598 100644 --- a/wallet/src/main/res/navigation/nav_graph.xml +++ b/wallet/src/main/res/navigation/nav_graph.xml @@ -96,6 +96,9 @@ android:id="@+id/action_promptWithdraw_to_errorFragment" app:destination="@id/errorFragment" app:popUpTo="@id/showBalance" /> + + Withdraw Digital Cash Exchange\'s Terms of Service + Exchange Fees Error Go Back @@ -103,6 +104,23 @@ Withdrawal Error Withdrawing is currently not possible. Please try again later! + Withdrawal Fee: + Rounding Loss: + Earliest Coin Expiry: + Coin Fees + Wire Fees + + Coin: %s (used %d time) + Coin: %s (used %d times) + + Withdraw Fee: %s + Deposit Fee: %s + Change Fee: %s + Refund Fee: %s + Timespan: %1$s - %2$s + Wire Fee: %s + Closing Fee: %s + Pending Operations Refuse Proposal (no action) -- cgit v1.2.3