gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-taler-android] branch master updated: Let all apps use the same A


From: gnunet
Subject: [taler-taler-android] branch master updated: Let all apps use the same Amount class
Date: Fri, 20 Mar 2020 19:44:22 +0100

This is an automated email from the git hooks/post-receive script.

torsten-grote pushed a commit to branch master
in repository taler-android.

The following commit(s) were added to refs/heads/master by this push:
     new db8b714  Let all apps use the same Amount class
db8b714 is described below

commit db8b71418b766258a7a4bda91e496b1b03cb28cd
Author: Torsten Grote <address@hidden>
AuthorDate: Fri Mar 20 15:43:23 2020 -0300

    Let all apps use the same Amount class
    
    The wallet now also uses taler-kotlin-common
---
 build.gradle                                       |   1 +
 .../main/java/net/taler/cashier/BalanceFragment.kt |  10 +-
 .../main/java/net/taler/cashier/MainViewModel.kt   |  25 +-
 .../net/taler/cashier/withdraw/WithdrawManager.kt  |   4 +-
 .../net/taler/merchantpos/config/MerchantConfig.kt |   5 +-
 .../taler/merchantpos/history/HistoryManager.kt    |   2 +-
 .../merchantpos/history/MerchantHistoryFragment.kt |   4 +-
 .../taler/merchantpos/history/RefundFragment.kt    |   6 +-
 .../taler/merchantpos/history/RefundUriFragment.kt |   4 +-
 .../java/net/taler/merchantpos/order/LiveOrder.kt  |  10 +-
 .../main/java/net/taler/merchantpos/order/Order.kt |  12 +-
 .../net/taler/merchantpos/order/OrderManager.kt    |   9 +-
 .../taler/merchantpos/order/OrderStateFragment.kt  |   7 +-
 .../taler/merchantpos/order/ProductsFragment.kt    |   2 +-
 .../taler/merchantpos/payment/PaymentManager.kt    |   2 +-
 .../merchantpos/payment/ProcessPaymentFragment.kt  |   4 +-
 merchant-terminal/src/main/res/values/strings.xml  |   2 +-
 taler-kotlin-common/build.gradle                   |   5 +
 .../src/main/java/net/taler/common/Amount.kt       | 192 ++++++++++++--
 .../src/main/java/net/taler/common/AndroidUtils.kt |   7 +-
 .../main/java/net/taler/common/ContractTerms.kt    |  19 +-
 .../src/main/java/net/taler/common/SignedAmount.kt |  33 ++-
 .../src/test/java/net/taler/common/AmountTest.kt   | 290 +++++++++++++++++++++
 wallet/.gitlab-ci.yml                              |   2 +-
 wallet/build.gradle                                |   9 +-
 wallet/src/main/java/net/taler/wallet/Amount.kt    | 141 ----------
 .../main/java/net/taler/wallet/BalanceFragment.kt  |   9 +-
 .../src/main/java/net/taler/wallet/MainActivity.kt |   1 -
 wallet/src/main/java/net/taler/wallet/Utils.kt     |  40 ---
 .../main/java/net/taler/wallet/WalletViewModel.kt  |   5 +-
 .../java/net/taler/wallet/history/HistoryEvent.kt  |  41 ++-
 .../net/taler/wallet/history/ReserveTransaction.kt |   1 +
 .../taler/wallet/history/WalletHistoryAdapter.kt   |  28 +-
 .../java/net/taler/wallet/payment/ContractTerms.kt |  56 ----
 .../net/taler/wallet/payment/PaymentManager.kt     |   5 +-
 .../wallet/payment/PaymentSuccessfulFragment.kt    |   2 +-
 .../net/taler/wallet/payment/ProductAdapter.kt     |   3 +-
 .../taler/wallet/payment/PromptPaymentFragment.kt  |  14 +-
 .../wallet/withdraw/PromptWithdrawFragment.kt      |   8 +-
 .../wallet/withdraw/ReviewExchangeTosFragment.kt   |   4 +-
 .../net/taler/wallet/withdraw/WithdrawManager.kt   |   4 +-
 wallet/src/main/res/values/strings.xml             |   2 +-
 42 files changed, 612 insertions(+), 418 deletions(-)

diff --git a/build.gradle b/build.gradle
index 4c2476d..b1f47dd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -17,6 +17,7 @@ allprojects {
     repositories {
         google()
         jcenter()
+        maven { url 'https://jitpack.io' }
     }
 }
 
diff --git a/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt 
b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt
index 2178a78..fffb21b 100644
--- a/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt
+++ b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt
@@ -16,7 +16,6 @@
 
 package net.taler.cashier
 
-import android.annotation.SuppressLint
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.Menu
@@ -34,14 +33,14 @@ import kotlinx.android.synthetic.main.fragment_balance.*
 import 
net.taler.cashier.BalanceFragmentDirections.Companion.actionBalanceFragmentToTransactionFragment
 import net.taler.cashier.withdraw.LastTransaction
 import net.taler.cashier.withdraw.WithdrawStatus
