aboutsummaryrefslogtreecommitdiff
path: root/wallet/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-07-01 18:05:51 -0300
committerSebastian <sebasjm@gmail.com>2022-07-01 18:06:32 -0300
commitc3c43726de9858f42fee9d4051ab3d3245b47099 (patch)
tree148c263dccacd278f063552fa8ffd4a990c84fad /wallet/src
parente05bfb78c4dafee6980f5b86196bca4ff302208c (diff)
downloadtaler-android-c3c43726de9858f42fee9d4051ab3d3245b47099.tar.gz
taler-android-c3c43726de9858f42fee9d4051ab3d3245b47099.tar.bz2
taler-android-c3c43726de9858f42fee9d4051ab3d3245b47099.zip
accept tips
Diffstat (limited to 'wallet/src')
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainActivity.kt5
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainViewModel.kt2
-rw-r--r--wallet/src/main/java/net/taler/wallet/tip/AlreadyAcceptedFragment.kt49
-rw-r--r--wallet/src/main/java/net/taler/wallet/tip/PromptTipFragment.kt160
-rw-r--r--wallet/src/main/java/net/taler/wallet/tip/TipManager.kt124
-rw-r--r--wallet/src/main/java/net/taler/wallet/tip/TipResponses.kt63
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt7
-rw-r--r--wallet/src/main/res/layout/fragment_already_accepted.xml52
-rw-r--r--wallet/src/main/res/layout/fragment_prompt_tip.xml257
-rw-r--r--wallet/src/main/res/navigation/nav_graph.xml24
-rw-r--r--wallet/src/main/res/values/strings.xml8
-rw-r--r--wallet/src/test/java/net/taler/wallet/tip/TipResponsesTest.kt93
12 files changed, 840 insertions, 4 deletions
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 <http://www.gnu.org/licenses/>
+ */
+
+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 <http://www.gnu.org/licenses/>
+ */
+
+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 <http://www.gnu.org/licenses/>
+ */
+
+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>(TipStatus.None)
+ internal val tipStatus: LiveData<TipStatus> = 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<Unit>("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 <http://www.gnu.org/licenses/>
+ */
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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/>
+ -->
+
+<LinearLayout 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"
+ android:layout_margin="15dp"
+ android:orientation="vertical"
+ tools:context=".tip.AlreadyAcceptedFragment">
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_gravity="center"
+ android:text="@string/tip_already_accepted"
+ android:textAlignment="center"
+ android:textColor="@android:color/holo_green_dark"
+ app:autoSizeTextType="uniform" />
+
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <Button
+ android:id="@+id/backButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/button_back" />
+
+</LinearLayout>
diff --git a/wallet/src/main/res/layout/fragment_prompt_tip.xml b/wallet/src/main/res/layout/fragment_prompt_tip.xml
new file mode 100644
index 0000000..d96ef60
--- /dev/null
+++ b/wallet/src/main/res/layout/fragment_prompt_tip.xml
@@ -0,0 +1,257 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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/>
+ -->
+
+<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=".tip.PromptTipFragment">
+
+ <TextView
+ android:id="@+id/introView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="8dp"
+ android:gravity="center"
+ android:text="@string/tip_total"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/effectiveAmountView"
+ 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:visibility="visible" />
+
+ <TextView
+ android:id="@+id/effectiveAmountView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:gravity="center"
+ android:textColor="@color/green"
+ android:textSize="24sp"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/chosenAmountLabel"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/introView"
+ tools:text="9.8 TESTKUDOS"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/chosenAmountLabel"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="16dp"
+ android:gravity="center"
+ android:text="@string/amount_chosen"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/chosenAmountView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/effectiveAmountView"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/chosenAmountView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="16dp"
+ android:gravity="center"
+ android:textSize="20sp"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/feeLabel"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/chosenAmountLabel"
+ tools:text="10 TESTKUDOS"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/feeLabel"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="16dp"
+ android:gravity="center"
+ android:text="@string/tip_fees"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/feeView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/chosenAmountView"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/feeView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="16dp"
+ android:gravity="center"
+ android:textColor="@color/red"
+ android:textSize="20sp"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/exchangeIntroView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/feeLabel"
+ tools:text="-0.2 TESTKUDOS"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/exchangeIntroView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="8dp"
+ android:gravity="center"
+ android:text="@string/tip_exchange"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/withdrawExchangeUrl"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/feeView"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/withdrawExchangeUrl"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginBottom="8dp"
+ android:layout_marginEnd="8dp"
+ android:gravity="center"
+ android:textSize="24sp"
+ android:visibility="invisible"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintBottom_toTopOf="@+id/merchantIntroView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/exchangeIntroView"
+ tools:text="demo.taler.net"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/merchantIntroView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="8dp"
+ android:gravity="center"
+ android:text="@string/tip_merchant_url"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/withdrawMerchantUrl"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/withdrawExchangeUrl"
+ tools:visibility="visible" />
+
+
+ <TextView
+ android:id="@+id/withdrawMerchantUrl"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="8dp"
+ android:gravity="center"
+ android:textSize="24sp"
+ android:visibility="invisible"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintBottom_toTopOf="@+id/withdrawCard"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.502"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/merchantIntroView"
+ tools:text="merchant.demo.taler.net"
+ tools:visibility="visible" />
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.google.android.material.card.MaterialCardView
+ android:id="@+id/withdrawCard"
+ style="@style/BottomCard"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:visibility="visible">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="8dp">
+
+ <Button
+ android:id="@+id/confirmWithdrawButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:backgroundTint="@color/green"
+ android:enabled="false"
+ android:text="@string/tip_button_confirm"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:enabled="true"
+ tools:text="@string/tip_button_confirm" />
+
+ <ProgressBar
+ android:id="@+id/confirmProgressBar"
+ style="?android:attr/progressBarStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/confirmWithdrawButton"
+ app:layout_constraintEnd_toEndOf="@+id/confirmWithdrawButton"
+ app:layout_constraintStart_toStartOf="@+id/confirmWithdrawButton"
+ app:layout_constraintTop_toTopOf="@+id/confirmWithdrawButton"
+ tools:visibility="visible" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ </com.google.android.material.card.MaterialCardView>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml
index 469a399..b3f96c5 100644
--- a/wallet/src/main/res/navigation/nav_graph.xml
+++ b/wallet/src/main/res/navigation/nav_graph.xml
@@ -34,6 +34,24 @@
<action
android:id="@+id/action_nav_main_to_nav_uri_input"
app:destination="@id/nav_uri_input" />
+ <action
+ android:id="@+id/action_nav_main_to_promptTip"
+ app:destination="@id/promptTip" />
+ </fragment>
+
+ <fragment
+ android:id="@+id/promptTip"
+ android:name="net.taler.wallet.tip.PromptTipFragment"
+ android:label="Review Tip"
+ tools:layout="@layout/fragment_prompt_tip">
+ <action
+ android:id="@+id/action_promptTip_to_nav_main"
+ app:destination="@id/nav_main"
+ app:popUpTo="@id/nav_main" />
+ <action
+ android:id="@+id/action_promptTip_to_alreadyAccepted"
+ app:destination="@id/alreadyAccepted"
+ app:popUpTo="@id/nav_main" />
</fragment>
<fragment
@@ -131,6 +149,12 @@
tools:layout="@layout/fragment_transaction_withdrawal" />
<fragment
+ android:id="@+id/alreadyAccepted"
+ android:name="net.taler.wallet.tip.AlreadyAcceptedFragment"
+ android:label="@string/tip_already_accepted"
+ tools:layout="@layout/fragment_already_accepted" />
+
+ <fragment
android:id="@+id/alreadyPaid"
android:name="net.taler.wallet.payment.AlreadyPaidFragment"
android:label="@string/payment_already_paid_title"
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
index 592070c..efaa7dd 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -187,6 +187,14 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="refund_success">Refund received: %s</string>
<string name="tip_title">Tip</string>
+ <string name="tip_already_accepted">This tip is already accepted.</string>
+ <string name="tip_total">Total</string>
+ <string name="tip_fees">Fee</string>
+ <string name="tip_exchange">Exchange</string>
+ <string name="tip_merchant_url">Merchant URL</string>
+ <string name="tip_button_confirm">Accept tip</string>
+ <string name="tip_received">Tip received</string>
+
<string name="wifi_disabled_error">Turn on Wi-Fi to get free Wi-Fi</string>
<string name="wifi_connect_error">Could not connect to free Wi-Fi: %s</string>
diff --git a/wallet/src/test/java/net/taler/wallet/tip/TipResponsesTest.kt b/wallet/src/test/java/net/taler/wallet/tip/TipResponsesTest.kt
new file mode 100644
index 0000000..9267f11
--- /dev/null
+++ b/wallet/src/test/java/net/taler/wallet/tip/TipResponsesTest.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.payment
+
+import kotlinx.serialization.json.Json
+import net.taler.common.Amount
+import net.taler.wallet.tip.ConfirmTipResult
+import net.taler.wallet.tip.PrepareTipResponse
+import org.junit.Test
+
+class TipResponsesTest {
+
+ private val json = Json {
+ ignoreUnknownKeys = true
+ }
+
+ @Test
+ fun testConfirmTipResult() {
+ val jsonStr = """
+ {
+ "type": "response",
+ "operation": "acceptTip",
+ "id": 47,
+ "result": {}
+ }
+ """.trimIndent()
+ val response = json.decodeFromString(ConfirmTipResult.serializer(), jsonStr)
+ response as ConfirmTipResult
+ assert(response != null)
+ }
+
+/*
+
+*/
+
+
+ @Test
+ fun testTipPossibleSerializer() {
+ val jsonStr = """
+ {
+ "accepted": false,
+ "tipAmountRaw": "ARS:2",
+ "exchangeBaseUrl": "http://exchange.taler:8081/",
+ "merchantBaseUrl": "http://merchant-backend.taler:9966/",
+ "expirationTimestamp": {
+ "t_s": 1688217455
+ },
+ "tipAmountEffective": "ARS:1.4",
+ "walletTipId": "SZH86ATJC4NZ427JHFVQ9M3S1TCQKVWSSZGSBW8MQ8VTVWD4M4GG"
+ }
+ """.trimIndent()
+ val response = json.decodeFromString(PrepareTipResponse.serializer(), jsonStr)
+ response as PrepareTipResponse.TipPossibleResponse
+ assert(response.walletTipId == "SZH86ATJC4NZ427JHFVQ9M3S1TCQKVWSSZGSBW8MQ8VTVWD4M4GG")
+ assert(response.tipAmountEffective == Amount(currency = "ARS", fraction = 40000000, value = 1))
+ }
+
+
+ @Test
+ fun testTipAcceptedSerializer() {
+ val jsonStr = """
+ {
+ "accepted": true,
+ "tipAmountRaw": "ARS:2",
+ "exchangeBaseUrl": "http://exchange.taler:8081/",
+ "merchantBaseUrl": "http://merchant-backend.taler:9966/",
+ "expirationTimestamp": {
+ "t_s": 1688217455
+ },
+ "tipAmountEffective": "ARS:1.4",
+ "walletTipId": "SZH86ATJC4NZ427JHFVQ9M3S1TCQKVWSSZGSBW8MQ8VTVWD4M4GG"
+ }
+ """.trimIndent()
+ val response = json.decodeFromString(PrepareTipResponse.serializer(), jsonStr)
+ assert(response is PrepareTipResponse.AlreadyAcceptedResponse)
+ assert((response as PrepareTipResponse.AlreadyAcceptedResponse).walletTipId == "SZH86ATJC4NZ427JHFVQ9M3S1TCQKVWSSZGSBW8MQ8VTVWD4M4GG")
+ }
+
+}