diff options
author | Torsten Grote <t@grobox.de> | 2020-03-03 14:03:45 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-03-03 14:03:45 -0300 |
commit | 30980bc83be99ea85205f44c815b78164b11f7b9 (patch) | |
tree | a8950120aa57df2839e4d1f40cb84ca841f27ad3 /app/src/main/java | |
parent | a9fd9aa024d1cafe50be76eb2ca6a818bce38862 (diff) | |
download | wallet-android-30980bc83be99ea85205f44c815b78164b11f7b9.tar.gz wallet-android-30980bc83be99ea85205f44c815b78164b11f7b9.tar.bz2 wallet-android-30980bc83be99ea85205f44c815b78164b11f7b9.zip |
Clean up and improve withdraw UI (first pass)
Diffstat (limited to 'app/src/main/java')
-rw-r--r-- | app/src/main/java/net/taler/wallet/MainActivity.kt | 6 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/PromptWithdraw.kt | 127 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/ReviewExchangeTOS.kt | 91 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/ShowBalance.kt | 154 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/Utils.kt | 11 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/WalletViewModel.kt | 176 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt | 107 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt | 80 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt | 193 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/withdraw/WithdrawSuccessfulFragment.kt (renamed from app/src/main/java/net/taler/wallet/WithdrawSuccessful.kt) | 28 |
10 files changed, 481 insertions, 492 deletions
diff --git a/app/src/main/java/net/taler/wallet/MainActivity.kt b/app/src/main/java/net/taler/wallet/MainActivity.kt index 79c3373..ebc7136 100644 --- a/app/src/main/java/net/taler/wallet/MainActivity.kt +++ b/app/src/main/java/net/taler/wallet/MainActivity.kt @@ -24,7 +24,7 @@ import android.content.IntentFilter import android.os.Bundle import android.util.Log import android.view.MenuItem -import android.view.View.INVISIBLE +import android.view.View.GONE import android.view.View.VISIBLE import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -74,7 +74,7 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, toolbar.setupWithNavController(nav, appBarConfiguration) model.showProgressBar.observe(this, Observer { show -> - progress_bar.visibility = if (show) VISIBLE else INVISIBLE + progress_bar.visibility = if (show) VISIBLE else GONE }) if (intent.action == ACTION_VIEW) intent.dataString?.let { uri -> @@ -131,7 +131,7 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, url.toLowerCase(ROOT).startsWith("taler://withdraw/") -> { Log.v(TAG, "navigating!") nav.navigate(R.id.action_showBalance_to_promptWithdraw) - model.getWithdrawalInfo(url) + model.withdrawManager.getWithdrawalInfo(url) } url.toLowerCase(ROOT).startsWith("taler://refund/") -> { // TODO implement refunds diff --git a/app/src/main/java/net/taler/wallet/PromptWithdraw.kt b/app/src/main/java/net/taler/wallet/PromptWithdraw.kt deleted file mode 100644 index ec65b0e..0000000 --- a/app/src/main/java/net/taler/wallet/PromptWithdraw.kt +++ /dev/null @@ -1,127 +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 <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet - -import android.annotation.SuppressLint -import android.os.Bundle -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.TextView -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders -import androidx.navigation.findNavController -import com.google.android.material.snackbar.Snackbar -import me.zhanghai.android.materialprogressbar.MaterialProgressBar - - -class PromptWithdraw : Fragment() { - - private lateinit var model: WalletViewModel - - private fun triggerLoading() { - val loading = - model.withdrawStatus.value is WithdrawStatus.Loading || model.withdrawStatus.value is WithdrawStatus.Withdrawing - val myActivity = activity!! - val progressBar = myActivity.findViewById<MaterialProgressBar>(R.id.progress_bar) - if (loading) { - progressBar.visibility = View.VISIBLE - } else { - progressBar.visibility = View.INVISIBLE - } - } - - private fun showWithdrawStatus(view: View, status: WithdrawStatus) { - val confirmButton = view.findViewById<Button>(R.id.button_confirm_withdraw) - val promptWithdraw = view.findViewById<View>(R.id.prompt_withdraw) - when (status) { - is WithdrawStatus.ReceivedDetails -> { - promptWithdraw.visibility = View.VISIBLE - confirmButton.isEnabled = true - val promptWithdraw = view.findViewById<View>(R.id.prompt_withdraw) - promptWithdraw.visibility = View.VISIBLE - val amountView = view.findViewById<TextView>(R.id.withdraw_amount) - val exchangeView = view.findViewById<TextView>(R.id.withdraw_exchange) - exchangeView.text = status.suggestedExchange - @SuppressLint("SetTextI18n") - amountView.text = "${status.amount.amount} ${status.amount.currency}" - } - is WithdrawStatus.Success -> { - this.model.withdrawStatus.value = WithdrawStatus.None() - activity!!.findNavController(R.id.nav_host_fragment) - .navigate(R.id.action_promptWithdraw_to_withdrawSuccessful) - } - is WithdrawStatus.Loading -> { - promptWithdraw.visibility = View.INVISIBLE - // Wait - } - is WithdrawStatus.Withdrawing -> { - confirmButton.isEnabled = false - - } - is WithdrawStatus.None -> { - - } - is WithdrawStatus.TermsOfServiceReviewRequired -> { - val navController = requireActivity().findNavController(R.id.nav_host_fragment) - navController.navigate(R.id.action_promptWithdraw_to_reviewExchangeTOS) - } - else -> { - val bar = Snackbar.make(view, "Bug: Unexpected result", Snackbar.LENGTH_SHORT) - bar.show() - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - model = activity?.run { - ViewModelProviders.of(this)[WalletViewModel::class.java] - } ?: throw Exception("Invalid Activity") - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val view = inflater.inflate(R.layout.fragment_prompt_withdraw, container, false) - - this.model.withdrawStatus.observe(this, Observer { - triggerLoading() - showWithdrawStatus(view, it) - }) - - view.findViewById<Button>(R.id.button_cancel_withdraw).setOnClickListener { - val navController = requireActivity().findNavController(R.id.nav_host_fragment) - model.cancelCurrentWithdraw() - navController.navigateUp() - } - - view.findViewById<Button>(R.id.button_confirm_withdraw).setOnClickListener { - val status = this.model.withdrawStatus.value - if (status !is WithdrawStatus.ReceivedDetails) { - return@setOnClickListener - } - model.acceptWithdrawal(status.talerWithdrawUri, status.suggestedExchange) - } - - return view - } -} diff --git a/app/src/main/java/net/taler/wallet/ReviewExchangeTOS.kt b/app/src/main/java/net/taler/wallet/ReviewExchangeTOS.kt deleted file mode 100644 index 4f5db9e..0000000 --- a/app/src/main/java/net/taler/wallet/ReviewExchangeTOS.kt +++ /dev/null @@ -1,91 +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 <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet - - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.CheckBox -import android.widget.TextView -import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders -import androidx.navigation.findNavController - -/** - * A simple [Fragment] subclass. - */ -class ReviewExchangeTOS : Fragment() { - - private lateinit var acceptButton: Button - private lateinit var model: WalletViewModel - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - model = activity?.run { - ViewModelProviders.of(this)[WalletViewModel::class.java] - } ?: throw Exception("Invalid Activity") - } - - private fun onAcceptCheck(checked: Boolean) { - acceptButton.isEnabled = checked - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - val view = inflater.inflate(R.layout.fragment_review_exchange_tos, container, false) - val navController = requireActivity().findNavController(R.id.nav_host_fragment) - view.findViewById<Button>(R.id.button_tos_abort).setOnClickListener { - model.cancelCurrentWithdraw() - navController.navigateUp() - } - acceptButton = view.findViewById(R.id.button_tos_accept) - acceptButton.setOnClickListener { - model.acceptCurrentTermsOfService() - } - val checkbox = view.findViewById<CheckBox>(R.id.checkBox_accept_tos) - checkbox.isChecked = false - checkbox.setOnCheckedChangeListener { buttonView, isChecked -> - onAcceptCheck(isChecked) - } - onAcceptCheck(false) - val tosTextField = view.findViewById<TextView>(R.id.text_tos) - model.withdrawStatus.observe(this, Observer { - when (it) { - is WithdrawStatus.TermsOfServiceReviewRequired -> { - tosTextField.text = it.tosText - } - is WithdrawStatus.Loading -> { - navController.navigate(R.id.action_reviewExchangeTOS_to_promptWithdraw) - } - is WithdrawStatus.ReceivedDetails -> { - navController.navigate(R.id.action_reviewExchangeTOS_to_promptWithdraw) - } - else -> { - } - } - }) - return view - } -} diff --git a/app/src/main/java/net/taler/wallet/ShowBalance.kt b/app/src/main/java/net/taler/wallet/ShowBalance.kt index 26fd050..4b52426 100644 --- a/app/src/main/java/net/taler/wallet/ShowBalance.kt +++ b/app/src/main/java/net/taler/wallet/ShowBalance.kt @@ -16,7 +16,6 @@ package net.taler.wallet - import android.annotation.SuppressLint import android.os.Bundle import android.util.Log @@ -25,20 +24,21 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.Button import android.widget.LinearLayout import android.widget.TextView import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator.QR_CODE_TYPES -import me.zhanghai.android.materialprogressbar.MaterialProgressBar import org.json.JSONObject class WalletBalanceAdapter(private var myDataset: WalletBalances) : @@ -70,9 +70,9 @@ class WalletBalanceAdapter(private var myDataset: WalletBalances) : val amountIncomingView = holder.rowView.findViewById<TextView>(R.id.balance_pending) if (amountIncoming.isZero()) { - amountIncomingRow.visibility = View.GONE + amountIncomingRow.visibility = GONE } else { - amountIncomingRow.visibility = View.VISIBLE + amountIncomingRow.visibility = VISIBLE @SuppressLint("SetTextI18n") amountIncomingView.text = "${amountIncoming.amount} ${amountIncoming.currency}" } @@ -120,7 +120,7 @@ class PendingOperationsAdapter(private var myDataset: PendingOperations) : "proposal-choice" -> { val btn1 = holder.rowView.findViewById<TextView>(R.id.button_pending_action_1) btn1.text = btn1.context.getString(R.string.pending_operations_refuse) - btn1.visibility = View.VISIBLE + btn1.visibility = VISIBLE btn1.setOnClickListener { this.listener?.onPendingOperationActionClick(p.type, p.detail) } @@ -128,7 +128,7 @@ class PendingOperationsAdapter(private var myDataset: PendingOperations) : else -> { val btn1 = holder.rowView.findViewById<TextView>(R.id.button_pending_action_1) btn1.text = btn1.context.getString(R.string.pending_operations_no_action) - btn1.visibility = View.GONE + btn1.visibility = GONE btn1.setOnClickListener {} } } @@ -151,93 +151,21 @@ interface PendingOperationClickListener { fun onPendingOperationActionClick(type: String, detail: JSONObject) } -/** - * A simple [Fragment] subclass. - * - */ class ShowBalance : Fragment(), PendingOperationClickListener { + private val model: WalletViewModel by activityViewModels() + private val withdrawManager by lazy { model.withdrawManager } + private lateinit var pendingOperationsLabel: View private lateinit var balancesView: RecyclerView private lateinit var balancesPlaceholderView: TextView - private lateinit var model: WalletViewModel private lateinit var balancesAdapter: WalletBalanceAdapter private lateinit var pendingAdapter: PendingOperationsAdapter - private fun triggerLoading() { - val loading: Boolean = - (model.testWithdrawalInProgress.value == true) || (model.balances.value == null) || !model.balances.value!!.initialized - - val myActivity = activity!! - val progressBar = myActivity.findViewById<MaterialProgressBar>(R.id.progress_bar) - if (loading) { - progressBar.visibility = View.VISIBLE - } else { - progressBar.visibility = View.INVISIBLE - } - } - - override fun onResume() { - super.onResume() - triggerLoading() - Log.v("taler-wallet", "called onResume on ShowBalance") - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.retry_pending -> { - model.retryPendingNow() - true - } - R.id.reload_balance -> { - triggerLoading() - model.balances.value = WalletBalances(false, listOf()) - model.getBalances() - true - } - else -> super.onOptionsItemSelected(item) - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.balance, menu) - super.onCreateOptionsMenu(menu, inflater) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) - - model = activity?.run { - ViewModelProvider(this)[WalletViewModel::class.java] - } ?: throw Exception("Invalid Activity") - - } - - - private fun updateBalances(balances: WalletBalances) { - if (!balances.initialized) { - balancesPlaceholderView.visibility = View.GONE - balancesView.visibility = View.GONE - } else if (balances.byCurrency.isEmpty()) { - balancesPlaceholderView.visibility = View.VISIBLE - balancesView.visibility = View.GONE - } else { - balancesPlaceholderView.visibility = View.GONE - balancesView.visibility = View.VISIBLE - } - Log.v(TAG, "updating balances $balances") - balancesAdapter.update(balances) - } - - private fun updatePending(pendingOperations: PendingOperations) { - if (pendingOperations.pending.isEmpty()) { - pendingOperationsLabel.visibility = View.GONE - } else { - pendingOperationsLabel.visibility = View.VISIBLE - } - pendingAdapter.update(pendingOperations) } override fun onCreateView( @@ -279,10 +207,10 @@ class ShowBalance : Fragment(), PendingOperationClickListener { val withdrawTestkudosButton = view.findViewById<Button>(R.id.button_withdraw_testkudos) withdrawTestkudosButton.setOnClickListener { - model.withdrawTestkudos() + withdrawManager.withdrawTestkudos() } - model.testWithdrawalInProgress.observe(viewLifecycleOwner, Observer { loading -> + withdrawManager.testWithdrawalInProgress.observe(viewLifecycleOwner, Observer { loading -> Log.v("taler-wallet", "observing balance loading $loading in show balance") withdrawTestkudosButton.isEnabled = !loading triggerLoading() @@ -308,6 +236,64 @@ class ShowBalance : Fragment(), PendingOperationClickListener { return view } + override fun onResume() { + super.onResume() + triggerLoading() + Log.v("taler-wallet", "called onResume on ShowBalance") + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.retry_pending -> { + model.retryPendingNow() + true + } + R.id.reload_balance -> { + triggerLoading() + model.balances.value = WalletBalances(false, listOf()) + model.getBalances() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.balance, menu) + super.onCreateOptionsMenu(menu, inflater) + } + + private fun triggerLoading() { + val withdrawInProgress = withdrawManager.testWithdrawalInProgress.value == true + val balances = model.balances.value + val loading: Boolean = (withdrawInProgress) || (balances == null) || !balances.initialized + model.showProgressBar.value = loading + } + + private fun updateBalances(balances: WalletBalances) { + if (!balances.initialized) { + balancesPlaceholderView.visibility = GONE + balancesView.visibility = GONE + } else if (balances.byCurrency.isEmpty()) { + balancesPlaceholderView.visibility = VISIBLE + balancesView.visibility = GONE + } else { + balancesPlaceholderView.visibility = GONE + balancesView.visibility = VISIBLE + } + Log.v(TAG, "updating balances $balances") + balancesAdapter.update(balances) + } + + private fun updatePending(pendingOperations: PendingOperations) { + if (pendingOperations.pending.isEmpty()) { + pendingOperationsLabel.visibility = GONE + } else { + pendingOperationsLabel.visibility = VISIBLE + } + pendingAdapter.update(pendingOperations) + } + override fun onPendingOperationClick(type: String, detail: JSONObject) { val v = view ?: return when { diff --git a/app/src/main/java/net/taler/wallet/Utils.kt b/app/src/main/java/net/taler/wallet/Utils.kt index 673fa2b..fb0b3ae 100644 --- a/app/src/main/java/net/taler/wallet/Utils.kt +++ b/app/src/main/java/net/taler/wallet/Utils.kt @@ -20,16 +20,21 @@ import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE -fun View.fadeIn() { +fun View.fadeIn(endAction: () -> Unit = {}) { + if (visibility == VISIBLE) return alpha = 0f visibility = VISIBLE - animate().alpha(1f).start() + animate().alpha(1f).withEndAction { + if (context != null) endAction.invoke() + }.start() } -fun View.fadeOut() { +fun View.fadeOut(endAction: () -> Unit = {}) { if (visibility == INVISIBLE) return animate().alpha(0f).withEndAction { + if (context == null) return@withEndAction visibility = INVISIBLE alpha = 1f + endAction.invoke() }.start() } diff --git a/app/src/main/java/net/taler/wallet/WalletViewModel.kt b/app/src/main/java/net/taler/wallet/WalletViewModel.kt index 35f59f1..d9e730d 100644 --- a/app/src/main/java/net/taler/wallet/WalletViewModel.kt +++ b/app/src/main/java/net/taler/wallet/WalletViewModel.kt @@ -18,6 +18,7 @@ package net.taler.wallet import android.app.Application import android.util.Log +import androidx.annotation.UiThread import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -36,6 +37,7 @@ import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.history.History import net.taler.wallet.history.HistoryEvent import net.taler.wallet.payment.PaymentManager +import net.taler.wallet.withdraw.WithdrawManager import org.json.JSONObject const val TAG = "taler-wallet" @@ -46,26 +48,6 @@ data class BalanceEntry(val available: Amount, val pendingIncoming: Amount) data class WalletBalances(val initialized: Boolean, val byCurrency: List<BalanceEntry>) -open class WithdrawStatus { - class None : WithdrawStatus() - data class Loading(val talerWithdrawUri: String) : WithdrawStatus() - data class TermsOfServiceReviewRequired( - val talerWithdrawUri: String, - val exchangeBaseUrl: String, - val tosText: String, - val tosEtag: String - ) : WithdrawStatus() - - class Success : WithdrawStatus() - data class ReceivedDetails( - val talerWithdrawUri: String, - val amount: Amount, - val suggestedExchange: String - ) : WithdrawStatus() - - data class Withdrawing(val talerWithdrawUri: String) : WithdrawStatus() -} - open class PendingOperationInfo( val type: String, val detail: JSONObject @@ -79,18 +61,10 @@ open class PendingOperations( @Suppress("EXPERIMENTAL_API_USAGE") class WalletViewModel(val app: Application) : AndroidViewModel(app) { - val testWithdrawalInProgress = MutableLiveData<Boolean>().apply { - value = false - } - val balances = MutableLiveData<WalletBalances>().apply { value = WalletBalances(false, listOf()) } - val withdrawStatus = MutableLiveData<WithdrawStatus>().apply { - value = WithdrawStatus.None() - } - val pendingOperations = MutableLiveData<PendingOperations>().apply { value = PendingOperations(listOf()) } @@ -112,14 +86,13 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { private var activeGetBalance = 0 private var activeGetPending = 0 - private var currentWithdrawRequestId = 0 - private val walletBackendApi = WalletBackendApi(app) private val mapper = ObjectMapper() .registerModule(KotlinModule()) .configure(FAIL_ON_UNKNOWN_PROPERTIES, false) + val withdrawManager = WithdrawManager(walletBackendApi) val paymentManager = PaymentManager(walletBackendApi, mapper) init { @@ -212,17 +185,10 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { awaitClose() } - fun withdrawTestkudos() { - testWithdrawalInProgress.value = true - - walletBackendApi.sendRequest("withdrawTestkudos", null) { _, _ -> - testWithdrawalInProgress.postValue(false) - } - } - + @UiThread fun dangerouslyReset() { walletBackendApi.sendRequest("reset", null) - testWithdrawalInProgress.value = false + withdrawManager.testWithdrawalInProgress.value = false balances.value = WalletBalances(false, listOf()) } @@ -239,113 +205,6 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { walletBackendApi.sendRequest("tunnelResponse", respJson) } - fun getWithdrawalInfo(talerWithdrawUri: String) { - val args = JSONObject() - args.put("talerWithdrawUri", talerWithdrawUri) - - 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)}") - 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) { - Log.v(TAG, "ignoring withdrawal info result, not loading.") - return@sendRequest - } - val wi = result.getJSONObject("bankWithdrawDetails") - val suggestedExchange = wi.getString("suggestedExchange") - // We just use the suggested exchange, in the future there will be - // a selection dialog. - getWithdrawalInfoWithExchange(talerWithdrawUri, suggestedExchange) - } - } - - private fun getWithdrawalInfoWithExchange(talerWithdrawUri: String, selectedExchange: String) { - val args = JSONObject() - args.put("talerWithdrawUri", talerWithdrawUri) - args.put("selectedExchange", selectedExchange) - - this.currentWithdrawRequestId++ - val myWithdrawRequestId = this.currentWithdrawRequestId - - walletBackendApi.sendRequest("getWithdrawDetailsForUri", args) { isError, result -> - if (isError) { - Log.e(TAG, "Error getWithdrawDetailsForUri ${result.toString(4)}") - 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 (with exchange details)") - val status = withdrawStatus.value - if (status !is WithdrawStatus.Loading) { - Log.v(TAG, "ignoring withdrawal info result, not loading.") - return@sendRequest - } - val ei = result.getJSONObject("exchangeWithdrawDetails") - val termsOfServiceAccepted = ei.getBoolean("termsOfServiceAccepted") - if (!termsOfServiceAccepted) { - val exchange = ei.getJSONObject("exchangeInfo") - val tosText = exchange.getString("termsOfServiceText") - val tosEtag = exchange.optString("termsOfServiceLastEtag", "undefined") - withdrawStatus.postValue( - WithdrawStatus.TermsOfServiceReviewRequired( - status.talerWithdrawUri, - selectedExchange, - tosText, - tosEtag - ) - ) - } else { - val wi = result.getJSONObject("bankWithdrawDetails") - val suggestedExchange = wi.getString("suggestedExchange") - val amount = Amount.fromJson(wi.getJSONObject("amount")) - withdrawStatus.postValue( - WithdrawStatus.ReceivedDetails( - status.talerWithdrawUri, - amount, - suggestedExchange - ) - ) - } - } - } - - fun acceptWithdrawal(talerWithdrawUri: String, selectedExchange: String) { - val args = JSONObject() - args.put("talerWithdrawUri", talerWithdrawUri) - args.put("selectedExchange", selectedExchange) - - withdrawStatus.value = WithdrawStatus.Withdrawing(talerWithdrawUri) - - walletBackendApi.sendRequest("acceptWithdrawal", args) { isError, _ -> - if (isError) { - Log.v(TAG, "got acceptWithdrawal error result") - return@sendRequest - } - Log.v(TAG, "got acceptWithdrawal result") - val status = withdrawStatus.value - if (status !is WithdrawStatus.Withdrawing) { - Log.v(TAG, "ignoring acceptWithdrawal result, invalid state") - } - withdrawStatus.postValue(WithdrawStatus.Success()) - } - } - fun retryPendingNow() { walletBackendApi.sendRequest("retryPendingNow", null) } @@ -354,29 +213,4 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { walletBackendApi.destroy() super.onCleared() } - - /** - * Accept the currently displayed terms of service. - */ - fun acceptCurrentTermsOfService() { - when (val s = withdrawStatus.value) { - is WithdrawStatus.TermsOfServiceReviewRequired -> { - val args = JSONObject() - args.put("exchangeBaseUrl", s.exchangeBaseUrl) - args.put("etag", s.tosEtag) - walletBackendApi.sendRequest("acceptExchangeTermsOfService", args) { isError, _ -> - if (isError) { - return@sendRequest - } - // Try withdrawing again with accepted ToS - getWithdrawalInfo(s.talerWithdrawUri) - } - } - } - } - - fun cancelCurrentWithdraw() { - currentWithdrawRequestId++ - withdrawStatus.value = WithdrawStatus.None() - } } diff --git a/app/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt b/app/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt new file mode 100644 index 0000000..0b14e32 --- /dev/null +++ b/app/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt @@ -0,0 +1,107 @@ +/* + * 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.withdraw + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.fragment_prompt_withdraw.* +import net.taler.wallet.R +import net.taler.wallet.WalletViewModel +import net.taler.wallet.fadeIn +import net.taler.wallet.fadeOut +import net.taler.wallet.withdraw.WithdrawStatus.Loading +import net.taler.wallet.withdraw.WithdrawStatus.TermsOfServiceReviewRequired +import net.taler.wallet.withdraw.WithdrawStatus.Withdrawing + +class PromptWithdrawFragment : 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_prompt_withdraw, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + button_cancel_withdraw.setOnClickListener { + withdrawManager.cancelCurrentWithdraw() + findNavController().navigateUp() + } + + button_confirm_withdraw.setOnClickListener { + val status = withdrawManager.withdrawStatus.value + if (status !is WithdrawStatus.ReceivedDetails) throw AssertionError() + it.fadeOut() + confirmProgressBar.fadeIn() + withdrawManager.acceptWithdrawal(status.talerWithdrawUri, status.suggestedExchange) + } + + withdrawManager.withdrawStatus.observe(viewLifecycleOwner, Observer { + showWithdrawStatus(it) + }) + } + + private fun showWithdrawStatus(status: WithdrawStatus) = when (status) { + is WithdrawStatus.ReceivedDetails -> { + model.showProgressBar.value = false + progressBar.fadeOut() + + introView.fadeIn() + @SuppressLint("SetTextI18n") + withdrawAmountView.text = "${status.amount.amount} ${status.amount.currency}" + withdrawAmountView.fadeIn() + feeView.fadeIn() + + exchangeIntroView.fadeIn() + withdrawExchangeUrl.text = status.suggestedExchange + withdrawExchangeUrl.fadeIn() + + button_confirm_withdraw.isEnabled = true + } + is WithdrawStatus.Success -> { + model.showProgressBar.value = false + withdrawManager.withdrawStatus.value = WithdrawStatus.None + findNavController().navigate(R.id.action_promptWithdraw_to_withdrawSuccessful) + } + is Loading -> { + model.showProgressBar.value = true + } + is Withdrawing -> { + model.showProgressBar.value = true + } + is TermsOfServiceReviewRequired -> { + model.showProgressBar.value = false + findNavController().navigate(R.id.action_promptWithdraw_to_reviewExchangeTOS) + } + is WithdrawStatus.None -> { + model.showProgressBar.value = false + } + } + +} diff --git a/app/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt b/app/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt new file mode 100644 index 0000000..cd01a33 --- /dev/null +++ b/app/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt @@ -0,0 +1,80 @@ +/* + * 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.withdraw + + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.fragment_review_exchange_tos.* +import net.taler.wallet.R +import net.taler.wallet.WalletViewModel +import net.taler.wallet.fadeIn +import net.taler.wallet.fadeOut + +class ReviewExchangeTosFragment : 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_review_exchange_tos, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + acceptTosCheckBox.isChecked = false + acceptTosCheckBox.setOnCheckedChangeListener { _, isChecked -> + acceptTosButton.isEnabled = isChecked + } + abortTosButton.setOnClickListener { + withdrawManager.cancelCurrentWithdraw() + findNavController().navigateUp() + } + acceptTosButton.setOnClickListener { + withdrawManager.acceptCurrentTermsOfService() + } + withdrawManager.withdrawStatus.observe(viewLifecycleOwner, Observer { + when (it) { + is WithdrawStatus.TermsOfServiceReviewRequired -> { + tosTextView.text = it.tosText + tosTextView.fadeIn() + acceptTosCheckBox.fadeIn() + progressBar.fadeOut() + } + is WithdrawStatus.Loading -> { + findNavController().navigate(R.id.action_reviewExchangeTOS_to_promptWithdraw) + } + is WithdrawStatus.ReceivedDetails -> { + findNavController().navigate(R.id.action_reviewExchangeTOS_to_promptWithdraw) + } + else -> { + } + } + }) + } + +} diff --git a/app/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt b/app/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt new file mode 100644 index 0000000..fa20318 --- /dev/null +++ b/app/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt @@ -0,0 +1,193 @@ +/* + * 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.withdraw + +import android.util.Log +import androidx.lifecycle.MutableLiveData +import net.taler.wallet.Amount +import net.taler.wallet.TAG +import net.taler.wallet.backend.WalletBackendApi +import org.json.JSONObject + +sealed class WithdrawStatus { + object None : WithdrawStatus() + data class Loading(val talerWithdrawUri: String) : WithdrawStatus() + data class TermsOfServiceReviewRequired( + val talerWithdrawUri: String, + val exchangeBaseUrl: String, + val tosText: String, + val tosEtag: String + ) : WithdrawStatus() + + object Success : WithdrawStatus() + data class ReceivedDetails( + val talerWithdrawUri: String, + val amount: Amount, + val suggestedExchange: String + ) : WithdrawStatus() + + data class Withdrawing(val talerWithdrawUri: String) : WithdrawStatus() +} + +class WithdrawManager(private val walletBackendApi: WalletBackendApi) { + + val withdrawStatus = MutableLiveData<WithdrawStatus>(WithdrawStatus.None) + val testWithdrawalInProgress = MutableLiveData<Boolean>(false) + + private var currentWithdrawRequestId = 0 + + fun withdrawTestkudos() { + testWithdrawalInProgress.value = true + + walletBackendApi.sendRequest("withdrawTestkudos", null) { _, _ -> + testWithdrawalInProgress.postValue(false) + } + } + + fun getWithdrawalInfo(talerWithdrawUri: String) { + val args = JSONObject() + args.put("talerWithdrawUri", talerWithdrawUri) + + 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)}") + 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) { + Log.v(TAG, "ignoring withdrawal info result, not loading.") + return@sendRequest + } + val wi = result.getJSONObject("bankWithdrawDetails") + val suggestedExchange = wi.getString("suggestedExchange") + // We just use the suggested exchange, in the future there will be + // a selection dialog. + getWithdrawalInfoWithExchange(talerWithdrawUri, suggestedExchange) + } + } + + private fun getWithdrawalInfoWithExchange(talerWithdrawUri: String, selectedExchange: String) { + val args = JSONObject() + args.put("talerWithdrawUri", talerWithdrawUri) + args.put("selectedExchange", selectedExchange) + + this.currentWithdrawRequestId++ + val myWithdrawRequestId = this.currentWithdrawRequestId + + walletBackendApi.sendRequest("getWithdrawDetailsForUri", args) { isError, result -> + if (isError) { + Log.e(TAG, "Error getWithdrawDetailsForUri ${result.toString(4)}") + 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 (with exchange details)") + val status = withdrawStatus.value + if (status !is WithdrawStatus.Loading) { + Log.v(TAG, "ignoring withdrawal info result, not loading.") + return@sendRequest + } + val ei = result.getJSONObject("exchangeWithdrawDetails") + val termsOfServiceAccepted = ei.getBoolean("termsOfServiceAccepted") + if (!termsOfServiceAccepted) { + val exchange = ei.getJSONObject("exchangeInfo") + val tosText = exchange.getString("termsOfServiceText") + val tosEtag = exchange.optString("termsOfServiceLastEtag", "undefined") + withdrawStatus.postValue( + WithdrawStatus.TermsOfServiceReviewRequired( + status.talerWithdrawUri, + selectedExchange, + tosText, + tosEtag + ) + ) + } else { + val wi = result.getJSONObject("bankWithdrawDetails") + val suggestedExchange = wi.getString("suggestedExchange") + val amount = Amount.fromJson(wi.getJSONObject("amount")) + withdrawStatus.postValue( + WithdrawStatus.ReceivedDetails( + status.talerWithdrawUri, + amount, + suggestedExchange + ) + ) + } + } + } + + fun acceptWithdrawal(talerWithdrawUri: String, selectedExchange: String) { + val args = JSONObject() + args.put("talerWithdrawUri", talerWithdrawUri) + args.put("selectedExchange", selectedExchange) + + withdrawStatus.value = WithdrawStatus.Withdrawing(talerWithdrawUri) + + walletBackendApi.sendRequest("acceptWithdrawal", args) { isError, _ -> + if (isError) { + Log.v(TAG, "got acceptWithdrawal error result") + return@sendRequest + } + Log.v(TAG, "got acceptWithdrawal result") + val status = withdrawStatus.value + if (status !is WithdrawStatus.Withdrawing) { + Log.v(TAG, "ignoring acceptWithdrawal result, invalid state") + } + withdrawStatus.postValue(WithdrawStatus.Success) + } + } + + /** + * Accept the currently displayed terms of service. + */ + fun acceptCurrentTermsOfService() { + when (val s = withdrawStatus.value) { + is WithdrawStatus.TermsOfServiceReviewRequired -> { + val args = JSONObject() + args.put("exchangeBaseUrl", s.exchangeBaseUrl) + args.put("etag", s.tosEtag) + walletBackendApi.sendRequest("acceptExchangeTermsOfService", args) { isError, _ -> + if (isError) { + return@sendRequest + } + // Try withdrawing again with accepted ToS + getWithdrawalInfo(s.talerWithdrawUri) + } + } + } + } + + fun cancelCurrentWithdraw() { + currentWithdrawRequestId++ + withdrawStatus.value = WithdrawStatus.None + } + +} diff --git a/app/src/main/java/net/taler/wallet/WithdrawSuccessful.kt b/app/src/main/java/net/taler/wallet/withdraw/WithdrawSuccessfulFragment.kt index 4ff7478..5daeff1 100644 --- a/app/src/main/java/net/taler/wallet/WithdrawSuccessful.kt +++ b/app/src/main/java/net/taler/wallet/withdraw/WithdrawSuccessfulFragment.kt @@ -14,29 +14,31 @@ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -package net.taler.wallet - +package net.taler.wallet.withdraw import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Button -import androidx.navigation.findNavController +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.fragment_withdraw_successful.* +import net.taler.wallet.R + +class WithdrawSuccessfulFragment : Fragment() { -/** - * A simple [Fragment] subclass. - */ -class WithdrawSuccessful : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - val view = inflater.inflate(R.layout.fragment_withdraw_successful, container, false) - view.findViewById<Button>(R.id.backButton).setOnClickListener { - activity!!.findNavController(R.id.nav_host_fragment).navigateUp() + return inflater.inflate(R.layout.fragment_withdraw_successful, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + backButton.setOnClickListener { + findNavController().navigateUp() } - return view } + } |