-import net.taler.common.Amount
+import net.taler.common.SignedAmount
 import net.taler.common.fadeIn
 import net.taler.common.fadeOut
 
 sealed class BalanceResult {
     object Error : BalanceResult()
     object Offline : BalanceResult()
-    class Success(val amount: Amount) : BalanceResult()
+    class Success(val amount: SignedAmount) : BalanceResult()
 }
 
 class BalanceFragment : Fragment() {
@@ -121,7 +120,7 @@ class BalanceFragment : Fragment() {
         else -> super.onOptionsItemSelected(item)
     }
 
-    private fun onBalanceUpdated(amount: Amount?, isOffline: Boolean = false) {
+    private fun onBalanceUpdated(amount: SignedAmount?, isOffline: Boolean = 
false) {
         val uiList = listOf(
             introView,
             button5, button10, button20, button50,
@@ -132,8 +131,7 @@ class BalanceFragment : Fragment() {
                 getString(if (isOffline) R.string.balance_offline else 
R.string.balance_error)
             uiList.forEach { it.fadeOut() }
         } else {
-            @SuppressLint("SetTextI18n")
-            balanceView.text = "${amount.amount} ${amount.currency}"
+            balanceView.text = amount.toString()
             uiList.forEach { it.fadeIn() }
         }
         progressBar.fadeOut()
diff --git a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt 
b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
index 6cd12ff..2b2d5f7 100644
--- a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
+++ b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
@@ -34,7 +34,8 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import net.taler.cashier.HttpHelper.makeJsonGetRequest
 import net.taler.cashier.withdraw.WithdrawManager
-import net.taler.common.Amount.Companion.fromStringSigned
+import net.taler.common.AmountParserException
+import net.taler.common.SignedAmount
 import net.taler.common.isOnline
 
 private val TAG = MainViewModel::class.java.simpleName
@@ -90,12 +91,16 @@ class MainViewModel(private val app: Application) : 
AndroidViewModel(app) {
             val result = when (val response = makeJsonGetRequest(url, config)) 
{
                 is HttpJsonResult.Success -> {
                     val balance = response.json.getString("balance")
-                    val amount = fromStringSigned(balance)!!
-                    mCurrency.postValue(amount.currency)
-                    prefs.edit().putString(PREF_KEY_CURRENCY, 
amount.currency).apply()
-                    // save config
-                    saveConfig(config)
-                    ConfigResult(true)
+                    try {
+                        val amount = SignedAmount.fromJSONString(balance)
+                        mCurrency.postValue(amount.amount.currency)
+                        prefs.edit().putString(PREF_KEY_CURRENCY, 
amount.amount.currency).apply()
+                        // save config
+                        saveConfig(config)
+                        ConfigResult(true)
+                    } catch (e: AmountParserException) {
+                        ConfigResult(false)
+                    }
                 }
                 is HttpJsonResult.Error -> {
                     val authError = response.statusCode == 401
@@ -124,7 +129,11 @@ class MainViewModel(private val app: Application) : 
AndroidViewModel(app) {
         val result = when (val response = makeJsonGetRequest(url, config)) {
             is HttpJsonResult.Success -> {
                 val balance = response.json.getString("balance")
-                fromStringSigned(balance)?.let { BalanceResult.Success(it) } 
?: BalanceResult.Error
+                try {
+                    BalanceResult.Success(SignedAmount.fromJSONString(balance))
+                } catch (e: AmountParserException) {
+                    BalanceResult.Error
+                }
             }
             is HttpJsonResult.Error -> {
                 if (app.isOnline()) BalanceResult.Error
diff --git 
a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt 
b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
index bfc82ce..88df6b7 100644
--- a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
@@ -75,9 +75,7 @@ class WithdrawManager(
     fun hasSufficientBalance(amount: Int): Boolean {
         val balanceResult = viewModel.balance.value
         if (balanceResult !is BalanceResult.Success) return false
-        val balanceStr = balanceResult.amount.amount
-        val balanceDouble = balanceStr.toDouble()
-        return amount <= balanceDouble
+        return balanceResult.amount.positive && amount <= 
balanceResult.amount.amount.value
     }
 
     @UiThread
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
index 8141f0f..0e707d3 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
@@ -68,14 +68,15 @@ data class ConfigProduct(
     override val productId: String?,
     override val description: String,
     override val descriptionI18n: Map<String, String>?,
-    override val price: String,
+    override val price: Amount,
     override val location: String?,
     override val image: String?,
     val categories: List<Int>,
     @JsonIgnore
     val quantity: Int = 0
 ) : Product() {
-    val priceAsDouble by lazy { Amount.fromString(price).amount.toDouble() }
+    @get:JsonIgnore
+    val totalPrice by lazy { price * quantity }
 
     fun toContractProduct() = ContractProduct(
         productId = productId,
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
index fc3f93a..3aaf3a4 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
@@ -42,7 +42,7 @@ data class HistoryItem(
     val timestamp: Timestamp
 ) {
     @get:JsonIgnore
-    val amount: Amount by lazy { Amount.fromString(amountStr) }
+    val amount: Amount by lazy { Amount.fromJSONString(amountStr) }
 
     @get:JsonIgnore
     val time = timestamp.ms
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
index afa925d..1099eda 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
@@ -16,7 +16,6 @@
 
 package net.taler.merchantpos.history
 
-import android.annotation.SuppressLint
 import android.os.Bundle
 import android.util.Log
 import android.view.LayoutInflater
@@ -148,8 +147,7 @@ private class HistoryItemAdapter(private val listener: 
RefundClickListener) :
         fun bind(item: HistoryItem) {
             orderSummaryView.text = item.summary
             val amount = item.amount
-            @SuppressLint("SetTextI18n")
-            orderAmountView.text = "${amount.amount} ${amount.currency}"
+            orderAmountView.text = amount.toString()
             orderIdView.text = v.context.getString(R.string.history_ref_no, 
item.orderId)
             orderTimeView.text = item.time.toRelativeTime(v.context)
             refundButton.setOnClickListener { listener.onRefundClicked(item) }
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
index aa2489a..609eadd 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
@@ -52,7 +52,7 @@ class RefundFragment : Fragment() {
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         val item = refundManager.toBeRefunded ?: throw IllegalStateException()
-        amountInputView.setText(item.amount.amount)
+        amountInputView.setText(item.amount.toString())
         currencyView.text = item.amount.currency
         abortButton.setOnClickListener { findNavController().navigateUp() }
         refundButton.setOnClickListener { onRefundButtonClicked(item) }
@@ -64,8 +64,8 @@ class RefundFragment : Fragment() {
 
     private fun onRefundButtonClicked(item: HistoryItem) {
         val inputAmount = amountInputView.text.toString().toDouble()
-        if (inputAmount > item.amount.amount.toDouble()) {
-            amountView.error = getString(R.string.refund_error_max_amount, 
item.amount.amount)
+        if (inputAmount > item.amountStr.toDouble()) {  // TODO real Amount 
comparision
+            amountView.error = getString(R.string.refund_error_max_amount, 
item.amountStr)
             return
         }
         if (inputAmount <= 0.0) {
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
index 6e5b96d..1bc4002 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
@@ -16,7 +16,6 @@
 
 package net.taler.merchantpos.history
 
-import android.annotation.SuppressLint
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
@@ -53,8 +52,7 @@ class RefundUriFragment : Fragment() {
             if (hasNfc(requireContext())) R.string.refund_intro_nfc else 
R.string.refund_intro
         refundIntroView.setText(introRes)
 
-        @SuppressLint("SetTextI18n")
-        refundAmountView.text = "${result.amount} 
${result.item.amount.currency}"
+        refundAmountView.text = result.amount.toString()
 
         refundRefView.text =
             getString(R.string.refund_order_ref, result.item.orderId, 
result.reason)
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt
index 847326b..f8d465b 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt
@@ -20,6 +20,7 @@ import androidx.annotation.UiThread
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Transformations
+import net.taler.common.Amount
 import net.taler.common.CombinedLiveData
 import net.taler.merchantpos.config.Category
 import net.taler.merchantpos.config.ConfigProduct
@@ -31,7 +32,7 @@ internal enum class RestartState { ENABLED, DISABLED, UNDO }
 
 internal interface LiveOrder {
     val order: LiveData<Order>
-    val orderTotal: LiveData<Double>
+    val orderTotal: LiveData<Amount>
     val restartState: LiveData<RestartState>
     val modifyOrderAllowed: LiveData<Boolean>
     val lastAddedProduct: ConfigProduct?
@@ -44,12 +45,13 @@ internal interface LiveOrder {
 
 internal class MutableLiveOrder(
     val id: Int,
+    private val currency: String,
     private val productsByCategory: HashMap<Category, ArrayList<ConfigProduct>>
 ) : LiveOrder {
     private val availableCategories: Map<Int, Category>
         get() = productsByCategory.keys.map { it.id to it }.toMap()
-    override val order: MutableLiveData<Order> = MutableLiveData(Order(id, 
availableCategories))
-    override val orderTotal: LiveData<Double> = Transformations.map(order) { 
it.total }
+    override val order: MutableLiveData<Order> = MutableLiveData(Order(id, 
currency, availableCategories))
+    override val orderTotal: LiveData<Amount> = Transformations.map(order) { 
it.total }
     override val restartState = MutableLiveData(DISABLED)
     private val selectedOrderLine = MutableLiveData<ConfigProduct>()
     override val selectedProductKey: String?
@@ -86,7 +88,7 @@ internal class MutableLiveOrder(
             undoOrder = null
         } else {
             undoOrder = order.value
-            order.value = Order(id, availableCategories)
+            order.value = Order(id, currency, availableCategories)
             restartState.value = UNDO
         }
     }
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt
index 5954e63..ff6e6b7 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt
@@ -16,10 +16,11 @@
 
 package net.taler.merchantpos.order
 
+import net.taler.common.Amount
 import net.taler.merchantpos.config.Category
 import net.taler.merchantpos.config.ConfigProduct
 
-data class Order(val id: Int, val availableCategories: Map<Int, Category>) {
+data class Order(val id: Int, val currency: String, val availableCategories: 
Map<Int, Category>) {
     val products = ArrayList<ConfigProduct>()
     val title: String = id.toString()
     val summary: String
@@ -29,17 +30,14 @@ data class Order(val id: Int, val availableCategories: 
Map<Int, Category>) {
                 "$quantity x ${category.localizedName}"
             }.joinToString()
         }
-    val total: Double
+    val total: Amount
         get() {
-            var total = 0.0
+            var total = Amount.zero(currency)
             products.forEach { product ->
-                val price = product.priceAsDouble
-                total += price * product.quantity
+                total += product.price * product.quantity
             }
             return total
         }
-    val totalAsString: String
-        get() = String.format("%.2f", total)
 
     operator fun plus(product: ConfigProduct): Order {
         val i = products.indexOf(product)
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt
index a30c264..ff2be48 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt
@@ -24,7 +24,6 @@ import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Transformations.map
 import com.fasterxml.jackson.core.type.TypeReference
 import com.fasterxml.jackson.databind.ObjectMapper
-import net.taler.common.Amount.Companion.fromString
 import net.taler.merchantpos.R
 import net.taler.merchantpos.config.Category
 import net.taler.merchantpos.config.ConfigProduct
@@ -41,6 +40,7 @@ class OrderManager(
         val TAG = OrderManager::class.java.simpleName
     }
 
+    private lateinit var currency: String
     private var orderCounter: Int = 0
     private val mCurrentOrderId = MutableLiveData<Int>()
     internal val currentOrderId: LiveData<Int> = mCurrentOrderId
@@ -75,7 +75,7 @@ class OrderManager(
         // group products by categories
         productsByCategory.clear()
         products.forEach { product ->
-            val productCurrency = fromString(product.price).currency
+            val productCurrency = product.price.currency
             if (productCurrency != currency) {
                 Log.e(TAG, "Product $product has currency $productCurrency, 
$currency expected")
                 return context.getString(
@@ -98,12 +98,13 @@ class OrderManager(
             }
         }
         return if (productsByCategory.size > 0) {
+            this.currency = currency
             mCategories.postValue(categories)
             mProducts.postValue(productsByCategory[categories[0]])
             // Initialize first empty order, note this won't work when 
updating config mid-flight
             if (orders.isEmpty()) {
                 val id = orderCounter++
-                orders[id] = MutableLiveOrder(id, productsByCategory)
+                orders[id] = MutableLiveOrder(id, currency, productsByCategory)
                 mCurrentOrderId.postValue(id)
             }
             null // success, no error string
@@ -129,7 +130,7 @@ class OrderManager(
         }
         if (nextId == null) {
             nextId = orderCounter++
-            orders[nextId] = MutableLiveOrder(nextId, productsByCategory)
+            orders[nextId] = MutableLiveOrder(nextId, currency, 
productsByCategory)
         }
         val currentOrder = order(currentId)
         if (currentOrder.isEmpty()) orders.remove(currentId)
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
index a90334b..f792d7a 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
@@ -95,12 +95,11 @@ class OrderStateFragment : Fragment() {
             onOrderChanged(order, tracker)
         })
         liveOrder.orderTotal.observe(viewLifecycleOwner, Observer { orderTotal 
->
-            if (orderTotal == 0.0) {
+            if (orderTotal.isZero()) {
                 totalView.fadeOut()
                 totalView.text = null
             } else {
-                val currency = viewModel.configManager.merchantConfig?.currency
-                totalView.text = getString(R.string.order_total, orderTotal, 
currency)
+                totalView.text = getString(R.string.order_total, orderTotal)
                 totalView.fadeIn()
             }
         })
@@ -184,7 +183,7 @@ private class OrderAdapter : Adapter<OrderViewHolder>() {
             v.isActivated = selected
             quantity.text = product.quantity.toString()
             name.text = product.localizedDescription
-            price.text = String.format("%.2f", product.priceAsDouble * 
product.quantity)
+            price.text = product.totalPrice.amountStr
         }
     }
 
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
index d4da73f..00eb509 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
@@ -104,7 +104,7 @@ private class ProductAdapter(
 
         fun bind(product: ConfigProduct) {
             name.text = product.localizedDescription
-            price.text = product.priceAsDouble.toString()
+            price.text = product.price.amountStr
             v.setOnClickListener { listener.onProductSelected(product) }
         }
     }
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
index 4cfb069..f83370e 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
@@ -71,7 +71,7 @@ class PaymentManager(
         val merchantConfig = configManager.merchantConfig!!
 
         val currency = merchantConfig.currency!!
-        val amount = "$currency:${order.totalAsString}"
+        val amount = order.total.toJSONString()
         val summary = order.summary
         val summaryI18n = order.summaryI18n
 
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
index 1d61894..9c9457c 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
@@ -16,7 +16,6 @@
 
 package net.taler.merchantpos.payment
 
-import android.annotation.SuppressLint
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
@@ -73,8 +72,7 @@ class ProcessPaymentFragment : Fragment() {
             return
         }
         payIntroView.fadeIn()
-        @SuppressLint("SetTextI18n")
-        amountView.text = "${payment.order.totalAsString} ${payment.currency}"
+        amountView.text = payment.order.total.toString()
         payment.orderId?.let {
             orderRefView.text = getString(R.string.payment_order_ref, it)
             orderRefView.fadeIn()
diff --git a/merchant-terminal/src/main/res/values/strings.xml 
b/merchant-terminal/src/main/res/values/strings.xml
index ae82f96..863ae6f 100644
--- a/merchant-terminal/src/main/res/values/strings.xml
+++ b/merchant-terminal/src/main/res/values/strings.xml
@@ -9,7 +9,7 @@
 
     <string name="order_label_title">Order #%s</string>
     <!-- The first placeholder is the amount and the second the currency -->
-    <string name="order_total">Total: %1$.2f %2$s</string>
+    <string name="order_total">Total: %s</string>
     <string name="order_restart">Restart</string>
     <string name="order_undo">Undo</string>
     <string name="order_previous">Prev</string>
diff --git a/taler-kotlin-common/build.gradle b/taler-kotlin-common/build.gradle
index 1d45a54..1c53839 100644
--- a/taler-kotlin-common/build.gradle
+++ b/taler-kotlin-common/build.gradle
@@ -60,4 +60,9 @@ dependencies {
 
     // JSON parsing and serialization
     implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2"
+
+    lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha'
+
+    testImplementation 'junit:junit:4.13'
+    testImplementation 'org.json:json:20190722'
 }
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt 
b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
index 0389db1..48bd643 100644
--- a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
+++ b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
@@ -16,47 +16,185 @@
 
 package net.taler.common
 
+import android.annotation.SuppressLint
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.JsonMappingException
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
 import org.json.JSONObject
+import java.lang.Math.floorDiv
+import kotlin.math.pow
+import kotlin.math.roundToInt
 
-data class Amount(val currency: String, val amount: String) {
+class AmountDeserializer : StdDeserializer<Amount>(Amount::class.java) {
+    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): 
Amount {
+        val node = p.codec.readValue(p, String::class.java)
+        try {
+            return Amount.fromJSONString(node)
+        } catch (e: AmountParserException) {
+            throw JsonMappingException(p, "Error parsing Amount", e)
+        }
+    }
+}
+
+class AmountParserException(msg: String? = null, cause: Throwable? = null) : 
Exception(msg, cause)
+class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : 
Exception(msg, cause)
+
+@JsonDeserialize(using = AmountDeserializer::class)
+data class Amount(
+    /**
+     * name of the currency using either a three-character ISO 4217 currency 
code,
+     * or a regional currency identifier starting with a "*" followed by at 
most 10 characters.
+     * ISO 4217 exponents in the name are not supported,
+     * although the "fraction" is corresponds to an ISO 4217 exponent of 6.
+     */
+    val currency: String,
+
+    /**
+     * The integer part may be at most 2^52.
+     * Note that "1" here would correspond to 1 EUR or 1 USD, depending on 
currency, not 1 cent.
+     */
+    val value: Long,
+
+    /**
+     * Unsigned 32 bit fractional value to be added to value representing
+     * an additional currency fraction, in units of one hundred millionth 
(1e-8)
+     * of the base currency value.  For example, a fraction
+     * of 50_000_000 would correspond to 50 cents.
+     */
+    val fraction: Int
+) {
 
     companion object {
-        private const val FRACTIONAL_BASE = 1e8
-        private val SIGNED_REGEX = Regex("""([+\-])(\w+):([0-9.]+)""")
 
-        fun fromString(strAmount: String): Amount {
-            val components = strAmount.split(":")
-            return Amount(components[0], components[1])
+        private const val FRACTIONAL_BASE: Int = 100000000 // 1e8
+
+        @Suppress("unused")
+        private val REGEX = 
Regex("""^[-_*A-Za-z0-9]{1,12}:([0-9]+)\.?([0-9]+)?$""")
+        private val REGEX_CURRENCY = Regex("""^[-_*A-Za-z0-9]{1,12}$""")
+        private val MAX_VALUE = 2.0.pow(52)
+        private const val MAX_FRACTION_LENGTH = 8
+        private const val MAX_FRACTION = 99_999_999
+
+        @Throws(AmountParserException::class)
+        @SuppressLint("CheckedExceptions")
+        fun zero(currency: String): Amount {
+            return Amount(checkCurrency(currency), 0, 0)
+        }
+
+        @Throws(AmountParserException::class)
+        @SuppressLint("CheckedExceptions")
+        fun fromJSONString(str: String): Amount {
+            val split = str.split(":")
+            if (split.size != 2) throw AmountParserException("Invalid Amount 
Format")
+            // currency
+            val currency = checkCurrency(split[0])
+            // value
+            val valueSplit = split[1].split(".")
+            val value = checkValue(valueSplit[0].toLongOrNull())
+            // fraction
+            val fraction: Int = if (valueSplit.size > 1) {
+                val fractionStr = valueSplit[1]
+                if (fractionStr.length > MAX_FRACTION_LENGTH)
+                    throw AmountParserException("Fraction $fractionStr too 
long")
+                val fraction = "0.$fractionStr".toDoubleOrNull()
+                    ?.times(FRACTIONAL_BASE)
+                    ?.roundToInt()
+                checkFraction(fraction)
+            } else 0
+            return Amount(currency, value, fraction)
+        }
+
+        @Throws(AmountParserException::class)
+        @SuppressLint("CheckedExceptions")
+        fun fromJsonObject(json: JSONObject): Amount {
+            val currency = checkCurrency(json.optString("currency"))
+            val value = checkValue(json.optString("value").toLongOrNull())
+            val fraction = 
checkFraction(json.optString("fraction").toIntOrNull())
+            return Amount(currency, value, fraction)
+        }
+
+        @Throws(AmountParserException::class)
+        private fun checkCurrency(currency: String): String {
+            if (!REGEX_CURRENCY.matches(currency))
+                throw AmountParserException("Invalid currency: $currency")
+            return currency
+        }
+
+        @Throws(AmountParserException::class)
+        private fun checkValue(value: Long?): Long {
+            if (value == null || value > MAX_VALUE)
+                throw AmountParserException("Value $value greater than 
$MAX_VALUE")
+            return value
         }
 
-        fun fromStringSigned(strAmount: String): Amount? {
-            val groups = SIGNED_REGEX.matchEntire(strAmount)?.groupValues ?: 
emptyList()
-            if (groups.size < 4) return null
-            var amount = groups[3].toDoubleOrNull() ?: return null
-            if (groups[1] == "-") amount *= -1
-            val currency = groups[2]
-            val amountStr = amount.toString()
-            // only display as many digits as required to precisely render the 
balance
-            return Amount(currency, amountStr.removeSuffix(".0"))
+        @Throws(AmountParserException::class)
+        private fun checkFraction(fraction: Int?): Int {
+            if (fraction == null || fraction > MAX_FRACTION)
+                throw AmountParserException("Fraction $fraction greater than 
$MAX_FRACTION")
+            return fraction
+        }
+
+    }
+
+    val amountStr: String
+        get() = if (fraction == 0) "$value" else {
+            var f = fraction
+            var fractionStr = ""
+            while (f > 0) {
+                fractionStr += f / (FRACTIONAL_BASE / 10)
+                f = (f * 10) % FRACTIONAL_BASE
+            }
+            "$value.$fractionStr"
         }
 
-        fun fromJson(jsonAmount: JSONObject): Amount {
-            val amountCurrency = jsonAmount.getString("currency")
-            val amountValue = jsonAmount.getString("value")
-            val amountFraction = jsonAmount.getString("fraction")
-            val amountIntValue = Integer.parseInt(amountValue)
-            val amountIntFraction = Integer.parseInt(amountFraction)
-            return Amount(
-                amountCurrency,
-                (amountIntValue + amountIntFraction / 
FRACTIONAL_BASE).toString()
-            )
+    @Throws(AmountOverflowException::class)
+    operator fun plus(other: Amount): Amount {
+        check(currency == other.currency) { "Can only subtract from same 
currency" }
+        val resultValue = value + other.value + floorDiv(fraction + 
other.fraction, FRACTIONAL_BASE)
+        if (resultValue > MAX_VALUE)
+            throw AmountOverflowException()
+        val resultFraction = (fraction + other.fraction) % FRACTIONAL_BASE
+        return Amount(currency, resultValue, resultFraction)
+    }
+
+    @Throws(AmountOverflowException::class)
+    operator fun times(factor: Int): Amount {
+        var result = this
+        for (i in 1 until factor) result += this
+        return result
+    }
+
+    @Throws(AmountOverflowException::class)
+    operator fun minus(other: Amount): Amount {
+        check(currency == other.currency) { "Can only subtract from same 
currency" }
+        var resultValue = value
+        var resultFraction = fraction
+        if (resultFraction < other.fraction) {
+            if (resultValue < 1L)
+                throw AmountOverflowException()
+            resultValue--
+            resultFraction += FRACTIONAL_BASE
         }
+        check(resultFraction >= other.fraction)
+        resultFraction -= other.fraction
+        if (resultValue < other.value)
+            throw AmountOverflowException()
+        resultValue -= other.value
+        return Amount(currency, resultValue, resultFraction)
     }
 
     fun isZero(): Boolean {
-        return amount.toDouble() == 0.0
+        return value == 0L && fraction == 0
     }
 
-    override fun toString() = "$amount $currency"
+    fun toJSONString(): String {
+        return "$currency:$amountStr"
+    }
+
+    override fun toString(): String {
+        return "$amountStr $currency"
+    }
 
 }
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt 
b/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt
index fc04da5..5bc5721 100644
--- a/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt
+++ b/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt
@@ -21,7 +21,6 @@ import android.content.Context.CONNECTIVITY_SERVICE
 import android.net.ConnectivityManager
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
 import android.os.Build.VERSION.SDK_INT
-import android.text.format.DateUtils
 import android.text.format.DateUtils.DAY_IN_MILLIS
 import android.text.format.DateUtils.FORMAT_ABBREV_MONTH
 import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE
@@ -29,6 +28,8 @@ import android.text.format.DateUtils.FORMAT_NO_YEAR
 import android.text.format.DateUtils.FORMAT_SHOW_DATE
 import android.text.format.DateUtils.FORMAT_SHOW_TIME
 import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import android.text.format.DateUtils.formatDateTime
+import android.text.format.DateUtils.getRelativeTimeSpanString
 import android.view.View
 import android.view.View.INVISIBLE
 import android.view.View.VISIBLE
@@ -78,6 +79,6 @@ fun Long.toRelativeTime(context: Context): CharSequence {
     val now = System.currentTimeMillis()
     return if (now - this > DAY_IN_MILLIS * 2) {
         val flags = FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or 
FORMAT_ABBREV_MONTH or FORMAT_NO_YEAR
-        DateUtils.formatDateTime(context, this, flags)
-    } else DateUtils.getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, 
FORMAT_ABBREV_RELATIVE)
+        formatDateTime(context, this, flags)
+    } else getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, 
FORMAT_ABBREV_RELATIVE)
 }
diff --git 
a/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt 
b/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt
index 1e70b6f..cd417ef 100644
--- a/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt
+++ b/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt
@@ -18,12 +18,20 @@ package net.taler.common
 
 import androidx.annotation.RequiresApi
 import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
 import com.fasterxml.jackson.annotation.JsonInclude
 import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY
 import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL
 import com.fasterxml.jackson.annotation.JsonProperty
 import net.taler.common.TalerUtils.getLocalizedString
 
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class ContractTerms(
+    val summary: String,
+    val products: List<ContractProduct>,
+    val amount: Amount
+)
+
 @JsonInclude(NON_NULL)
 abstract class Product {
     @get:JsonProperty("product_id")
@@ -32,7 +40,7 @@ abstract class Product {
 
     @get:JsonProperty("description_i18n")
     abstract val descriptionI18n: Map<String, String>?
-    abstract val price: String
+    abstract val price: Amount
 
     @get:JsonProperty("delivery_location")
     abstract val location: String?
@@ -48,11 +56,16 @@ data class ContractProduct(
     override val productId: String?,
     override val description: String,
     override val descriptionI18n: Map<String, String>?,
-    override val price: String,
+    override val price: Amount,
     override val location: String?,
     override val image: String?,
     val quantity: Int
-) : Product()
+) : Product() {
+    @get:JsonIgnore
+    val totalPrice: Amount by lazy {
+        price * quantity
+    }
+}
 
 @JsonInclude(NON_EMPTY)
 class Timestamp(
diff --git a/wallet/src/test/java/net/taler/wallet/ExampleUnitTest.kt 
b/taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt
similarity index 51%
rename from wallet/src/test/java/net/taler/wallet/ExampleUnitTest.kt
rename to taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt
index de74f68..03a0d6e 100644
--- a/wallet/src/test/java/net/taler/wallet/ExampleUnitTest.kt
+++ b/taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt
@@ -14,20 +14,27 @@
  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-package net.taler.wallet
+package net.taler.common
 
-import org.junit.Test
+import android.annotation.SuppressLint
 
-import org.junit.Assert.*
+data class SignedAmount(
+    val positive: Boolean,
+    val amount: Amount
+) {
 
-/**
- * Example local unit test, which will execute on the development machine 
(host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
-    @Test
-    fun addition_isCorrect() {
-        assertEquals(4, 2 + 2)
+    companion object {
+        @Throws(AmountParserException::class)
+        @SuppressLint("CheckedExceptions")
+        fun fromJSONString(str: String): SignedAmount = when (str.substring(0, 
1)) {
+            "-" -> SignedAmount(false, Amount.fromJSONString(str.substring(1)))
+            "+" -> SignedAmount(true, Amount.fromJSONString(str.substring(1)))
+            else -> SignedAmount(true, Amount.fromJSONString(str))
+        }
     }
-}
+
+    override fun toString(): String {
+        return if (positive) "$amount" else "-$amount"
+    }
+
+}
\ No newline at end of file
diff --git a/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt 
b/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt
new file mode 100644
index 0000000..c09da3c
--- /dev/null
+++ b/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt
@@ -0,0 +1,290 @@
+/*
+ * 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.common
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import com.fasterxml.jackson.module.kotlin.readValue
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+
+class AmountTest {
+
+    @Test
+    fun `test fromJSONString() works`() {
+        var str = "TESTKUDOS:23.42"
+        var amount = Amount.fromJSONString(str)
+        assertEquals(str, amount.toJSONString())
+        assertEquals("TESTKUDOS", amount.currency)
+        assertEquals(23, amount.value)
+        assertEquals((0.42 * 1e8).toInt(), amount.fraction)
+        assertEquals("23.42 TESTKUDOS", amount.toString())
+
+        str = "EUR:500000000.00000001"
+        amount = Amount.fromJSONString(str)
+        assertEquals(str, amount.toJSONString())
+        assertEquals("EUR", amount.currency)
+        assertEquals(500000000, amount.value)
+        assertEquals(1, amount.fraction)
+        assertEquals("500000000.00000001 EUR", amount.toString())
+
+        str = "EUR:1500000000.00000003"
+        amount = Amount.fromJSONString(str)
+        assertEquals(str, amount.toJSONString())
+        assertEquals("EUR", amount.currency)
+        assertEquals(1500000000, amount.value)
+        assertEquals(3, amount.fraction)
+        assertEquals("1500000000.00000003 EUR", amount.toString())
+    }
+
+    @Test
+    fun `test fromJSONString() accepts max values, rejects above`() {
+        val maxValue = 4503599627370496
+        val str = "TESTKUDOS123:$maxValue.99999999"
+        val amount = Amount.fromJSONString(str)
+        assertEquals(str, amount.toJSONString())
+        assertEquals("TESTKUDOS123", amount.currency)
+        assertEquals(maxValue, amount.value)
+        assertEquals("$maxValue.99999999 TESTKUDOS123", amount.toString())
+
+        // longer currency not accepted
+        assertThrows<AmountParserException>("longer currency was accepted") {
+            Amount.fromJSONString("TESTKUDOS1234:$maxValue.99999999")
+        }
+
+        // max value + 1 not accepted
+        assertThrows<AmountParserException>("max value + 1 was accepted") {
+            Amount.fromJSONString("TESTKUDOS123:${maxValue + 1}.99999999")
+        }
+
+        // max fraction + 1 not accepted
+        assertThrows<AmountParserException>("max fraction + 1 was accepted") {
+            Amount.fromJSONString("TESTKUDOS123:$maxValue.999999990")
+        }
+    }
+
+    @Test
+    fun `test JSON deserialization()`() {
+        val mapper = ObjectMapper().registerModule(KotlinModule())
+        var str = "TESTKUDOS:23.42"
+        var amount: Amount = mapper.readValue("\"$str\"")
+        assertEquals(str, amount.toJSONString())
+        assertEquals("TESTKUDOS", amount.currency)
+        assertEquals(23, amount.value)
+        assertEquals((0.42 * 1e8).toInt(), amount.fraction)
+        assertEquals("23.42 TESTKUDOS", amount.toString())
+
+        str = "EUR:500000000.00000001"
+        amount = mapper.readValue("\"$str\"")
+        assertEquals(str, amount.toJSONString())
+        assertEquals("EUR", amount.currency)
+        assertEquals(500000000, amount.value)
+        assertEquals(1, amount.fraction)
+        assertEquals("500000000.00000001 EUR", amount.toString())
+
+        str = "EUR:1500000000.00000003"
+        amount = mapper.readValue("\"$str\"")
+        assertEquals(str, amount.toJSONString())
+        assertEquals("EUR", amount.currency)
+        assertEquals(1500000000, amount.value)
+        assertEquals(3, amount.fraction)
+        assertEquals("1500000000.00000003 EUR", amount.toString())
+    }
+
+    @Test
+    fun `test fromJSONString() rejections`() {
+        assertThrows<AmountParserException> {
+            Amount.fromJSONString("TESTKUDOS:0,5")
+        }
+        assertThrows<AmountParserException> {
+            Amount.fromJSONString("+TESTKUDOS:0.5")
+        }
+        assertThrows<AmountParserException> {
+            Amount.fromJSONString("0.5")
+        }
+        assertThrows<AmountParserException> {
+            Amount.fromJSONString(":0.5")
+        }
+        assertThrows<AmountParserException> {
+            Amount.fromJSONString("EUR::0.5")
+        }
+        assertThrows<AmountParserException> {
+            Amount.fromJSONString("EUR:.5")
+        }
+    }
+
+    @Test
+    fun `test fromJsonObject() works`() {
+        val map = mapOf(
+            "currency" to "TESTKUDOS",
+            "value" to "23",
+            "fraction" to "42000000"
+        )
+
+        val amount = Amount.fromJsonObject(JSONObject(map))
+        assertEquals("TESTKUDOS:23.42", amount.toJSONString())
+        assertEquals("TESTKUDOS", amount.currency)
+        assertEquals(23, amount.value)
+        assertEquals(42000000, amount.fraction)
+        assertEquals("23.42 TESTKUDOS", amount.toString())
+    }
+
+    @Test
+    fun `test fromJsonObject() accepts max values, rejects above`() {
+        val maxValue = 4503599627370496
+        val maxFraction = 99999999
+        var map = mapOf(
+            "currency" to "TESTKUDOS123",
+            "value" to "$maxValue",
+            "fraction" to "$maxFraction"
+        )
+
+        val amount = Amount.fromJsonObject(JSONObject(map))
+        assertEquals("TESTKUDOS123:$maxValue.$maxFraction", 
amount.toJSONString())
+        assertEquals("TESTKUDOS123", amount.currency)
+        assertEquals(maxValue, amount.value)
+        assertEquals(maxFraction, amount.fraction)
+        assertEquals("$maxValue.$maxFraction TESTKUDOS123", amount.toString())
+
+        // longer currency not accepted
+        assertThrows<AmountParserException>("longer currency was accepted") {
+            map = mapOf(
+                "currency" to "TESTKUDOS1234",
+                "value" to "$maxValue",
+                "fraction" to "$maxFraction"
+            )
+            Amount.fromJsonObject(JSONObject(map))
+        }
+
+        // max value + 1 not accepted
+        assertThrows<AmountParserException>("max value + 1 was accepted") {
+            map = mapOf(
+                "currency" to "TESTKUDOS123",
+                "value" to "${maxValue + 1}",
+                "fraction" to "$maxFraction"
+            )
+            Amount.fromJsonObject(JSONObject(map))
+        }
+
+        // max fraction + 1 not accepted
+        assertThrows<AmountParserException>("max fraction + 1 was accepted") {
+            map = mapOf(
+                "currency" to "TESTKUDOS123",
+                "value" to "$maxValue",
+                "fraction" to "${maxFraction + 1}"
+            )
+            Amount.fromJsonObject(JSONObject(map))
+        }
+    }
+
+    @Test
+    fun `test addition`() {
+        assertEquals(
+            Amount.fromJSONString("EUR:2"),
+            Amount.fromJSONString("EUR:1") + Amount.fromJSONString("EUR:1")
+        )
+        assertEquals(
+            Amount.fromJSONString("EUR:3"),
+            Amount.fromJSONString("EUR:1.5") + Amount.fromJSONString("EUR:1.5")
+        )
+        assertEquals(
+            Amount.fromJSONString("EUR:500000000.00000002"),
+            Amount.fromJSONString("EUR:500000000.00000001") + 
Amount.fromJSONString("EUR:0.00000001")
+        )
+        assertThrows<AmountOverflowException>("addition didn't overflow") {
+            Amount.fromJSONString("EUR:4503599627370496.99999999") + 
Amount.fromJSONString("EUR:0.00000001")
+        }
+        assertThrows<AmountOverflowException>("addition didn't overflow") {
+            Amount.fromJSONString("EUR:4000000000000000") + 
Amount.fromJSONString("EUR:4000000000000000")
+        }
+    }
+
+    @Test
+    fun `test times`() {
+        assertEquals(
+            Amount.fromJSONString("EUR:2"),
+            Amount.fromJSONString("EUR:2") * 1
+        )
+        assertEquals(
+            Amount.fromJSONString("EUR:2"),
+            Amount.fromJSONString("EUR:1") * 2
+        )
+        assertEquals(
+            Amount.fromJSONString("EUR:4.5"),
+            Amount.fromJSONString("EUR:1.5") * 3
+        )
+        assertEquals(
+            Amount.fromJSONString("EUR:1500000000.00000003"),
+            Amount.fromJSONString("EUR:500000000.00000001") * 3
+        )
+        assertThrows<AmountOverflowException>("times didn't overflow") {
+            Amount.fromJSONString("EUR:4000000000000000") * 2
+        }
+    }
+
+    @Test
+    fun `test subtraction`() {
+        assertEquals(
+            Amount.fromJSONString("EUR:0"),
+            Amount.fromJSONString("EUR:1") - Amount.fromJSONString("EUR:1")
+        )
+        assertEquals(
+            Amount.fromJSONString("EUR:1.5"),
+            Amount.fromJSONString("EUR:3") - Amount.fromJSONString("EUR:1.5")
+        )
+        assertEquals(
+            Amount.fromJSONString("EUR:500000000.00000001"),
+            Amount.fromJSONString("EUR:500000000.00000002") - 
Amount.fromJSONString("EUR:0.00000001")
+        )
+        assertThrows<AmountOverflowException>("subtraction didn't underflow") {
+            Amount.fromJSONString("EUR:23.42") - 
Amount.fromJSONString("EUR:42.23")
+        }
+        assertThrows<AmountOverflowException>("subtraction didn't underflow") {
+            Amount.fromJSONString("EUR:0.5") - 
Amount.fromJSONString("EUR:0.50000001")
+        }
+    }
+
+    @Test
+    fun `test isZero()`() {
+        assertTrue(Amount.zero("EUR").isZero())
+        assertTrue(Amount.fromJSONString("EUR:0").isZero())
+        assertTrue(Amount.fromJSONString("EUR:0.0").isZero())
+        assertTrue(Amount.fromJSONString("EUR:0.00000").isZero())
+        assertTrue((Amount.fromJSONString("EUR:1.001") - 
Amount.fromJSONString("EUR:1.001")).isZero())
+
+        assertFalse(Amount.fromJSONString("EUR:0.00000001").isZero())
+        assertFalse(Amount.fromJSONString("EUR:1.0").isZero())
+        assertFalse(Amount.fromJSONString("EUR:0001.0").isZero())
+    }
+
+    private inline fun <reified T : Throwable> assertThrows(
+        msg: String? = null,
+        function: () -> Any
+    ) {
+        try {
+            function.invoke()
+            fail(msg)
+        } catch (e: Exception) {
+            assertTrue(e is T)
+        }
+    }
+
+}
diff --git a/wallet/.gitlab-ci.yml b/wallet/.gitlab-ci.yml
index a07cb28..acd4a49 100644
--- a/wallet/.gitlab-ci.yml
+++ b/wallet/.gitlab-ci.yml
@@ -1,7 +1,7 @@
 .binary_deps:
   only:
     changes:
-      - "wallet"
+      - wallet/**/*
   before_script:
     - wget 
"https://git.taler.net/wallet-android.git/plain/akono.aar?h=binary-deps"; -O 
akono/akono.aar
     - mkdir -p app/src/main/assets
diff --git a/wallet/build.gradle b/wallet/build.gradle
index c31e392..3b8e13d 100644
--- a/wallet/build.gradle
+++ b/wallet/build.gradle
@@ -23,7 +23,7 @@ android {
     buildToolsVersion "29.0.3"
     defaultConfig {
         applicationId "net.taler.wallet"
-        minSdkVersion 21
+        minSdkVersion 24
         targetSdkVersion 29
         versionCode 6
         versionName "0.6.0pre8"
@@ -48,10 +48,8 @@ android {
 
 dependencies {
     implementation project(":akono")
+    implementation project(":taler-kotlin-common")
 
-    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-    implementation 'androidx.appcompat:appcompat:1.1.0'
-    implementation 'androidx.core:core-ktx:1.2.0'
     implementation 'com.google.android.material:material:1.1.0'
     implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
 
@@ -61,12 +59,9 @@ dependencies {
 
     // ViewModel and LiveData
     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"
 
     // QR codes
-    implementation 'com.google.zxing:core:3.4.0'
     implementation 'com.journeyapps:zxing-android-embedded:3.2.0@aar'
 
     // Nicer ProgressBar
diff --git a/wallet/src/main/java/net/taler/wallet/Amount.kt 
b/wallet/src/main/java/net/taler/wallet/Amount.kt
deleted file mode 100644
index a19e9bc..0000000
--- a/wallet/src/main/java/net/taler/wallet/Amount.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under 
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
- * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
-
-package net.taler.wallet
-
-import com.fasterxml.jackson.core.JsonParser
-import com.fasterxml.jackson.databind.DeserializationContext
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize
-import com.fasterxml.jackson.databind.deser.std.StdDeserializer
-import org.json.JSONObject
-import kotlin.math.round
-
-private const val FRACTIONAL_BASE = 1e8
-
-@JsonDeserialize(using = AmountDeserializer::class)
-data class Amount(val currency: String, val amount: String) {
-    fun isZero(): Boolean {
-        return amount.toDouble() == 0.0
-    }
-
-    companion object {
-        fun fromJson(jsonAmount: JSONObject): Amount {
-            val amountCurrency = jsonAmount.getString("currency")
-            val amountValue = jsonAmount.getString("value")
-            val amountFraction = jsonAmount.getString("fraction")
-            val amountIntValue = Integer.parseInt(amountValue)
-            val amountIntFraction = Integer.parseInt(amountFraction)
-            return Amount(
-                amountCurrency,
-                (amountIntValue + amountIntFraction / 
FRACTIONAL_BASE).toString()
-            )
-        }
-
-        fun fromString(strAmount: String): Amount {
-            val components = strAmount.split(":")
-            return Amount(components[0], components[1])
-        }
-    }
-
-    override fun toString(): String {
-        return String.format("%.2f $currency", amount.toDouble())
-    }
-}
-
-class AmountDeserializer : StdDeserializer<Amount>(Amount::class.java) {
-    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): 
Amount {
-        val node = p.codec.readValue(p, String::class.java)
-        return Amount.fromString(node)
-    }
-}
-
-class ParsedAmount(
-    /**
-     * name of the currency using either a three-character ISO 4217 currency 
code,
-     * or a regional currency identifier starting with a "*" followed by at 
most 10 characters.
-     * ISO 4217 exponents in the name are not supported,
-     * although the "fraction" is corresponds to an ISO 4217 exponent of 6.
-     */
-    val currency: String,
-
-    /**
-     * unsigned 32 bit value in the currency,
-     * note that "1" here would correspond to 1 EUR or 1 USD, depending on 
currency, not 1 cent.
-     */
-    val value: UInt,
-
-    /**
-     * unsigned 32 bit fractional value to be added to value
-     * representing an additional currency fraction,
-     * in units of one millionth (1e-6) of the base currency value.
-     * For example, a fraction of 500,000 would correspond to 50 cents.
-     */
-    val fraction: Double
-) {
-    companion object {
-        fun parseAmount(str: String): ParsedAmount {
-            val split = str.split(":")
-            check(split.size == 2)
-            val currency = split[0]
-            val valueSplit = split[1].split(".")
-            val value = valueSplit[0].toUInt()
-            val fraction: Double = if (valueSplit.size > 1) {
-                round("0.${valueSplit[1]}".toDouble() * FRACTIONAL_BASE)
-            } else 0.0
-            return ParsedAmount(currency, value, fraction)
-        }
-    }
-
-    operator fun minus(other: ParsedAmount): ParsedAmount {
-        check(currency == other.currency) { "Can only subtract from same 
currency" }
-        var resultValue = value
-        var resultFraction = fraction
-        if (resultFraction < other.fraction) {
-            if (resultValue < 1u) {
-                return ParsedAmount(currency, 0u, 0.0)
-            }
-            resultValue--
-            resultFraction += FRACTIONAL_BASE
-        }
-        check(resultFraction >= other.fraction)
-        resultFraction -= other.fraction
-        if (resultValue < other.value) {
-            return ParsedAmount(currency, 0u, 0.0)
-        }
-        resultValue -= other.value
-        return ParsedAmount(currency, resultValue, resultFraction)
-    }
-
-    fun isZero(): Boolean {
-        return value == 0u && fraction == 0.0
-    }
-
-    @Suppress("unused")
-    fun toJSONString(): String {
-        return "$currency:${getValueString()}"
-    }
-
-    override fun toString(): String {
-        return "${getValueString()} $currency"
-    }
-
-    private fun getValueString(): String {
-        return "$value${(fraction / FRACTIONAL_BASE).toString().substring(1)}"
-    }
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/BalanceFragment.kt 
b/wallet/src/main/java/net/taler/wallet/BalanceFragment.kt
index 84a1b3c..e4ec681 100644
--- a/wallet/src/main/java/net/taler/wallet/BalanceFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/BalanceFragment.kt
@@ -177,7 +177,7 @@ class BalanceAdapter : Adapter<BalanceViewHolder>() {
 
         fun bind(item: BalanceItem) {
             currencyView.text = item.available.currency
-            amountView.text = item.available.amount
+            amountView.text = item.available.amountStr
 
             val amountIncoming = item.pendingIncoming
             if (amountIncoming.isZero()) {
@@ -186,11 +186,8 @@ class BalanceAdapter : Adapter<BalanceViewHolder>() {
             } else {
                 balanceInboundAmount.visibility = VISIBLE
                 balanceInboundLabel.visibility = VISIBLE
-                balanceInboundAmount.text = v.context.getString(
-                    R.string.balances_inbound_amount,
-                    amountIncoming.amount,
-                    amountIncoming.currency
-                )
+                balanceInboundAmount.text =
+                    v.context.getString(R.string.balances_inbound_amount, 
amountIncoming)
             }
         }
     }
diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt 
b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
index c2f20f7..df7bdc6 100644
--- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
@@ -59,7 +59,6 @@ class MainActivity : AppCompatActivity(), 
OnNavigationItemSelectedListener,
 
     private lateinit var nav: NavController
 
-    @SuppressLint("SetTextI18n")
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
diff --git a/wallet/src/main/java/net/taler/wallet/Utils.kt 
b/wallet/src/main/java/net/taler/wallet/Utils.kt
deleted file mode 100644
index fb0b3ae..0000000
--- a/wallet/src/main/java/net/taler/wallet/Utils.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under 
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
- * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet
-
-import android.view.View
-import android.view.View.INVISIBLE
-import android.view.View.VISIBLE
-
-fun View.fadeIn(endAction: () -> Unit = {}) {
-    if (visibility == VISIBLE) return
-    alpha = 0f
-    visibility = VISIBLE
-    animate().alpha(1f).withEndAction {
-        if (context != null) endAction.invoke()
-    }.start()
-}
-
-fun View.fadeOut(endAction: () -> Unit = {}) {
-    if (visibility == INVISIBLE) return
-    animate().alpha(0f).withEndAction {
-        if (context == null) return@withEndAction
-        visibility = INVISIBLE
-        alpha = 1f
-        endAction.invoke()
-    }.start()
-}
diff --git a/wallet/src/main/java/net/taler/wallet/WalletViewModel.kt 
b/wallet/src/main/java/net/taler/wallet/WalletViewModel.kt
index 14a800f..9599123 100644
--- a/wallet/src/main/java/net/taler/wallet/WalletViewModel.kt
+++ b/wallet/src/main/java/net/taler/wallet/WalletViewModel.kt
@@ -26,6 +26,7 @@ import androidx.lifecycle.distinctUntilChanged
 import 
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.module.kotlin.KotlinModule
+import net.taler.common.Amount
 import net.taler.wallet.backend.WalletBackendApi
 import net.taler.wallet.history.HistoryManager
 import net.taler.wallet.payment.PaymentManager
@@ -90,10 +91,10 @@ class WalletViewModel(val app: Application) : 
AndroidViewModel(app) {
             for (currency in currencyList) {
                 val jsonAmount = byCurrency.getJSONObject(currency)
                     .getJSONObject("available")
-                val amount = Amount.fromJson(jsonAmount)
+                val amount = Amount.fromJsonObject(jsonAmount)
                 val jsonAmountIncoming = byCurrency.getJSONObject(currency)
                     .getJSONObject("pendingIncoming")
-                val amountIncoming = Amount.fromJson(jsonAmountIncoming)
+                val amountIncoming = Amount.fromJsonObject(jsonAmountIncoming)
                 balanceList.add(BalanceItem(amount, amountIncoming))
             }
             mBalances.postValue(balanceList)
diff --git a/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt 
b/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt
index 9e5c99d..b78c062 100644
--- a/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt
+++ b/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt
@@ -29,7 +29,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo
 import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY
 import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME
 import com.fasterxml.jackson.annotation.JsonTypeName
-import net.taler.wallet.ParsedAmount.Companion.parseAmount
+import net.taler.common.Amount
+import net.taler.common.Timestamp
 import net.taler.wallet.R
 import org.json.JSONObject
 
@@ -70,13 +71,6 @@ enum class RefreshReason {
     BACKUP_RESTORED
 }
 
-
-@JsonInclude(NON_EMPTY)
-class Timestamp(
-    @JsonProperty("t_ms")
-    val ms: Long
-)
-
 @JsonInclude(NON_EMPTY)
 class ReserveShortInfo(
     /**
@@ -181,12 +175,12 @@ class ReserveBalanceUpdatedEvent(
     /**
      * Amount currently left in the reserve.
      */
-    val amountReserveBalance: String,
+    val amountReserveBalance: Amount,
     /**
      * Amount we expected to be in the reserve at that time,
      * considering ongoing withdrawals from that reserve.
      */
-    val amountExpected: String
+    val amountExpected: Amount
 ) : HistoryEvent(timestamp) {
     override val title = R.string.history_event_reserve_balance_updated
 }
@@ -208,11 +202,11 @@ class HistoryWithdrawnEvent(
      * Amount that has been subtracted from the reserve's balance
      * for this withdrawal.
      */
-    val amountWithdrawnRaw: String,
+    val amountWithdrawnRaw: Amount,
     /**
      * Amount that actually was added to the wallet's balance.
      */
-    val amountWithdrawnEffective: String
+    val amountWithdrawnEffective: Amount
 ) : HistoryEvent(timestamp) {
     override val layout = R.layout.history_receive
     override val title = R.string.history_event_withdrawn
@@ -263,7 +257,7 @@ class HistoryPaymentSentEvent(
     /**
      * Amount that was paid, including deposit and wire fees.
      */
-    val amountPaidWithFees: String,
+    val amountPaidWithFees: Amount,
     /**
      * Session ID that the payment was (re-)submitted under.
      */
@@ -285,7 +279,7 @@ class HistoryPaymentAbortedEvent(
     /**
      * Amount that was lost due to refund and refreshing fees.
      */
-    val amountLost: String
+    val amountLost: Amount
 ) : HistoryEvent(timestamp) {
     override val layout = R.layout.history_payment
     override val title = R.string.history_event_payment_aborted
@@ -300,11 +294,11 @@ class HistoryRefreshedEvent(
      * Amount that is now available again because it has
      * been refreshed.
      */
-    val amountRefreshedEffective: String,
+    val amountRefreshedEffective: Amount,
     /**
      * Amount that we spent for refreshing.
      */
-    val amountRefreshedRaw: String,
+    val amountRefreshedRaw: Amount,
     /**
      * Why was the refreshing done?
      */
@@ -321,8 +315,7 @@ class HistoryRefreshedEvent(
     override val layout = R.layout.history_payment
     override val icon = R.drawable.history_refresh
     override val title = R.string.history_event_refreshed
-    override val showToUser =
-        !(parseAmount(amountRefreshedRaw) - 
parseAmount(amountRefreshedEffective)).isZero()
+    override val showToUser = !(amountRefreshedRaw - 
amountRefreshedEffective).isZero()
 }
 
 @JsonTypeName("order-redirected")
@@ -352,7 +345,7 @@ class HistoryTipAcceptedEvent(
     /**
      * Raw amount of the tip, without extra fees that apply.
      */
-    val tipRaw: String
+    val tipRaw: Amount
 ) : HistoryEvent(timestamp) {
     override val icon = R.drawable.history_tip_accepted
     override val title = R.string.history_event_tip_accepted
@@ -370,7 +363,7 @@ class HistoryTipDeclinedEvent(
     /**
      * Raw amount of the tip, without extra fees that apply.
      */
-    val tipAmount: String
+    val tipAmount: Amount
 ) : HistoryEvent(timestamp) {
     override val icon = R.drawable.history_tip_declined
     override val title = R.string.history_event_tip_declined
@@ -391,15 +384,15 @@ class HistoryRefundedEvent(
      * Part of the refund that couldn't be applied because
      * the refund permissions were expired.
      */
-    val amountRefundedInvalid: String,
+    val amountRefundedInvalid: Amount,
     /**
      * Amount that has been refunded by the merchant.
      */
-    val amountRefundedRaw: String,
+    val amountRefundedRaw: Amount,
     /**
      * Amount will be added to the wallet's balance after fees and refreshing.
      */
-    val amountRefundedEffective: String
+    val amountRefundedEffective: Amount
 ) : HistoryEvent(timestamp) {
     override val icon = R.drawable.history_refund
     override val title = R.string.history_event_refund
@@ -444,7 +437,7 @@ data class OrderShortInfo(
     /**
      * Amount that must be paid for the contract.
      */
-    val amount: String,
+    val amount: Amount,
     /**
      * Summary of the proposal, given by the merchant.
      */
diff --git 
a/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt 
b/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
index 45c539c..6c8fdaa 100644
--- a/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
+++ b/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo
 import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY
 import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME
 import com.fasterxml.jackson.annotation.JsonTypeName
+import net.taler.common.Timestamp
 
 
 @JsonTypeInfo(
diff --git 
a/wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt 
b/wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
index 71bdebc..5424b62 100644
--- a/wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
@@ -38,9 +38,8 @@ import androidx.annotation.CallSuper
 import androidx.core.net.toUri
 import androidx.recyclerview.widget.RecyclerView.Adapter
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import net.taler.common.Amount
 import net.taler.wallet.BuildConfig
-import net.taler.wallet.ParsedAmount
-import net.taler.wallet.ParsedAmount.Companion.parseAmount
 import net.taler.wallet.R
 
 
@@ -119,7 +118,7 @@ internal class WalletHistoryAdapter(
             info.text = when (event) {
                 is ExchangeAddedEvent -> event.exchangeBaseUrl
                 is ExchangeUpdatedEvent -> event.exchangeBaseUrl
-                is ReserveBalanceUpdatedEvent -> 
parseAmount(event.amountReserveBalance).toString()
+                is ReserveBalanceUpdatedEvent -> 
event.amountReserveBalance.toString()
                 is HistoryPaymentSentEvent -> event.orderShortInfo.summary
                 is HistoryOrderAcceptedEvent -> event.orderShortInfo.summary
                 is HistoryOrderRefusedEvent -> event.orderShortInfo.summary
@@ -151,36 +150,30 @@ internal class WalletHistoryAdapter(
             title.text = getHostname(event.exchangeBaseUrl)
             summary.setText(event.title)
 
-            val parsedEffective = parseAmount(event.amountWithdrawnEffective)
-            val parsedRaw = parseAmount(event.amountWithdrawnRaw)
-            showAmounts(parsedEffective, parsedRaw)
+            showAmounts(event.amountWithdrawnEffective, 
event.amountWithdrawnRaw)
         }
 
         private fun bind(event: HistoryRefundedEvent) {
             title.text = event.orderShortInfo.summary
             summary.setText(event.title)
 
-            val parsedEffective = parseAmount(event.amountRefundedEffective)
-            val parsedRaw = parseAmount(event.amountRefundedRaw)
-            showAmounts(parsedEffective, parsedRaw)
+            showAmounts(event.amountRefundedEffective, event.amountRefundedRaw)
         }
 
         private fun bind(event: HistoryTipAcceptedEvent) {
             title.setText(event.title)
             summary.text = null
-            val amount = parseAmount(event.tipRaw)
-            showAmounts(amount, amount)
+            showAmounts(event.tipRaw, event.tipRaw)
         }
 
         private fun bind(event: HistoryTipDeclinedEvent) {
             title.setText(event.title)
             summary.text = null
-            val amount = parseAmount(event.tipAmount)
-            showAmounts(amount, amount)
+            showAmounts(event.tipAmount, event.tipAmount)
             amountWithdrawn.paintFlags = amountWithdrawn.paintFlags or 
STRIKE_THRU_TEXT_FLAG
         }
 
-        private fun showAmounts(effective: ParsedAmount, raw: ParsedAmount) {
+        private fun showAmounts(effective: Amount, raw: Amount) {
             @SuppressLint("SetTextI18n")
             amountWithdrawn.text = "+$raw"
             val calculatedFee = raw - effective
@@ -220,19 +213,18 @@ internal class WalletHistoryAdapter(
         private fun bind(event: HistoryPaymentSentEvent) {
             title.text = event.orderShortInfo.summary
             @SuppressLint("SetTextI18n")
-            amountPaidWithFees.text = 
"-${parseAmount(event.amountPaidWithFees)}"
+            amountPaidWithFees.text = "-${event.amountPaidWithFees}"
         }
 
         private fun bind(event: HistoryPaymentAbortedEvent) {
             title.text = event.orderShortInfo.summary
             @SuppressLint("SetTextI18n")
-            amountPaidWithFees.text = "-${parseAmount(event.amountLost)}"
+            amountPaidWithFees.text = "-${event.amountLost}"
         }
 
         private fun bind(event: HistoryRefreshedEvent) {
             title.text = ""
-            val fee =
-                parseAmount(event.amountRefreshedRaw) - 
parseAmount(event.amountRefreshedEffective)
+            val fee = event.amountRefreshedRaw - event.amountRefreshedEffective
             @SuppressLint("SetTextI18n")
             if (fee.isZero()) amountPaidWithFees.text = null
             else amountPaidWithFees.text = "-$fee"
diff --git a/wallet/src/main/java/net/taler/wallet/payment/ContractTerms.kt 
b/wallet/src/main/java/net/taler/wallet/payment/ContractTerms.kt
deleted file mode 100644
index da91dea..0000000
--- a/wallet/src/main/java/net/taler/wallet/payment/ContractTerms.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under 
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
- * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.wallet.payment
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties
-import com.fasterxml.jackson.annotation.JsonProperty
-import net.taler.wallet.Amount
-
-
-@JsonIgnoreProperties(ignoreUnknown = true)
-data class ContractTerms(
-    val summary: String,
-    val products: List<ContractProduct>,
-    val amount: Amount
-)
-
-interface Product {
-    val id: String?
-    val description: String
-    val price: Amount
-    val location: String?
-    val image: String?
-}
-
-@JsonIgnoreProperties("totalPrice")
-data class ContractProduct(
-    @JsonProperty("product_id")
-    override val id: String?,
-    override val description: String,
-    override val price: Amount,
-    @JsonProperty("delivery_location")
-    override val location: String?,
-    override val image: String?,
-    val quantity: Int
-) : Product {
-
-    val totalPrice: Amount by lazy {
-        val amount = price.amount.toDouble() * quantity
-        Amount(price.currency, amount.toString())
-    }
-
-}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt 
b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
index ee0edaf..8aaebbc 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
@@ -22,7 +22,8 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.module.kotlin.readValue
-import net.taler.wallet.Amount
+import net.taler.common.Amount
+import net.taler.common.ContractTerms
 import net.taler.wallet.TAG
 import net.taler.wallet.backend.WalletBackendApi
 import org.json.JSONObject
@@ -79,7 +80,7 @@ class PaymentManager(
         "payment-possible" -> PayStatus.Prepared(
             contractTerms = getContractTerms(json),
             proposalId = json.getString("proposalId"),
-            totalFees = Amount.fromJson(json.getJSONObject("totalFees"))
+            totalFees = Amount.fromJsonObject(json.getJSONObject("totalFees"))
         )
         "paid" -> PayStatus.AlreadyPaid(getContractTerms(json))
         "insufficient-balance" -> 
PayStatus.InsufficientBalance(getContractTerms(json))
diff --git 
a/wallet/src/main/java/net/taler/wallet/payment/PaymentSuccessfulFragment.kt 
b/wallet/src/main/java/net/taler/wallet/payment/PaymentSuccessfulFragment.kt
index 2084c45..2a868b0 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PaymentSuccessfulFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentSuccessfulFragment.kt
@@ -23,8 +23,8 @@ import android.view.ViewGroup
 import androidx.fragment.app.Fragment
 import androidx.navigation.fragment.findNavController
 import kotlinx.android.synthetic.main.fragment_payment_successful.*
+import net.taler.common.fadeIn
 import net.taler.wallet.R
-import net.taler.wallet.fadeIn
 
 /**
  * Fragment that shows the success message for a payment.
diff --git a/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt 
b/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt
index 4b1b062..24bbd27 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt
@@ -28,6 +28,7 @@ import android.widget.ImageView
 import android.widget.TextView
 import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import net.taler.common.ContractProduct
 import net.taler.wallet.R
 import net.taler.wallet.payment.ProductAdapter.ProductViewHolder
 
@@ -76,7 +77,7 @@ internal class ProductAdapter(private val listener: 
ProductImageClickListener) :
             } else {
                 image.visibility = VISIBLE
                 // product.image was validated before, so non-null below
-                val match = REGEX_PRODUCT_IMAGE.matchEntire(product.image)!!
+                val match = REGEX_PRODUCT_IMAGE.matchEntire(product.image!!)!!
                 val decodedString = Base64.decode(match.groups[2]!!.value, 
Base64.DEFAULT)
                 val bitmap = decodeByteArray(decodedString, 0, 
decodedString.size)
                 image.setImageBitmap(bitmap)
diff --git 
a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt 
b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
index 44dcf26..2eea59e 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
@@ -16,7 +16,6 @@
 
 package net.taler.wallet.payment
 
-import android.annotation.SuppressLint
 import android.graphics.Bitmap
 import android.os.Bundle
 import android.view.LayoutInflater
@@ -33,11 +32,12 @@ import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.transition.TransitionManager.beginDelayedTransition
 import kotlinx.android.synthetic.main.payment_bottom_bar.*
 import kotlinx.android.synthetic.main.payment_details.*
-import net.taler.wallet.Amount
+import net.taler.common.Amount
+import net.taler.common.ContractTerms
+import net.taler.common.fadeIn
+import net.taler.common.fadeOut
 import net.taler.wallet.R
 import net.taler.wallet.WalletViewModel
-import net.taler.wallet.fadeIn
-import net.taler.wallet.fadeOut
 
 /**
  * Show a payment and ask the user to accept/decline.
@@ -144,11 +144,9 @@ class PromptPaymentFragment : Fragment(), 
ProductImageClickListener {
         adapter.setItems(contractTerms.products)
         if (contractTerms.products.size == 1) 
paymentManager.toggleDetailsShown()
         val amount = contractTerms.amount
-        @SuppressLint("SetTextI18n")
-        totalView.text = "${amount.amount} ${amount.currency}"
+        totalView.text = amount.toString()
         if (totalFees != null && !totalFees.isZero()) {
-            val fee = "${totalFees.amount} ${totalFees.currency}"
-            feeView.text = getString(R.string.payment_fee, fee)
+            feeView.text = getString(R.string.payment_fee, totalFees)
             feeView.fadeIn()
         } else {
             feeView.visibility = GONE
diff --git 
a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt 
b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt
index 454816b..8fb4cb8 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt
@@ -16,7 +16,6 @@
 
 package net.taler.wallet.withdraw
 
-import android.annotation.SuppressLint
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
@@ -26,10 +25,10 @@ import androidx.fragment.app.activityViewModels
 import androidx.lifecycle.Observer
 import androidx.navigation.fragment.findNavController
 import kotlinx.android.synthetic.main.fragment_prompt_withdraw.*
+import net.taler.common.fadeIn
+import net.taler.common.fadeOut
 import net.taler.wallet.R
 import net.taler.wallet.WalletViewModel
-import net.taler.wallet.fadeIn
-import net.taler.wallet.fadeOut
 import net.taler.wallet.withdraw.WithdrawStatus.Loading
 import net.taler.wallet.withdraw.WithdrawStatus.TermsOfServiceReviewRequired
 import net.taler.wallet.withdraw.WithdrawStatus.Withdrawing
@@ -73,8 +72,7 @@ class PromptWithdrawFragment : Fragment() {
             progressBar.fadeOut()
 
             introView.fadeIn()
-            @SuppressLint("SetTextI18n")
-            withdrawAmountView.text = "${status.amount.amount} 
${status.amount.currency}"
+            withdrawAmountView.text = status.amount.toString()
             withdrawAmountView.fadeIn()
             feeView.fadeIn()
 
diff --git 
a/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt 
b/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt
index cd01a33..eac9e13 100644
--- 
a/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt
+++ 
b/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt
@@ -26,10 +26,10 @@ import androidx.fragment.app.activityViewModels
 import androidx.lifecycle.Observer
 import androidx.navigation.fragment.findNavController
 import kotlinx.android.synthetic.main.fragment_review_exchange_tos.*
+import net.taler.common.fadeIn
+import net.taler.common.fadeOut
 import net.taler.wallet.R
 import net.taler.wallet.WalletViewModel
-import net.taler.wallet.fadeIn
-import net.taler.wallet.fadeOut
 
 class ReviewExchangeTosFragment : Fragment() {
 
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt 
b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
index e3af757..d686465 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
@@ -18,7 +18,7 @@ package net.taler.wallet.withdraw
 
 import android.util.Log
 import androidx.lifecycle.MutableLiveData
-import net.taler.wallet.Amount
+import net.taler.common.Amount
 import net.taler.wallet.TAG
 import net.taler.wallet.backend.WalletBackendApi
 import org.json.JSONObject
@@ -124,7 +124,7 @@ class WithdrawManager(private val walletBackendApi: 
WalletBackendApi) {
             }
             val wi = result.getJSONObject("bankWithdrawDetails")
             val suggestedExchange = wi.getString("suggestedExchange")
-            val amount = Amount.fromJson(wi.getJSONObject("amount"))
+            val amount = Amount.fromJsonObject(wi.getJSONObject("amount"))
 
             val ei = result.getJSONObject("exchangeWithdrawDetails")
             val termsOfServiceAccepted = 
ei.getBoolean("termsOfServiceAccepted")
diff --git a/wallet/src/main/res/values/strings.xml 
b/wallet/src/main/res/values/strings.xml
index 8981e04..04a507b 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -40,7 +40,7 @@
     <string name="aiddescription">my aid</string>
 
     <string name="balances_title">Balances</string>
-    <string name="balances_inbound_amount">+%1s %2s</string>
+    <string name="balances_inbound_amount">+%s</string>
     <string name="balances_inbound_label">inbound</string>
     <string name="balances_empty_state">There is no digital cash in your 
wallet.\n\nYou can get test money from the demo 
bank:\n\nhttps://bank.demo.taler.net</string>
 

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]