taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

commit cb8050d841a2e20271c8c94a114b0c351f182f8a
parent 47d63c9d8d9c77e361c4a455d5bc761a4350fbf0
Author: Iván Ávalos <avalos@disroot.org>
Date:   Tue, 20 Aug 2024 16:06:57 +0200

[wallet] WIP: taxes

bug 0009081

Diffstat:
Mtaler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt | 7+++++++
Mwallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt | 30++++++++++++++++++++++++++----
Mwallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt | 34++++++++++++++++++++++++++++++++--
Mwallet/src/main/res/layout-w550dp/payment_bottom_bar.xml | 20++++++++++++++++++--
Mwallet/src/main/res/layout/list_item_product.xml | 27+++++++++++++++++++++------
Mwallet/src/main/res/layout/payment_bottom_bar.xml | 19+++++++++++++++++--
Mwallet/src/main/res/values/strings.xml | 2++
7 files changed, 123 insertions(+), 16 deletions(-)

diff --git a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt @@ -65,6 +65,7 @@ data class ContractProduct( override val location: String? = null, override val image: String? = null, val quantity: Int = 1, + val taxes: List<Tax>? = null, ) : Product() { val totalPrice: Amount? by lazy { price?.let { price * quantity } @@ -72,6 +73,12 @@ data class ContractProduct( } @Serializable +data class Tax( + val name: String, + val tax: Amount, +) + +@Serializable data class ContractMerchant( val name: String ) diff --git a/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt b/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt @@ -16,6 +16,7 @@ package net.taler.wallet.payment +import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory.decodeByteArray import android.util.Base64 @@ -26,9 +27,11 @@ import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.compose.ui.util.fastDistinctBy import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder +import net.taler.common.Amount import net.taler.common.ContractProduct import net.taler.wallet.R import net.taler.wallet.payment.ProductAdapter.ProductViewHolder @@ -41,6 +44,7 @@ internal class ProductAdapter(private val listener: ProductImageClickListener) : RecyclerView.Adapter<ProductViewHolder>() { private var items = emptyList<ContractProduct>() + private var taxesEqual = true override fun getItemCount() = items.size @@ -62,12 +66,16 @@ internal class ProductAdapter(private val listener: ProductImageClickListener) : diffResult.dispatchUpdatesTo(this) items = newItems + taxesEqual = newItems.distinctBy { it.taxes }.size > 1 } internal inner class ProductViewHolder(v: View) : ViewHolder(v) { + private val context: Context = v.context + private val quantity: TextView = v.findViewById(R.id.quantity) private val image: ImageView = v.findViewById(R.id.image) private val name: TextView = v.findViewById(R.id.name) + private val taxes: TextView = v.findViewById(R.id.taxes) private val price: TextView = v.findViewById(R.id.price) fun bind(product: ContractProduct) { @@ -87,10 +95,24 @@ internal class ProductAdapter(private val listener: ProductImageClickListener) : } } name.text = product.description - price.visibility = product.totalPrice?.let { - price.text = it.toString() - VISIBLE - } ?: GONE + + if (product.totalPrice != null) { + price.visibility = VISIBLE + price.text = product.totalPrice.toString() + } else { + price.visibility = GONE + } + + if (!taxesEqual && product.taxes != null) { + taxes.visibility = VISIBLE + taxes.text = product.taxes!!.filter { + !it.tax.isZero() + }.joinToString(separator = "\n") { + context.getString(R.string.payment_tax, it.name, it.tax) + } + } else { + taxes.visibility = GONE + } } } } diff --git a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt @@ -22,6 +22,7 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.View.GONE +import android.view.View.VISIBLE import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -33,6 +34,7 @@ import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import kotlinx.coroutines.launch import net.taler.common.Amount import net.taler.common.ContractTerms +import net.taler.common.Tax import net.taler.common.fadeIn import net.taler.common.fadeOut import net.taler.common.showError @@ -102,7 +104,8 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener { is PayStatus.Prepared -> { showLoading(false) val fees = payStatus.amountEffective - payStatus.amountRaw - showOrder(payStatus.contractTerms, payStatus.amountRaw, fees) + val taxes = compileTaxes(payStatus.contractTerms) + showOrder(payStatus.contractTerms, payStatus.amountRaw, fees, taxes) ui.bottom.confirmButton.isEnabled = true ui.bottom.confirmButton.setOnClickListener { model.showProgressBar.value = true @@ -153,17 +156,44 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener { } } - private fun showOrder(contractTerms: ContractTerms, amount: Amount, totalFees: Amount? = null) { + private fun compileTaxes(contractTerms: ContractTerms): List<Tax>? { + val distinct = contractTerms.products.distinctBy { it.taxes } + return if (distinct.size == 1) { + distinct.first().taxes + } else { + null + } + } + + private fun showOrder( + contractTerms: ContractTerms, + amount: Amount, + totalFees: Amount? = null, + taxes: List<Tax>? = null, + ) { ui.details.orderView.text = contractTerms.summary adapter.update(contractTerms.products) ui.details.productsList.fadeIn() ui.bottom.totalView.text = amount.toString() + if (totalFees != null && !totalFees.isZero()) { ui.bottom.feeView.text = getString(R.string.payment_fee, totalFees) ui.bottom.feeView.fadeIn() } else { ui.bottom.feeView.visibility = GONE } + + if (taxes != null) { + ui.bottom.feeView.visibility = VISIBLE + ui.bottom.feeView.text = taxes.filter { + !it.tax.isZero() + }.joinToString(separator = "\n") { + requireContext().getString(R.string.payment_tax, it.name, it.tax) + } + } else { + ui.bottom.feeView.visibility = GONE + } + ui.details.orderLabelView.fadeIn() ui.details.orderView.fadeIn() ui.bottom.totalLabelView.fadeIn() diff --git a/wallet/src/main/res/layout-w550dp/payment_bottom_bar.xml b/wallet/src/main/res/layout-w550dp/payment_bottom_bar.xml @@ -67,16 +67,32 @@ android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginEnd="16dp" - android:layout_marginBottom="8dp" android:visibility="gone" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/confirmButton" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toTopOf="@id/taxView" app:layout_constraintTop_toBottomOf="@+id/totalView" tools:text="@string/payment_fee" tools:visibility="visible" /> + <TextView + android:id="@+id/taxView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginEnd="16dp" + android:layout_marginBottom="8dp" + android:visibility="gone" + app:layout_constraintEnd_toStartOf="@+id/confirmButton" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/feeView" + app:layout_constraintBottom_toBottomOf="parent" + tools:text="@string/payment_tax" + tools:visibility="visible" /> + <Button android:id="@+id/confirmButton" android:layout_width="wrap_content" diff --git a/wallet/src/main/res/layout/list_item_product.xml b/wallet/src/main/res/layout/list_item_product.xml @@ -30,7 +30,6 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.0" tools:text="31" /> <ImageView @@ -55,21 +54,37 @@ android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/taxes" app:layout_constraintEnd_toStartOf="@+id/price" app:layout_constraintStart_toEndOf="@+id/image" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.0" tools:text="A product item that in some cases could have a very long name" /> <TextView - android:id="@+id/price" + android:id="@+id/taxes" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:paddingTop="3dp" + app:layout_constraintEnd_toStartOf="@id/price" + app:layout_constraintStart_toEndOf="@id/image" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.0" + android:textStyle="italic" + android:visibility="gone" + tools:visibility="visible" + tools:text="incl. 0.2€ VAT" /> + + <TextView + android:id="@+id/price" + android:layout_width="wrap_content" + android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.0" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + android:textStyle="bold" tools:text="23.42" /> + </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/wallet/src/main/res/layout/payment_bottom_bar.xml b/wallet/src/main/res/layout/payment_bottom_bar.xml @@ -71,7 +71,7 @@ android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:visibility="gone" - app:layout_constraintBottom_toTopOf="@+id/confirmButton" + app:layout_constraintBottom_toTopOf="@+id/taxView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toStartOf="parent" @@ -79,6 +79,21 @@ tools:text="@string/payment_fee" tools:visibility="visible" /> + <TextView + android:id="@+id/taxView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:visibility="gone" + app:layout_constraintBottom_toTopOf="@+id/confirmButton" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/feeView" + tools:text="@string/payment_tax" + tools:visibility="visible" /> + <Button android:id="@+id/confirmButton" android:layout_width="wrap_content" @@ -91,7 +106,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/feeView" + app:layout_constraintTop_toBottomOf="@+id/taxView" tools:enabled="true" /> <ProgressBar diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml @@ -188,6 +188,8 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="payment_pay_template_title">Customize your order</string> <string name="payment_pending">Payment not completed, it will be retried</string> <string name="payment_prompt_title">Review Payment</string> + <!-- including <amount> <tax name> --> + <string name="payment_tax">incl. %1$s %2$s</string> <string name="payment_template_error">Error creating order</string> <string name="payment_title">Payment</string>