From c3c43726de9858f42fee9d4051ab3d3245b47099 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 1 Jul 2022 18:05:51 -0300 Subject: accept tips --- .../src/main/java/net/taler/wallet/MainActivity.kt | 5 + .../main/java/net/taler/wallet/MainViewModel.kt | 2 + .../taler/wallet/tip/AlreadyAcceptedFragment.kt | 49 ++++ .../java/net/taler/wallet/tip/PromptTipFragment.kt | 160 +++++++++++++ .../main/java/net/taler/wallet/tip/TipManager.kt | 124 ++++++++++ .../main/java/net/taler/wallet/tip/TipResponses.kt | 63 +++++ .../net/taler/wallet/transactions/Transactions.kt | 7 +- .../main/res/layout/fragment_already_accepted.xml | 52 +++++ wallet/src/main/res/layout/fragment_prompt_tip.xml | 257 +++++++++++++++++++++ wallet/src/main/res/navigation/nav_graph.xml | 24 ++ wallet/src/main/res/values/strings.xml | 8 + .../java/net/taler/wallet/tip/TipResponsesTest.kt | 93 ++++++++ 12 files changed, 840 insertions(+), 4 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/tip/AlreadyAcceptedFragment.kt create mode 100644 wallet/src/main/java/net/taler/wallet/tip/PromptTipFragment.kt create mode 100644 wallet/src/main/java/net/taler/wallet/tip/TipManager.kt create mode 100644 wallet/src/main/java/net/taler/wallet/tip/TipResponses.kt create mode 100644 wallet/src/main/res/layout/fragment_already_accepted.xml create mode 100644 wallet/src/main/res/layout/fragment_prompt_tip.xml create mode 100644 wallet/src/test/java/net/taler/wallet/tip/TipResponsesTest.kt diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt index 3b8be4f..40da9a2 100644 --- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt +++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt @@ -166,6 +166,11 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, nav.navigate(R.id.action_nav_main_to_promptPayment) model.paymentManager.preparePay(url) } + action.startsWith("tip/") -> { + Log.v(TAG, "navigating!") + nav.navigate(R.id.action_nav_main_to_promptTip) + model.tipManager.prepareTip(url) + } action.startsWith("withdraw/") -> { Log.v(TAG, "navigating!") // there's more than one entry point, so use global action diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt index 7bb6ad9..5041037 100644 --- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt +++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt @@ -36,6 +36,7 @@ import net.taler.wallet.exchanges.ExchangeManager import net.taler.wallet.payment.PaymentManager import net.taler.wallet.pending.PendingOperationsManager import net.taler.wallet.refund.RefundManager +import net.taler.wallet.tip.TipManager import net.taler.wallet.transactions.TransactionManager import net.taler.wallet.withdraw.WithdrawManager import org.json.JSONObject @@ -86,6 +87,7 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) { } val withdrawManager = WithdrawManager(api, viewModelScope) + val tipManager = TipManager(api, viewModelScope) val paymentManager = PaymentManager(api, viewModelScope) val pendingOperationsManager: PendingOperationsManager = PendingOperationsManager(api) val transactionManager: TransactionManager = TransactionManager(api, viewModelScope) diff --git a/wallet/src/main/java/net/taler/wallet/tip/AlreadyAcceptedFragment.kt b/wallet/src/main/java/net/taler/wallet/tip/AlreadyAcceptedFragment.kt new file mode 100644 index 0000000..d76b6a1 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/tip/AlreadyAcceptedFragment.kt @@ -0,0 +1,49 @@ +/* + * 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.tip + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import net.taler.wallet.databinding.FragmentAlreadyAcceptedBinding + +/** + * Display the message that the user already paid for the order + * that the merchant is proposing. + */ +class AlreadyAcceptedFragment : Fragment() { + + private lateinit var ui: FragmentAlreadyAcceptedBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + ui = FragmentAlreadyAcceptedBinding.inflate(inflater, container, false) + return ui.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + ui.backButton.setOnClickListener { + findNavController().navigateUp() + } + } + +} diff --git a/wallet/src/main/java/net/taler/wallet/tip/PromptTipFragment.kt b/wallet/src/main/java/net/taler/wallet/tip/PromptTipFragment.kt new file mode 100644 index 0000000..a5c504c --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/tip/PromptTipFragment.kt @@ -0,0 +1,160 @@ +/* + * 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.tip + +import android.graphics.Bitmap +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.snackbar.Snackbar.LENGTH_LONG +import net.taler.common.Amount +import net.taler.common.ContractTerms +import net.taler.common.fadeIn +import net.taler.common.fadeOut +import net.taler.wallet.MainViewModel +import net.taler.wallet.R +import net.taler.wallet.cleanExchange +import net.taler.wallet.databinding.FragmentPromptPaymentBinding +import net.taler.wallet.databinding.FragmentPromptTipBinding +import net.taler.wallet.withdraw.ExchangeSelection + +/** + * Show a tip and ask the user to accept/decline. + */ +class PromptTipFragment : Fragment() { + + private val model: MainViewModel by activityViewModels() + private val tipManager by lazy { model.tipManager } + + private lateinit var ui: FragmentPromptTipBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + ui = FragmentPromptTipBinding.inflate(inflater, container, false) + ui.introView.fadeIn() + return ui.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + tipManager.tipStatus.observe(viewLifecycleOwner, ::onPaymentStatusChanged) + + } + + override fun onDestroy() { + super.onDestroy() + if (!requireActivity().isChangingConfigurations) { + // tipManager.abortTip() + } + } + + private fun showLoading(show: Boolean) { + model.showProgressBar.value = show + if (show) { + ui.progressBar.fadeIn() + } else { + ui.progressBar.fadeOut() + } + } + + private fun onPaymentStatusChanged(payStatus: TipStatus?) { + when (payStatus) { + is TipStatus.Prepared -> { + showLoading(false) + showContent(payStatus.tipAmountRaw, payStatus.tipAmountEffective, payStatus.exchangeBaseUrl, payStatus.merchantBaseUrl) + //showOrder(payStatus.contractTerms, payStatus.amountRaw, fees) + ui.confirmWithdrawButton.isEnabled = true + ui.confirmWithdrawButton.setOnClickListener { + model.showProgressBar.value = true + tipManager.confirmTip( + payStatus.walletTipId, + payStatus.tipAmountRaw.currency + ) + ui.confirmWithdrawButton.fadeOut() + ui.progressBar.fadeIn() + } + } + is TipStatus.AlreadyAccepted -> { + showLoading(false) + tipManager.resetTipStatus() + findNavController().navigate(R.id.action_promptTip_to_alreadyAccepted) + } + is TipStatus.Success -> { + showLoading(false) + tipManager.resetTipStatus() + findNavController().navigate(R.id.action_promptTip_to_nav_main) + model.showTransactions(payStatus.currency) + Snackbar.make(requireView(), R.string.tip_received, LENGTH_LONG).show() + } + is TipStatus.Error -> { + showLoading(false) + ui.introView.text = getString(R.string.payment_error, payStatus.error) + ui.introView.fadeIn() + } + is TipStatus.None -> { + // No payment active. + showLoading(false) + } + is TipStatus.Loading -> { + // Wait until loaded ... + showLoading(true) + } + } + } + + private fun showContent( + amountRaw: Amount, + amountEffective: Amount, + exchange: String, + merchant: String, + ) { + model.showProgressBar.value = false + ui.progressBar.fadeOut() + + ui.introView.fadeIn() + ui.effectiveAmountView.text = amountEffective.toString() + ui.effectiveAmountView.fadeIn() + + ui.chosenAmountLabel.fadeIn() + ui.chosenAmountView.text = amountRaw.toString() + ui.chosenAmountView.fadeIn() + + ui.feeLabel.fadeIn() + ui.feeView.text = + getString(R.string.amount_negative, (amountRaw - amountEffective).toString()) + ui.feeView.fadeIn() + + ui.exchangeIntroView.fadeIn() + ui.withdrawExchangeUrl.text = cleanExchange(exchange) + ui.withdrawExchangeUrl.fadeIn() + + ui.merchantIntroView.fadeIn() + ui.withdrawMerchantUrl.text = cleanExchange(merchant) + ui.withdrawMerchantUrl.fadeIn() + + ui.withdrawCard.fadeIn() + } + +} diff --git a/wallet/src/main/java/net/taler/wallet/tip/TipManager.kt b/wallet/src/main/java/net/taler/wallet/tip/TipManager.kt new file mode 100644 index 0000000..855feb0 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/tip/TipManager.kt @@ -0,0 +1,124 @@ +/* + * 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.tip + +import android.util.Log +import androidx.annotation.UiThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import net.taler.common.Amount +import net.taler.common.Timestamp +import net.taler.wallet.TAG +import net.taler.wallet.backend.WalletBackendApi +import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.tip.PrepareTipResponse.TipPossibleResponse +import net.taler.wallet.tip.PrepareTipResponse.AlreadyAcceptedResponse + +sealed class TipStatus { + object None : TipStatus() + object Loading : TipStatus() + data class Prepared( + val walletTipId: String, + val merchantBaseUrl: String, + val exchangeBaseUrl: String, + val expirationTimestamp: Timestamp, + val tipAmountRaw: Amount, + val tipAmountEffective: Amount, + ) : TipStatus() + + data class AlreadyAccepted( + val walletTipId: String, + ) : TipStatus() + + // TODO bring user to fulfilment URI + data class Error(val error: String) : TipStatus() + data class Success(val currency: String) : TipStatus() +} + +class TipManager( + private val api: WalletBackendApi, + private val scope: CoroutineScope, +) { + + private val mTipStatus = MutableLiveData(TipStatus.None) + internal val tipStatus: LiveData = mTipStatus + + @UiThread + fun prepareTip(url: String) = scope.launch { + mTipStatus.value = TipStatus.Loading + api.request("prepareTip", PrepareTipResponse.serializer()) { + put("talerTipUri", url) + }.onError { + handleError("prepareTip", it) + }.onSuccess { response -> + mTipStatus.value = when (response) { + is TipPossibleResponse -> response.toTipStatusPrepared() + is AlreadyAcceptedResponse -> TipStatus.AlreadyAccepted( + response.walletTipId + ) + } + } + } + + fun confirmTip(tipId: String, currency: String) = scope.launch { + api.request("acceptTip", ConfirmTipResult.serializer()) { + put("walletTipId", tipId) + }.onError { + handleError("acceptTip", it) + }.onSuccess { + mTipStatus.postValue(TipStatus.Success(currency)) + } + } + +/* + @UiThread + fun abortTip() { + val ps = tipStatus.value + if (ps is TipStatus.Prepared) { + abortProposal(ps.walletTipId) + } + resetTipStatus() + } +*/ + +/* + internal fun abortProposal(proposalId: String) = scope.launch { + Log.i(TAG, "aborting proposal") + api.request("abortProposal") { + put("proposalId", proposalId) + }.onError { + Log.e(TAG, "received error response to abortProposal") + handleError("abortProposal", it) + }.onSuccess { + mTipStatus.postValue(TipStatus.None) + } + } +*/ + + @UiThread + fun resetTipStatus() { + mTipStatus.value = TipStatus.None + } + + private fun handleError(operation: String, error: TalerErrorInfo) { + Log.e(TAG, "got $operation error result $error") + mTipStatus.value = TipStatus.Error(error.userFacingMsg) + } + +} diff --git a/wallet/src/main/java/net/taler/wallet/tip/TipResponses.kt b/wallet/src/main/java/net/taler/wallet/tip/TipResponses.kt new file mode 100644 index 0000000..aa2da15 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/tip/TipResponses.kt @@ -0,0 +1,63 @@ +/* + * 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.tip + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator +import net.taler.common.Amount +import net.taler.common.ContractTerms +import net.taler.common.Timestamp +import net.taler.wallet.backend.TalerErrorInfo + +@OptIn(ExperimentalSerializationApi::class) +@Serializable +@JsonClassDiscriminator("accepted") +sealed class PrepareTipResponse { + + @Serializable + @SerialName("false") + data class TipPossibleResponse( + val walletTipId: String, + val merchantBaseUrl: String, + val exchangeBaseUrl: String, + val expirationTimestamp: Timestamp, + val tipAmountRaw: Amount, + val tipAmountEffective: Amount, + ) : PrepareTipResponse() { + fun toTipStatusPrepared() = TipStatus.Prepared( + walletTipId = walletTipId, + merchantBaseUrl = merchantBaseUrl, + exchangeBaseUrl = exchangeBaseUrl, + expirationTimestamp = expirationTimestamp, + tipAmountEffective = tipAmountEffective, + tipAmountRaw = tipAmountRaw + ) + } + + @Serializable + @SerialName("true") + data class AlreadyAcceptedResponse( + val walletTipId: String, + ) : PrepareTipResponse() +} + +@Serializable +class ConfirmTipResult { + +} diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt index cca370e..ca01501 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -212,9 +212,8 @@ class TransactionTip( override val transactionId: String, override val timestamp: Timestamp, override val pending: Boolean, - // TODO status: TipStatus, - val exchangeBaseUrl: String, - val merchant: ContractMerchant, + val frozen: Boolean, + val merchantBaseUrl: String, override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount @@ -225,7 +224,7 @@ class TransactionTip( @Transient override val amountType = AmountType.Positive override fun getTitle(context: Context): String { - return context.getString(R.string.transaction_tip_from, merchant.name) + return context.getString(R.string.transaction_tip_from, merchantBaseUrl) } override val generalTitleRes = R.string.tip_title diff --git a/wallet/src/main/res/layout/fragment_already_accepted.xml b/wallet/src/main/res/layout/fragment_already_accepted.xml new file mode 100644 index 0000000..b1a7bb1 --- /dev/null +++ b/wallet/src/main/res/layout/fragment_already_accepted.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + +