summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.gradle1
-rw-r--r--cashier/src/main/java/net/taler/cashier/BalanceFragment.kt10
-rw-r--r--cashier/src/main/java/net/taler/cashier/MainViewModel.kt25
-rw-r--r--cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt4
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt5
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt2
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt4
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt6
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt4
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt10
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt12
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt9
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt7
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt2
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt2
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt4
-rw-r--r--merchant-terminal/src/main/res/values/strings.xml2
-rw-r--r--taler-kotlin-common/build.gradle5
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/Amount.kt192
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt7
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt19
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt (renamed from wallet/src/test/java/net/taler/wallet/ExampleUnitTest.kt)33
-rw-r--r--taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt290
-rw-r--r--wallet/.gitlab-ci.yml2
-rw-r--r--wallet/build.gradle9
-rw-r--r--wallet/src/main/java/net/taler/wallet/Amount.kt141
-rw-r--r--wallet/src/main/java/net/taler/wallet/BalanceFragment.kt9
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainActivity.kt1
-rw-r--r--wallet/src/main/java/net/taler/wallet/Utils.kt40
-rw-r--r--wallet/src/main/java/net/taler/wallet/WalletViewModel.kt5
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt41
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt1
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt28
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/ContractTerms.kt56
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt5
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PaymentSuccessfulFragment.kt2
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/ProductAdapter.kt3
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt14
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt8
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt4
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt4
-rw-r--r--wallet/src/main/res/values/strings.xml2
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
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>