From db8b71418b766258a7a4bda91e496b1b03cb28cd Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 20 Mar 2020 15:43:23 -0300 Subject: 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 | 40 +++ .../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 +- .../test/java/net/taler/wallet/ExampleUnitTest.kt | 33 --- 43 files changed, 632 insertions(+), 438 deletions(-) create mode 100644 taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt create mode 100644 taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/Amount.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/Utils.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/payment/ContractTerms.kt delete mode 100644 wallet/src/test/java/net/taler/wallet/ExampleUnitTest.kt 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?, - override val price: String, + override val price: Amount, override val location: String?, override val image: String?, val categories: List, @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 - val orderTotal: LiveData + val orderTotal: LiveData val restartState: LiveData val modifyOrderAllowed: LiveData val lastAddedProduct: ConfigProduct? @@ -44,12 +45,13 @@ internal interface LiveOrder { internal class MutableLiveOrder( val id: Int, + private val currency: String, private val productsByCategory: HashMap> ) : LiveOrder { private val availableCategories: Map get() = productsByCategory.keys.map { it.id to it }.toMap() - override val order: MutableLiveData = MutableLiveData(Order(id, availableCategories)) - override val orderTotal: LiveData = Transformations.map(order) { it.total } + override val order: MutableLiveData = MutableLiveData(Order(id, currency, availableCategories)) + override val orderTotal: LiveData = Transformations.map(order) { it.total } override val restartState = MutableLiveData(DISABLED) private val selectedOrderLine = MutableLiveData() 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) { +data class Order(val id: Int, val currency: String, val availableCategories: Map) { val products = ArrayList() val title: String = id.toString() val summary: String @@ -29,17 +30,14 @@ data class Order(val id: Int, val availableCategories: Map) { "$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() internal val currentOrderId: LiveData = 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() { 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 @@ Order #%s - Total: %1$.2f %2$s + Total: %s Restart Undo Prev 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::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, + 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? - 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?, - 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/taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt b/taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt new file mode 100644 index 0000000..03a0d6e --- /dev/null +++ b/taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt @@ -0,0 +1,40 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.common + +import android.annotation.SuppressLint + +data class SignedAmount( + val positive: Boolean, + val amount: Amount +) { + + 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 + */ + +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("longer currency was accepted") { + Amount.fromJSONString("TESTKUDOS1234:$maxValue.99999999") + } + + // max value + 1 not accepted + assertThrows("max value + 1 was accepted") { + Amount.fromJSONString("TESTKUDOS123:${maxValue + 1}.99999999") + } + + // max fraction + 1 not accepted + assertThrows("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 { + Amount.fromJSONString("TESTKUDOS:0,5") + } + assertThrows { + Amount.fromJSONString("+TESTKUDOS:0.5") + } + assertThrows { + Amount.fromJSONString("0.5") + } + assertThrows { + Amount.fromJSONString(":0.5") + } + assertThrows { + Amount.fromJSONString("EUR::0.5") + } + assertThrows { + 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("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("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("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("addition didn't overflow") { + Amount.fromJSONString("EUR:4503599627370496.99999999") + Amount.fromJSONString("EUR:0.00000001") + } + assertThrows("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("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("subtraction didn't underflow") { + Amount.fromJSONString("EUR:23.42") - Amount.fromJSONString("EUR:42.23") + } + assertThrows("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 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 - */ - -@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::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() { 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() { } 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 - */ - -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 - */ - -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, - 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 @@ my aid Balances - +%1s %2s + +%s inbound There is no digital cash in your wallet.\n\nYou can get test money from the demo bank:\n\nhttps://bank.demo.taler.net diff --git a/wallet/src/test/java/net/taler/wallet/ExampleUnitTest.kt b/wallet/src/test/java/net/taler/wallet/ExampleUnitTest.kt deleted file mode 100644 index de74f68..0000000 --- a/wallet/src/test/java/net/taler/wallet/ExampleUnitTest.kt +++ /dev/null @@ -1,33 +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 - */ - -package net.taler.wallet - -import org.junit.Test - -import org.junit.Assert.* - -/** - * 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) - } -} -- cgit v1.2.3