diff options
author | Torsten Grote <t@grobox.de> | 2020-02-12 16:53:03 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-02-12 17:09:09 -0300 |
commit | ada382885e9c103fe0795817a8585270a3079302 (patch) | |
tree | 3da610aa39d2c767307d974d54f2f4eb32be9188 | |
parent | c20a7945f2b37863264c3b9bdcc85454018bd4cd (diff) | |
download | wallet-android-ada382885e9c103fe0795817a8585270a3079302.tar.gz wallet-android-ada382885e9c103fe0795817a8585270a3079302.tar.bz2 wallet-android-ada382885e9c103fe0795817a8585270a3079302.zip |
Refactor payment code to make it easier to extend
17 files changed, 462 insertions, 479 deletions
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index a88ded0..a705caf 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -4,6 +4,8 @@ <option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" /> </AndroidXmlCodeStyleSettings> <JetCodeStyleSettings> + <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" /> + <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> </JetCodeStyleSettings> <codeStyleSettings language="XML"> diff --git a/app/build.gradle b/app/build.gradle index c40ddef..8c0746e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,9 +35,9 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.1.0' + implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'com.google.android.material:material:1.0.0' + implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' @@ -45,12 +45,12 @@ dependencies { implementation project(":akono") implementation 'com.google.guava:guava:28.0-android' - def nav_version = "2.2.0-rc04" + def nav_version = "2.2.1" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" // ViewModel and LiveData - def lifecycle_version = "2.2.0-rc03" + def lifecycle_version = "2.2.0" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" diff --git a/app/src/main/java/net/taler/wallet/MainActivity.kt b/app/src/main/java/net/taler/wallet/MainActivity.kt index 3f38a44..8981b62 100644 --- a/app/src/main/java/net/taler/wallet/MainActivity.kt +++ b/app/src/main/java/net/taler/wallet/MainActivity.kt @@ -14,10 +14,8 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - package net.taler.wallet - import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -26,41 +24,37 @@ import android.os.Bundle import android.util.Log import android.view.Menu import android.view.MenuItem -import android.view.View +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.Toolbar import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout -import androidx.lifecycle.ViewModelProviders -import androidx.navigation.NavController +import androidx.lifecycle.Observer import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController -import com.google.android.material.floatingactionbutton.FloatingActionButton -import com.google.android.material.navigation.NavigationView +import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener import com.google.android.material.snackbar.Snackbar +import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentResult -import me.zhanghai.android.materialprogressbar.MaterialProgressBar -import java.util.* +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.app_bar_main.* +import java.util.Locale.ROOT +class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, + ResetDialogEventListener { -class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener, ResetDialogEventListener { - - lateinit var model: WalletViewModel + private val model: WalletViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - val toolbar: Toolbar = findViewById(R.id.toolbar) - setSupportActionBar(toolbar) - val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout) - val navView: NavigationView = findViewById(R.id.nav_view) - - navView.menu.getItem(0).isChecked = true + nav_view.menu.getItem(0).isChecked = true + nav_view.setNavigationItemSelectedListener(this) - val fab: FloatingActionButton = findViewById(R.id.fab) fab.setOnClickListener { val integrator = IntentIntegrator(this) integrator.setPrompt("Place merchant's QR Code inside the viewfinder rectangle to initiate payment.") @@ -68,24 +62,21 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte } fab.hide() - navView.setNavigationItemSelectedListener(this) - + setSupportActionBar(toolbar) val navController = findNavController(R.id.nav_host_fragment) - - val appBarConfiguration = - AppBarConfiguration(setOf(R.id.showBalance, R.id.settings, R.id.walletHistory), drawerLayout) - - findViewById<Toolbar>(R.id.toolbar) - .setupWithNavController(navController, appBarConfiguration) - - model = ViewModelProviders.of(this)[WalletViewModel::class.java] - - val progressBar = findViewById<MaterialProgressBar>(R.id.progress_bar) - progressBar.visibility = View.INVISIBLE + val appBarConfiguration = AppBarConfiguration( + setOf(R.id.showBalance, R.id.settings, R.id.walletHistory), + drawer_layout + ) + toolbar.setupWithNavController(navController, appBarConfiguration) model.init() model.getBalances() + model.showProgressBar.observe(this, Observer { show -> + progress_bar.visibility = if (show) VISIBLE else INVISIBLE + }) + val triggerPaymentFilter = IntentFilter(HostCardEmulatorService.TRIGGER_PAYMENT_ACTION) registerReceiver(object : BroadcastReceiver() { override fun onReceive(p0: Context?, p1: Intent?) { @@ -97,7 +88,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte val url = p1!!.extras!!.get("contractUrl") as String findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_promptPayment) - model.preparePay(url) + model.paymentManager.preparePay(url) } }, triggerPaymentFilter) @@ -188,12 +179,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte val scanResult: IntentResult? = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) if (scanResult == null || scanResult.contents == null) { - val bar: Snackbar = Snackbar.make( - findViewById(R.id.nav_host_fragment), - "QR Code scan canceled.", - Snackbar.LENGTH_SHORT - ) - bar.show() + Snackbar.make(nav_view, "QR Code scan canceled.", LENGTH_SHORT).show() return } @@ -203,35 +189,37 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte private fun handleTalerUri(url: String, from: String) { when { - url.toLowerCase(Locale.ROOT).startsWith("taler://pay/") -> { + url.toLowerCase(ROOT).startsWith("taler://pay/") -> { Log.v(TAG, "navigating!") findNavController(R.id.nav_host_fragment).navigate(R.id.action_showBalance_to_promptPayment) - model.preparePay(url) + model.paymentManager.preparePay(url) } - url.toLowerCase(Locale.ROOT).startsWith("taler://withdraw/") -> { + url.toLowerCase(ROOT).startsWith("taler://withdraw/") -> { Log.v(TAG, "navigating!") findNavController(R.id.nav_host_fragment).navigate(R.id.action_showBalance_to_promptWithdraw) model.getWithdrawalInfo(url) } + url.toLowerCase(ROOT).startsWith("taler://refund/") -> { + // TODO implement refunds + Snackbar.make(nav_view, "Refunds are not yet implemented", LENGTH_SHORT).show() + } else -> { - val bar: Snackbar = Snackbar.make( - findViewById(R.id.nav_host_fragment), - "URL from $from doesn't contain Taler payment.", - Snackbar.LENGTH_SHORT - ) - bar.show() + Snackbar.make( + nav_view, + "URL from $from doesn't contain a supported Taler Uri.", + LENGTH_SHORT + ).show() } } } override fun onResetConfirmed() { model.dangerouslyReset() - val snackbar = Snackbar.make(findViewById(R.id.nav_host_fragment), "Wallet has been reset", Snackbar.LENGTH_SHORT) - snackbar.show() + Snackbar.make(nav_view, "Wallet has been reset", LENGTH_SHORT).show() } override fun onResetCancelled() { - val snackbar = Snackbar.make(findViewById(R.id.nav_host_fragment), "Reset cancelled", Snackbar.LENGTH_SHORT) - snackbar.show() + Snackbar.make(nav_view, "Reset cancelled", LENGTH_SHORT).show() } + } diff --git a/app/src/main/java/net/taler/wallet/PromptPayment.kt b/app/src/main/java/net/taler/wallet/PromptPayment.kt deleted file mode 100644 index aa7512d..0000000 --- a/app/src/main/java/net/taler/wallet/PromptPayment.kt +++ /dev/null @@ -1,174 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 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 android.util.Log -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 - -/** - * Show a payment and ask the user to accept/decline. - */ -class PromptPayment : Fragment() { - - lateinit var model: WalletViewModel - - var fragmentView: View? = null - - private fun triggerLoading() { - val loading = model.payStatus.value == null || (model.payStatus.value is PayStatus.Loading) - val progressBar = requireActivity().findViewById<MaterialProgressBar>(R.id.progress_bar) - if (loading) { - progressBar.visibility = View.VISIBLE - } else { - progressBar.visibility = View.INVISIBLE - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - model = activity?.run { - ViewModelProviders.of(this)[WalletViewModel::class.java] - } ?: throw Exception("Invalid Activity") - } - - override fun onResume() { - super.onResume() - Log.v("taler-wallet", "called onResume on PromptPayment") - triggerLoading() - } - - private fun fillOrderInfo(view: View, contractTerms: ContractTerms, totalFees: Amount?) { - val feesAmountView = view.findViewById<TextView>(R.id.order_fees_amount) - val amountView = view.findViewById<TextView>(R.id.order_amount) - val summaryView = view.findViewById<TextView>(R.id.order_summary) - summaryView.text = contractTerms.summary - val amount = contractTerms.amount - @SuppressLint("SetTextI18n") - amountView.text = "${amount.amount} ${amount.currency}" - val feesBox = view.findViewById<View>(R.id.order_fees_box) - if (totalFees != null) { - @SuppressLint("SetTextI18n") - feesAmountView.text = "${totalFees.amount} ${totalFees.currency}" - feesBox.visibility = View.VISIBLE - } else { - feesBox.visibility = View.INVISIBLE - } - - } - - - private fun showPayStatus(view: View, payStatus: PayStatus) { - val promptPaymentDetails = view.findViewById<View>(R.id.prompt_payment_details) - val balanceInsufficientWarning = view.findViewById<View>(R.id.balance_insufficient_warning) - val errorTextView = view.findViewById<TextView>(R.id.pay_error_text) - val confirmPaymentButton = view.findViewById<Button>(R.id.button_confirm_payment) - when (payStatus) { - is PayStatus.Prepared -> { - fillOrderInfo(view, payStatus.contractTerms, payStatus.totalFees) - promptPaymentDetails.visibility = View.VISIBLE - balanceInsufficientWarning.visibility = View.GONE - errorTextView.visibility = View.GONE - confirmPaymentButton.isEnabled = true - - confirmPaymentButton.setOnClickListener { - model.confirmPay(payStatus.proposalId) - confirmPaymentButton.isEnabled = false - } - } - is PayStatus.InsufficientBalance -> { - fillOrderInfo(view, payStatus.contractTerms, null) - promptPaymentDetails.visibility = View.VISIBLE - balanceInsufficientWarning.visibility = View.VISIBLE - errorTextView.visibility = View.GONE - confirmPaymentButton.isEnabled = false - } - is PayStatus.Success -> { - model.payStatus.value = PayStatus.None() - activity!!.findNavController(R.id.nav_host_fragment).navigate(R.id.action_promptPayment_to_paymentSuccessful) - } - is PayStatus.AlreadyPaid -> { - activity!!.findNavController(R.id.nav_host_fragment).navigate(R.id.action_promptPayment_to_alreadyPaid) - model.payStatus.value = PayStatus.None() - } - is PayStatus.Error -> { - errorTextView.visibility = View.VISIBLE - errorTextView.text = "Error: ${payStatus.error}" - } - is PayStatus.None -> { - // No payment active. - } - is PayStatus.Loading -> { - // Wait until loaded ... - } - else -> { - val bar = Snackbar.make(view , "Bug: Unexpected result", Snackbar.LENGTH_SHORT) - bar.show() - } - } - } - - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - - val view = inflater.inflate(R.layout.fragment_prompt_payment, container, false) - fragmentView = view - - val promptPaymentDetails = view.findViewById<View>(R.id.prompt_payment_details) - // Set invisible until data is loaded. - promptPaymentDetails.visibility = View.INVISIBLE - - val abortPaymentButton = view.findViewById<Button>(R.id.button_abort_payment) - - val errorTextView = view.findViewById<TextView>(R.id.pay_error_text) - errorTextView.visibility = View.GONE - - abortPaymentButton.setOnClickListener { - when (val ps = model.payStatus.value) { - is PayStatus.Prepared -> { - model.abortProposal(ps.proposalId) - } - } - model.payStatus.value = PayStatus.None() - requireActivity().findNavController(R.id.nav_host_fragment).navigateUp() - } - - triggerLoading() - - model.payStatus.observe(viewLifecycleOwner, Observer { - triggerLoading() - showPayStatus(view, it) - }) - 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 c4c96d6..1a31b86 100644 --- a/app/src/main/java/net/taler/wallet/ShowBalance.kt +++ b/app/src/main/java/net/taler/wallet/ShowBalance.kt @@ -19,7 +19,12 @@ package net.taler.wallet import android.os.Bundle import android.util.Log -import android.view.* +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import android.widget.Button import android.widget.LinearLayout import android.widget.TextView @@ -320,7 +325,7 @@ class ShowBalance : Fragment(), PendingOperationClickListener { if (proposalId == "") { return } - model.abortProposal(proposalId) + model.paymentManager.abortProposal(proposalId) } } } diff --git a/app/src/main/java/net/taler/wallet/WalletViewModel.kt b/app/src/main/java/net/taler/wallet/WalletViewModel.kt index ad41e77..af1037e 100644 --- a/app/src/main/java/net/taler/wallet/WalletViewModel.kt +++ b/app/src/main/java/net/taler/wallet/WalletViewModel.kt @@ -18,7 +18,11 @@ package net.taler.wallet import android.app.Application import android.util.Log -import androidx.lifecycle.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData +import androidx.lifecycle.switchMap import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule @@ -31,6 +35,7 @@ import kotlinx.coroutines.flow.onStart 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 org.json.JSONObject const val TAG = "taler-wallet" @@ -41,23 +46,6 @@ data class BalanceEntry(val available: Amount, val pendingIncoming: Amount) data class WalletBalances(val initialized: Boolean, val byCurrency: List<BalanceEntry>) -data class ContractTerms(val summary: String, val amount: Amount) - -open class PayStatus { - class None : PayStatus() - class Loading : PayStatus() - data class Prepared( - val contractTerms: ContractTerms, - val proposalId: String, - val totalFees: Amount - ) : PayStatus() - - data class InsufficientBalance(val contractTerms: ContractTerms) : PayStatus() - data class AlreadyPaid(val contractTerms: ContractTerms) : PayStatus() - data class Error(val error: String) : PayStatus() - class Success : PayStatus() -} - open class WithdrawStatus { class None : WithdrawStatus() data class Loading(val talerWithdrawUri: String) : WithdrawStatus() @@ -100,10 +88,6 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { value = WalletBalances(false, listOf()) } - val payStatus = MutableLiveData<PayStatus>().apply { - value = PayStatus.None() - } - val withdrawStatus = MutableLiveData<WithdrawStatus>().apply { value = WithdrawStatus.None() } @@ -124,13 +108,16 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { .asLiveData(Dispatchers.IO) } + val showProgressBar = MutableLiveData<Boolean>() + private var activeGetBalance = 0 private var activeGetPending = 0 - private var currentPayRequestId = 0 private var currentWithdrawRequestId = 0 private val walletBackendApi = WalletBackendApi(app) + val paymentManager = PaymentManager(walletBackendApi) + private val mapper = ObjectMapper() .registerModule(KotlinModule()) .configure(FAIL_ON_UNKNOWN_PROPERTIES, false) @@ -241,83 +228,6 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { } } - - fun preparePay(url: String) { - val args = JSONObject() - args.put("url", url) - - this.currentPayRequestId += 1 - val myPayRequestId = this.currentPayRequestId - this.payStatus.value = PayStatus.Loading() - - walletBackendApi.sendRequest("preparePay", args) { isError, result -> - if (isError) { - Log.v(TAG, "got preparePay error result") - payStatus.value = PayStatus.Error(result.toString(0)) - return@sendRequest - } - Log.v(TAG, "got preparePay result") - if (myPayRequestId != this.currentPayRequestId) { - Log.v(TAG, "preparePay result was for old request") - return@sendRequest - } - val status = result.getString("status") - var contractTerms: ContractTerms? = null - var proposalId: String? = null - var totalFees: Amount? = null - if (result.has("proposalId")) { - proposalId = result.getString("proposalId") - } - if (result.has("contractTermsRaw")) { - val ctJson = JSONObject(result.getString("contractTermsRaw")) - val amount = Amount.fromString(ctJson.getString("amount")) - val summary = ctJson.getString("summary") - contractTerms = ContractTerms(summary, amount) - } - if (result.has("totalFees")) { - totalFees = Amount.fromJson(result.getJSONObject("totalFees")) - } - val res = when (status) { - "payment-possible" -> PayStatus.Prepared( - contractTerms!!, - proposalId!!, - totalFees!! - ) - "paid" -> PayStatus.AlreadyPaid(contractTerms!!) - "insufficient-balance" -> PayStatus.InsufficientBalance( - contractTerms!! - ) - "error" -> PayStatus.Error("got some error") - else -> PayStatus.Error("unknown status") - } - payStatus.postValue(res) - } - } - - fun abortProposal(proposalId: String) { - val args = JSONObject() - args.put("proposalId", proposalId) - - Log.i(TAG, "aborting proposal") - - walletBackendApi.sendRequest("abortProposal", args) { isError, _ -> - if (isError) { - Log.e(TAG, "received error response to abortProposal") - return@sendRequest - } - payStatus.postValue(PayStatus.None()) - } - } - - fun confirmPay(proposalId: String) { - val args = JSONObject() - args.put("proposalId", proposalId) - - walletBackendApi.sendRequest("confirmPay", args) { isError, result -> - payStatus.postValue(PayStatus.Success()) - } - } - fun dangerouslyReset() { walletBackendApi.sendRequest("reset", null) testWithdrawalInProgress.value = false diff --git a/app/src/main/java/net/taler/wallet/AlreadyPaid.kt b/app/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt index 65785b9..62d90a5 100644 --- a/app/src/main/java/net/taler/wallet/AlreadyPaid.kt +++ b/app/src/main/java/net/taler/wallet/payment/AlreadyPaidFragment.kt @@ -14,33 +14,34 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -package net.taler.wallet +package net.taler.wallet.payment 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_already_paid.* +import net.taler.wallet.R /** * Display the message that the user already paid for the order * that the merchant is proposing. */ -class AlreadyPaid : Fragment() { +class AlreadyPaidFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - // Inflate the layout for this fragment - val view = inflater.inflate(R.layout.fragment_already_paid, container, false) - view.findViewById<Button>(R.id.button_success_back).setOnClickListener { - activity!!.findNavController(R.id.nav_host_fragment).navigateUp() - } - return view + return inflater.inflate(R.layout.fragment_already_paid, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + button_success_back.setOnClickListener { + findNavController().navigateUp() + } + } } diff --git a/app/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/app/src/main/java/net/taler/wallet/payment/PaymentManager.kt new file mode 100644 index 0000000..2e40250 --- /dev/null +++ b/app/src/main/java/net/taler/wallet/payment/PaymentManager.kt @@ -0,0 +1,108 @@ +package net.taler.wallet.payment + +import android.util.Log +import androidx.annotation.UiThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import net.taler.wallet.Amount +import net.taler.wallet.TAG +import net.taler.wallet.backend.WalletBackendApi +import org.json.JSONObject + +class PaymentManager(private val walletBackendApi: WalletBackendApi) { + + private val mPayStatus = MutableLiveData<PayStatus>(PayStatus.None) + internal val payStatus: LiveData<PayStatus> = mPayStatus + + private var currentPayRequestId = 0 + + @UiThread + fun preparePay(url: String) { + mPayStatus.value = PayStatus.Loading + + val args = JSONObject(mapOf("url" to url)) + + currentPayRequestId += 1 + val payRequestId = currentPayRequestId + + walletBackendApi.sendRequest("preparePay", args) { isError, result -> + when { + isError -> { + Log.v(TAG, "got preparePay error result") + mPayStatus.value = PayStatus.Error(result.toString()) + } + payRequestId != this.currentPayRequestId -> { + Log.v(TAG, "preparePay result was for old request") + } + else -> { + val status = result.getString("status") + mPayStatus.postValue(getPayStatusUpdate(status, result)) + } + } + } + } + + private fun getPayStatusUpdate(status: String, json: JSONObject) = when (status) { + "payment-possible" -> PayStatus.Prepared( + contractTerms = getContractTerms(json), + proposalId = json.getString("proposalId"), + totalFees = Amount.fromJson(json.getJSONObject("totalFees")) + ) + "paid" -> PayStatus.AlreadyPaid(getContractTerms(json)) + "insufficient-balance" -> PayStatus.InsufficientBalance(getContractTerms(json)) + "error" -> PayStatus.Error("got some error") + else -> PayStatus.Error("unknown status") + } + + private fun getContractTerms(json: JSONObject): ContractTerms { + val ctJson = JSONObject(json.getString("contractTermsRaw")) + val amount = Amount.fromString(ctJson.getString("amount")) + val summary = ctJson.getString("summary") + return ContractTerms(summary, amount) + } + + fun confirmPay(proposalId: String) { + val args = JSONObject(mapOf("proposalId" to proposalId)) + + walletBackendApi.sendRequest("confirmPay", args) { _, _ -> + mPayStatus.postValue(PayStatus.Success) + } + } + + fun abortProposal(proposalId: String) { + val args = JSONObject(mapOf("proposalId" to proposalId)) + + Log.i(TAG, "aborting proposal") + + walletBackendApi.sendRequest("abortProposal", args) { isError, _ -> + if (isError) { + Log.e(TAG, "received error response to abortProposal") + return@sendRequest + } + mPayStatus.postValue(PayStatus.None) + } + } + + @UiThread + fun resetPayStatus() { + mPayStatus.value = PayStatus.None + } + +} + +sealed class PayStatus { + object None : PayStatus() + object Loading : PayStatus() + data class Prepared( + val contractTerms: ContractTerms, + val proposalId: String, + val totalFees: Amount + ) : PayStatus() + + data class InsufficientBalance(val contractTerms: ContractTerms) : PayStatus() + data class AlreadyPaid(val contractTerms: ContractTerms) : PayStatus() + data class Error(val error: String) : PayStatus() + object Success : PayStatus() +} + +data class ContractTerms(val summary: String, val amount: Amount) diff --git a/app/src/main/java/net/taler/wallet/PaymentSuccessful.kt b/app/src/main/java/net/taler/wallet/payment/PaymentSuccessfulFragment.kt index 6332c39..608abfd 100644 --- a/app/src/main/java/net/taler/wallet/PaymentSuccessful.kt +++ b/app/src/main/java/net/taler/wallet/payment/PaymentSuccessfulFragment.kt @@ -14,33 +14,33 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - -package net.taler.wallet +package net.taler.wallet.payment import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Button import androidx.fragment.app.Fragment -import androidx.navigation.findNavController - +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.fragment_payment_successful.* +import net.taler.wallet.R /** * Fragment that shows the success message for a payment. - * */ -class PaymentSuccessful : Fragment() { +class PaymentSuccessfulFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - // Inflate the layout for this fragment - val view = inflater.inflate(R.layout.fragment_payment_successful, container, false) - view.findViewById<Button>(R.id.button_success_back).setOnClickListener { - activity!!.findNavController(R.id.nav_host_fragment).navigateUp() + return inflater.inflate(R.layout.fragment_payment_successful, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + button_success_back.setOnClickListener { + findNavController().navigateUp() } - return view } + } diff --git a/app/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt b/app/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt new file mode 100644 index 0000000..4b4bf01 --- /dev/null +++ b/app/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt @@ -0,0 +1,136 @@ +/* + This file is part of GNU Taler + (C) 2019 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.payment + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.observe +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.fragment_prompt_payment.* +import net.taler.wallet.Amount +import net.taler.wallet.R +import net.taler.wallet.WalletViewModel + +/** + * Show a payment and ask the user to accept/decline. + */ +class PromptPaymentFragment : Fragment() { + + private val model: WalletViewModel by activityViewModels() + private val paymentManager by lazy { model.paymentManager } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_prompt_payment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + paymentManager.payStatus.observe(viewLifecycleOwner, this::onPaymentStatusChanged) + + button_abort_payment.setOnClickListener { + when (val ps = paymentManager.payStatus.value) { + is PayStatus.Prepared -> { + paymentManager.abortProposal(ps.proposalId) + } + } + paymentManager.resetPayStatus() + findNavController().navigateUp() + } + } + + private fun showLoading(show: Boolean) { + model.showProgressBar.value = show + } + + private fun onPaymentStatusChanged(payStatus: PayStatus) { + when (payStatus) { + is PayStatus.Prepared -> { + showLoading(false) + showOrder(payStatus.contractTerms, payStatus.totalFees) + button_confirm_payment.isEnabled = true + button_confirm_payment.setOnClickListener { + showLoading(true) + paymentManager.confirmPay(payStatus.proposalId) + button_confirm_payment.isEnabled = false + } + } + is PayStatus.InsufficientBalance -> { + showLoading(false) + showOrder(payStatus.contractTerms, null) + error_text.setText(R.string.payment_balance_insufficient) + fadeInView(error_text) + } + is PayStatus.Success -> { + showLoading(false) + paymentManager.resetPayStatus() + findNavController().navigate(R.id.action_promptPayment_to_paymentSuccessful) + } + is PayStatus.AlreadyPaid -> { + showLoading(false) + paymentManager.resetPayStatus() + findNavController().navigate(R.id.action_promptPayment_to_alreadyPaid) + } + is PayStatus.Error -> { + showLoading(false) + error_text.text = getString(R.string.payment_error, payStatus.error) + fadeInView(error_text) + } + is PayStatus.None -> { + // No payment active. + showLoading(false) + } + is PayStatus.Loading -> { + // Wait until loaded ... + showLoading(true) + } + } + } + + private fun showOrder(contractTerms: ContractTerms, totalFees: Amount?) { + order_summary.text = contractTerms.summary + val amount = contractTerms.amount + @SuppressLint("SetTextI18n") + order_amount.text = "${amount.amount} ${amount.currency}" + if (totalFees != null && !totalFees.isZero()) { + val fee = "${totalFees.amount} ${totalFees.currency}" + order_fees_amount.text = getString(R.string.payment_fee, fee) + fadeInView(order_fees_amount) + } else { + order_fees_amount.visibility = INVISIBLE + } + fadeInView(order_summary_label) + fadeInView(order_summary) + fadeInView(order_amount_label) + fadeInView(order_amount) + } + + private fun fadeInView(v: View) { + v.alpha = 0f + v.visibility = VISIBLE + v.animate().alpha(1f).start() + } + +} diff --git a/app/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layout/app_bar_main.xml index 7bf5acd..51d3242 100644 --- a/app/src/main/res/layout/app_bar_main.xml +++ b/app/src/main/res/layout/app_bar_main.xml @@ -23,15 +23,18 @@ android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/AppTheme.PopupOverlay"/> + <me.zhanghai.android.materialprogressbar.MaterialProgressBar android:id="@+id/progress_bar" + style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal" android:layout_width="match_parent" android:layout_height="4dp" + android:layout_alignParentBottom="true" android:indeterminate="true" + android:visibility="invisible" app:mpb_progressStyle="horizontal" app:mpb_useIntrinsicPadding="false" - android:layout_alignParentBottom="true" - style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal"/> + tools:visibility="visible" /> </RelativeLayout> </com.google.android.material.appbar.AppBarLayout> diff --git a/app/src/main/res/layout/fragment_already_paid.xml b/app/src/main/res/layout/fragment_already_paid.xml index 69c949e..f2ff7e7 100644 --- a/app/src/main/res/layout/fragment_already_paid.xml +++ b/app/src/main/res/layout/fragment_already_paid.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="15dp" - tools:context=".PaymentSuccessful"> + tools:context=".payment.PaymentSuccessfulFragment"> <LinearLayout diff --git a/app/src/main/res/layout/fragment_payment_successful.xml b/app/src/main/res/layout/fragment_payment_successful.xml index 64ddad9..af8bed2 100644 --- a/app/src/main/res/layout/fragment_payment_successful.xml +++ b/app/src/main/res/layout/fragment_payment_successful.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="15dp" - tools:context=".PaymentSuccessful"> + tools:context=".payment.PaymentSuccessfulFragment"> <LinearLayout diff --git a/app/src/main/res/layout/fragment_prompt_payment.xml b/app/src/main/res/layout/fragment_prompt_payment.xml index 1febf9f..8bc7b07 100644 --- a/app/src/main/res/layout/fragment_prompt_payment.xml +++ b/app/src/main/res/layout/fragment_prompt_payment.xml @@ -1,122 +1,120 @@ <?xml version="1.0" encoding="utf-8"?> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_margin="15dp" - tools:context=".PromptPayment"> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".payment.PromptPaymentFragment"> <TextView - android:text="Error: (placeholder)" + android:id="@+id/error_text" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="15sp" - android:layout_gravity="center" - android:id="@+id/pay_error_text" android:textColor="@android:color/holo_red_dark"/> + android:layout_margin="@dimen/activity_horizontal_margin" + android:textColor="@android:color/holo_red_dark" + android:textSize="22sp" + android:visibility="invisible" + app:layout_constraintBottom_toTopOf="@+id/order_summary_label" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" + tools:text="@string/payment_balance_insufficient" + tools:visibility="visible" /> + <TextView + android:id="@+id/order_summary_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/activity_horizontal_margin" + android:text="@string/payment_label_order_summary" + android:visibility="invisible" + app:layout_constraintBottom_toTopOf="@+id/order_summary" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/error_text" + tools:visibility="visible" /> - <LinearLayout - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:id="@+id/prompt_payment_details"> - - <Space android:layout_width="match_parent" - android:layout_height="15dp" - android:layout_weight="1"/> - - <TextView - android:text="Order Summary" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:id="@+id/textView2"/> - - <TextView - android:text="One Cappuccino" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textSize="25sp" - android:layout_gravity="center" - android:id="@+id/order_summary"/> - - <Space android:layout_width="match_parent" - android:layout_height="25dp"/> - - - <TextView - android:text="Amount" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:id="@+id/textView3"/> - <TextView - android:text="10 TESTKUDOS" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textSize="25sp" - android:layout_gravity="center" - android:id="@+id/order_amount"/> - - <LinearLayout android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:layout_gravity="center" - android:id="@+id/order_fees_box"> - - <TextView - android:text="(plus additional " - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center"/> - - <TextView - android:text="0.5 TESTKUDOS" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:id="@+id/order_fees_amount"/> - - <TextView - android:text=" payment fees)" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - /> - - </LinearLayout> - - <Space android:layout_width="match_parent" - android:layout_height="15dp" - android:layout_weight="1"/> - - <TextView - android:text="Balance Insufficient!" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textSize="25sp" - android:layout_gravity="center" - android:id="@+id/balance_insufficient_warning" android:textColor="@android:color/holo_red_dark"/> - - - <Space android:layout_width="match_parent" - android:layout_height="15dp" - android:layout_weight="1"/> - - <LinearLayout android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal"> - <Button android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Abort Payment" - android:id="@+id/button_abort_payment"/> + <TextView + android:id="@+id/order_summary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="@dimen/activity_horizontal_margin" + android:layout_marginTop="16dp" + android:gravity="center_horizontal" + android:textAppearance="@style/TextAppearance.AppCompat.Headline" + android:textSize="25sp" + android:visibility="invisible" + app:layout_constraintBottom_toTopOf="@+id/order_amount_label" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/order_summary_label" + tools:text="2 x Cappuccino, 1 x Hot Meals, 1 x Dessert" + tools:visibility="visible" /> - <Space android:layout_width="15dp" android:layout_height="match_parent" - android:layout_weight="1"/> + <TextView + android:id="@+id/order_amount_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/activity_horizontal_margin" + android:text="@string/payment_label_amount" + android:visibility="invisible" + app:layout_constraintBottom_toTopOf="@+id/order_amount" + app:layout_constraintEnd_toEndOf="@+id/order_summary" + app:layout_constraintStart_toStartOf="@+id/order_summary" + app:layout_constraintTop_toBottomOf="@+id/order_summary" + tools:visibility="visible" /> - <Button android:layout_width="wrap_content" android:layout_height="wrap_content" - android:id="@+id/button_confirm_payment" - android:text="Confirm Payment"/> - </LinearLayout> + <TextView + android:id="@+id/order_amount" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/activity_horizontal_margin" + android:textAppearance="@style/TextAppearance.AppCompat.Headline" + android:visibility="invisible" + app:layout_constraintBottom_toTopOf="@+id/order_fees_amount" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/order_amount_label" + tools:text="10 TESTKUDOS" + tools:visibility="visible" /> - </LinearLayout> -</FrameLayout>
\ No newline at end of file + <TextView + android:id="@+id/order_fees_amount" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:visibility="invisible" + app:layout_constraintBottom_toTopOf="@+id/button_abort_payment" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/order_amount" + tools:text="@string/payment_fee" + tools:visibility="visible" /> + + <Button + android:id="@+id/button_abort_payment" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/activity_horizontal_margin" + android:text="@string/payment_button_abort" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/button_confirm_payment" + app:layout_constraintHorizontal_chainStyle="spread_inside" + app:layout_constraintStart_toStartOf="parent" /> + + <Button + android:id="@+id/button_confirm_payment" + style="@style/Widget.AppCompat.Button.Colored" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/activity_horizontal_margin" + android:enabled="false" + android:text="@string/payment_button_confirm" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/button_abort_payment" + tools:enabled="true" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_prompt_withdraw.xml b/app/src/main/res/layout/fragment_prompt_withdraw.xml index fc8046b..4cabfe8 100644 --- a/app/src/main/res/layout/fragment_prompt_withdraw.xml +++ b/app/src/main/res/layout/fragment_prompt_withdraw.xml @@ -21,7 +21,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:id="@+id/textView2"/> + android:id="@+id/order_summary_label"/> <TextView android:text="(amount)" @@ -47,7 +47,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:id="@+id/textView3"/> + android:id="@+id/order_amount_label"/> <TextView android:text="(exchange base url)" android:layout_width="wrap_content" diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index f958b9c..0fa1b4a 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -19,7 +19,7 @@ </fragment> <fragment android:id="@+id/promptPayment" - android:name="net.taler.wallet.PromptPayment" + android:name="net.taler.wallet.payment.PromptPaymentFragment" android:label="Review Payment" tools:layout="@layout/fragment_prompt_payment"> <action @@ -33,7 +33,7 @@ </fragment> <fragment android:id="@+id/paymentSuccessful" - android:name="net.taler.wallet.PaymentSuccessful" + android:name="net.taler.wallet.payment.PaymentSuccessfulFragment" android:label="Payment Successful" tools:layout="@layout/fragment_payment_successful" /> <fragment @@ -48,7 +48,7 @@ tools:layout="@layout/fragment_show_history" /> <fragment android:id="@+id/alreadyPaid" - android:name="net.taler.wallet.AlreadyPaid" + android:name="net.taler.wallet.payment.AlreadyPaidFragment" android:label="Already Paid" tools:layout="@layout/fragment_already_paid" /> <fragment diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 39fd3a6..8602063 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,6 +36,12 @@ <string name="history_reload">Reload History</string> <string name="history_empty">The wallet history is empty</string> - <!-- TODO: Remove or change this placeholder text --> - <string name="hello_blank_fragment">Hello blank fragment</string> + <string name="payment_fee">(plus an additional %s payment fee)</string> + <string name="payment_button_confirm">Confirm Payment</string> + <string name="payment_button_abort">Abort Payment</string> + <string name="payment_label_amount">Amount</string> + <string name="payment_label_order_summary">Order Summary</string> + <string name="payment_error">Error: %s</string> + <string name="payment_balance_insufficient">Balance Insufficient!</string> + </resources> |