summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2019-12-30 12:18:38 -0300
committerTorsten Grote <t@grobox.de>2019-12-30 12:18:38 -0300
commit1ec44435ab0ab098a04b3f4ee9f2599d99535c41 (patch)
tree219f16617853af2ba431d8f0f02ed4cc845703b1
parent35ba17db04f48f4ec41f55ac6944051093f7b78b (diff)
downloadwallet-android-1ec44435ab0ab098a04b3f4ee9f2599d99535c41.tar.gz
wallet-android-1ec44435ab0ab098a04b3f4ee9f2599d99535c41.tar.bz2
wallet-android-1ec44435ab0ab098a04b3f4ee9f2599d99535c41.zip
Deserialize and render more wallet history events
-rw-r--r--app/src/main/java/net/taler/wallet/Amount.kt120
-rw-r--r--app/src/main/java/net/taler/wallet/WalletViewModel.kt26
-rw-r--r--app/src/main/java/net/taler/wallet/history/HistoryEvent.kt176
-rw-r--r--app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt16
-rw-r--r--app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt89
-rw-r--r--app/src/main/res/drawable/ic_account_balance.xml9
-rw-r--r--app/src/main/res/drawable/ic_add_circle.xml9
-rw-r--r--app/src/main/res/drawable/ic_cancel.xml9
-rw-r--r--app/src/main/res/drawable/ic_cash_usd_outline.xml9
-rw-r--r--app/src/main/res/layout/history_payment_sent.xml70
-rw-r--r--app/src/main/res/layout/history_row.xml49
-rw-r--r--app/src/main/res/layout/history_withdrawn.xml40
-rw-r--r--app/src/main/res/values/colors.xml3
-rw-r--r--app/src/main/res/values/strings.xml8
-rw-r--r--app/src/test/java/net/taler/wallet/history/HistoryEventTest.kt171
-rw-r--r--app/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt16
16 files changed, 747 insertions, 73 deletions
diff --git a/app/src/main/java/net/taler/wallet/Amount.kt b/app/src/main/java/net/taler/wallet/Amount.kt
new file mode 100644
index 0000000..656228f
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/Amount.kt
@@ -0,0 +1,120 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 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 org.json.JSONObject
+import kotlin.math.round
+
+private const val FRACTIONAL_BASE = 1e8
+
+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])
+ }
+ }
+}
+
+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 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/app/src/main/java/net/taler/wallet/WalletViewModel.kt b/app/src/main/java/net/taler/wallet/WalletViewModel.kt
index 94d2d8a..f556ff3 100644
--- a/app/src/main/java/net/taler/wallet/WalletViewModel.kt
+++ b/app/src/main/java/net/taler/wallet/WalletViewModel.kt
@@ -30,32 +30,6 @@ import org.json.JSONObject
const val TAG = "taler-wallet"
-data class Amount(val currency: String, val amount: String) {
- fun isZero(): Boolean {
- return amount.toDouble() == 0.0
- }
-
- companion object {
- const val FRACTIONAL_BASE = 1e8
- 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])
- }
- }
-}
-
data class BalanceEntry(val available: Amount, val pendingIncoming: Amount)
diff --git a/app/src/main/java/net/taler/wallet/history/HistoryEvent.kt b/app/src/main/java/net/taler/wallet/history/HistoryEvent.kt
index 34f3164..31473f6 100644
--- a/app/src/main/java/net/taler/wallet/history/HistoryEvent.kt
+++ b/app/src/main/java/net/taler/wallet/history/HistoryEvent.kt
@@ -1,10 +1,30 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 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.history
+import androidx.annotation.DrawableRes
+import androidx.annotation.LayoutRes
+import androidx.annotation.StringRes
import com.fasterxml.jackson.annotation.*
import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY
import com.fasterxml.jackson.annotation.JsonSubTypes.Type
import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME
+import net.taler.wallet.R
enum class ReserveType {
/**
@@ -20,7 +40,22 @@ enum class ReserveType {
}
@JsonInclude(NON_EMPTY)
-class ReserveCreationDetail(val type: ReserveType)
+class ReserveCreationDetail(val type: ReserveType, val bankUrl: String?)
+
+enum class RefreshReason {
+ @JsonProperty("manual")
+ MANUAL,
+ @JsonProperty("pay")
+ PAY,
+ @JsonProperty("refund")
+ REFUND,
+ @JsonProperty("abort-pay")
+ ABORT_PAY,
+ @JsonProperty("recoup")
+ RECOUP,
+ @JsonProperty("backup-restored")
+ BACKUP_RESTORED
+}
@JsonInclude(NON_EMPTY)
@@ -45,7 +80,7 @@ class ReserveShortInfo(
val reserveCreationDetail: ReserveCreationDetail
)
-class History: ArrayList<HistoryEvent>()
+class History : ArrayList<HistoryEvent>()
@JsonTypeInfo(
use = NAME,
@@ -56,7 +91,11 @@ class History: ArrayList<HistoryEvent>()
Type(value = ExchangeAddedEvent::class, name = "exchange-added"),
Type(value = ExchangeUpdatedEvent::class, name = "exchange-updated"),
Type(value = ReserveBalanceUpdatedEvent::class, name = "reserve-balance-updated"),
- Type(value = HistoryWithdrawnEvent::class, name = "withdrawn")
+ Type(value = HistoryWithdrawnEvent::class, name = "withdrawn"),
+ Type(value = HistoryOrderAcceptedEvent::class, name = "order-accepted"),
+ Type(value = HistoryOrderRefusedEvent::class, name = "order-refused"),
+ Type(value = HistoryPaymentSentEvent::class, name = "payment-sent"),
+ Type(value = HistoryRefreshedEvent::class, name = "refreshed")
)
@JsonIgnoreProperties(
value = [
@@ -64,7 +103,13 @@ class History: ArrayList<HistoryEvent>()
]
)
abstract class HistoryEvent(
- val timestamp: Timestamp
+ val timestamp: Timestamp,
+ @get:LayoutRes
+ open val layout: Int = R.layout.history_row,
+ @get:StringRes
+ open val title: Int = 0,
+ @get:DrawableRes
+ open val icon: Int = R.drawable.ic_account_balance
)
@@ -73,13 +118,17 @@ class ExchangeAddedEvent(
timestamp: Timestamp,
val exchangeBaseUrl: String,
val builtIn: Boolean
-) : HistoryEvent(timestamp)
+) : HistoryEvent(timestamp) {
+ override val title = R.string.history_event_exchange_added
+}
@JsonTypeName("exchange-updated")
class ExchangeUpdatedEvent(
timestamp: Timestamp,
val exchangeBaseUrl: String
-) : HistoryEvent(timestamp)
+) : HistoryEvent(timestamp) {
+ override val title = R.string.history_event_exchange_updated
+}
@JsonTypeName("reserve-balance-updated")
@@ -99,7 +148,9 @@ class ReserveBalanceUpdatedEvent(
* considering ongoing withdrawals from that reserve.
*/
val amountExpected: String
-) : HistoryEvent(timestamp)
+) : HistoryEvent(timestamp) {
+ override val title = R.string.history_event_reserve_balance_updated
+}
@JsonTypeName("withdrawn")
class HistoryWithdrawnEvent(
@@ -123,8 +174,94 @@ class HistoryWithdrawnEvent(
* Amount that actually was added to the wallet's balance.
*/
val amountWithdrawnEffective: String
-) : HistoryEvent(timestamp)
+) : HistoryEvent(timestamp) {
+ override val layout = R.layout.history_withdrawn
+ override val title = R.string.history_event_withdrawn
+ override val icon = R.drawable.history_withdrawn
+}
+@JsonTypeName("order-accepted")
+class HistoryOrderAcceptedEvent(
+ timestamp: Timestamp,
+ /**
+ * Condensed info about the order.
+ */
+ val orderShortInfo: OrderShortInfo
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.ic_add_circle
+ override val title = R.string.history_event_order_accepted
+}
+
+@JsonTypeName("order-refused")
+class HistoryOrderRefusedEvent(
+ timestamp: Timestamp,
+ /**
+ * Condensed info about the order.
+ */
+ val orderShortInfo: OrderShortInfo
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.ic_cancel
+ override val title = R.string.history_event_order_refused
+}
+
+@JsonTypeName("payment-sent")
+class HistoryPaymentSentEvent(
+ timestamp: Timestamp,
+ /**
+ * Condensed info about the order that we already paid for.
+ */
+ val orderShortInfo: OrderShortInfo,
+ /**
+ * Set to true if the payment has been previously sent
+ * to the merchant successfully, possibly with a different session ID.
+ */
+ val replay: Boolean,
+ /**
+ * Number of coins that were involved in the payment.
+ */
+ val numCoins: Int,
+ /**
+ * Amount that was paid, including deposit and wire fees.
+ */
+ val amountPaidWithFees: String,
+ /**
+ * Session ID that the payment was (re-)submitted under.
+ */
+ val sessionId: String?
+) : HistoryEvent(timestamp) {
+ override val layout = R.layout.history_payment_sent
+ override val title = R.string.history_event_payment_sent
+ override val icon = R.drawable.ic_cash_usd_outline
+}
+
+@JsonTypeName("refreshed")
+class HistoryRefreshedEvent(
+ timestamp: Timestamp,
+ /**
+ * Amount that is now available again because it has
+ * been refreshed.
+ */
+ val amountRefreshedEffective: String,
+ /**
+ * Amount that we spent for refreshing.
+ */
+ val amountRefreshedRaw: String,
+ /**
+ * Why was the refreshing done?
+ */
+ val refreshReason: RefreshReason,
+ val numInputCoins: Int,
+ val numRefreshedInputCoins: Int,
+ val numOutputCoins: Int,
+ /**
+ * Identifier for a refresh group, contains one or
+ * more refresh session IDs.
+ */
+ val refreshGroupId: String
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.ic_history_black_24dp
+ override val title = R.string.history_event_refreshed
+}
@JsonTypeInfo(
use = NAME,
@@ -145,3 +282,26 @@ class WithdrawalSourceTip(
class WithdrawalSourceReserve(
val reservePub: String
) : WithdrawalSource()
+
+data class OrderShortInfo(
+ /**
+ * Wallet-internal identifier of the proposal.
+ */
+ val proposalId: String,
+ /**
+ * Order ID, uniquely identifies the order within a merchant instance.
+ */
+ val orderId: String,
+ /**
+ * Base URL of the merchant.
+ */
+ val merchantBaseUrl: String,
+ /**
+ * Amount that must be paid for the contract.
+ */
+ val amount: String,
+ /**
+ * Summary of the proposal, given by the merchant.
+ */
+ val summary: String
+)
diff --git a/app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt b/app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
index 880639f..f4cfcb8 100644
--- a/app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
+++ b/app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
@@ -1,3 +1,19 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 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.history
import com.fasterxml.jackson.annotation.JsonProperty
diff --git a/app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt b/app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
index 37dc742..14fc1e9 100644
--- a/app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
+++ b/app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
@@ -1,13 +1,31 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 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.history
import android.text.format.DateUtils.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.CallSuper
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import net.taler.wallet.ParsedAmount.Companion.parseAmount
import net.taler.wallet.R
@@ -18,16 +36,14 @@ internal class WalletHistoryAdapter(private var history: History = History()) :
setHasStableIds(false)
}
- override fun getItemViewType(position: Int): Int = when (history[position]) {
- is HistoryWithdrawnEvent -> R.layout.history_withdrawn
- else -> R.layout.history_row
- }
+ override fun getItemViewType(position: Int): Int = history[position].layout
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryEventViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
- R.layout.history_withdrawn -> HistoryWithdrawnEventViewHolder(view)
- else -> HistoryEventViewHolder(view)
+ R.layout.history_withdrawn -> HistoryWithdrawnViewHolder(view)
+ R.layout.history_payment_sent -> HistoryPaymentSentViewHolder(view)
+ else -> GenericHistoryEventViewHolder(view)
}
}
@@ -45,14 +61,17 @@ internal class WalletHistoryAdapter(private var history: History = History()) :
}
-internal open class HistoryEventViewHolder(protected val v: View) : ViewHolder(v) {
+internal abstract class HistoryEventViewHolder(protected val v: View) : ViewHolder(v) {
- protected val title: TextView = v.findViewById(R.id.title)
+ private val icon: ImageView = v.findViewById(R.id.icon)
+ private val title: TextView = v.findViewById(R.id.title)
private val time: TextView = v.findViewById(R.id.time)
@CallSuper
open fun bind(event: HistoryEvent) {
- title.text = event::class.java.simpleName
+ icon.setImageResource(event.icon)
+ if (event.title == 0) title.text = event::class.java.simpleName
+ else title.setText(event.title)
time.text = getRelativeTime(event.timestamp.ms)
}
@@ -61,22 +80,60 @@ internal open class HistoryEventViewHolder(protected val v: View) : ViewHolder(v
return getRelativeTimeSpanString(timestamp, now, MINUTE_IN_MILLIS, FORMAT_ABBREV_RELATIVE)
}
- protected fun getString(resId: Int) = v.context.getString(resId)
+}
+
+internal class GenericHistoryEventViewHolder(v: View) : HistoryEventViewHolder(v) {
+
+ private val info: TextView = v.findViewById(R.id.info)
+
+ override fun bind(event: HistoryEvent) {
+ super.bind(event)
+ info.text = when (event) {
+ is ExchangeAddedEvent -> event.exchangeBaseUrl
+ is ExchangeUpdatedEvent -> event.exchangeBaseUrl
+ is ReserveBalanceUpdatedEvent -> parseAmount(event.amountReserveBalance).toString()
+ is HistoryPaymentSentEvent -> event.orderShortInfo.summary
+ is HistoryOrderAcceptedEvent -> event.orderShortInfo.summary
+ is HistoryOrderRefusedEvent -> event.orderShortInfo.summary
+ is HistoryRefreshedEvent -> {
+ "${parseAmount(event.amountRefreshedRaw)} - ${parseAmount(event.amountRefreshedEffective)}"
+ }
+ else -> ""
+ }
+ }
}
-internal class HistoryWithdrawnEventViewHolder(v: View) : HistoryEventViewHolder(v) {
+internal class HistoryWithdrawnViewHolder(v: View) : HistoryEventViewHolder(v) {
- private val amountWithdrawnRaw: TextView = v.findViewById(R.id.amountWithdrawnRaw)
- private val amountWithdrawnEffective: TextView = v.findViewById(R.id.amountWithdrawnEffective)
+ private val exchangeUrl: TextView = v.findViewById(R.id.exchangeUrl)
+ private val amountWithdrawn: TextView = v.findViewById(R.id.amountWithdrawn)
+ private val fee: TextView = v.findViewById(R.id.fee)
override fun bind(event: HistoryEvent) {
super.bind(event)
event as HistoryWithdrawnEvent
- title.text = getString(R.string.history_event_withdrawn)
- amountWithdrawnRaw.text = event.amountWithdrawnRaw
- amountWithdrawnEffective.text = event.amountWithdrawnEffective
+ exchangeUrl.text = event.exchangeBaseUrl
+ val parsedEffective = parseAmount(event.amountWithdrawnEffective)
+ val parsedRaw = parseAmount(event.amountWithdrawnRaw)
+ amountWithdrawn.text = parsedRaw.toString()
+ fee.text = (parsedRaw - parsedEffective).toString()
+ }
+
+}
+
+internal class HistoryPaymentSentViewHolder(v: View) : HistoryEventViewHolder(v) {
+
+ private val summary: TextView = v.findViewById(R.id.summary)
+ private val amountPaidWithFees: TextView = v.findViewById(R.id.amountPaidWithFees)
+
+ override fun bind(event: HistoryEvent) {
+ super.bind(event)
+ event as HistoryPaymentSentEvent
+
+ summary.text = event.orderShortInfo.summary
+ amountPaidWithFees.text = parseAmount(event.amountPaidWithFees).toString()
}
}
diff --git a/app/src/main/res/drawable/ic_account_balance.xml b/app/src/main/res/drawable/ic_account_balance.xml
new file mode 100644
index 0000000..03224e2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_account_balance.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M4,10v7h3v-7L4,10zM10,10v7h3v-7h-3zM2,22h19v-3L2,19v3zM16,10v7h3v-7h-3zM11.5,1L2,6v2h19L21,6l-9.5,-5z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_add_circle.xml b/app/src/main/res/drawable/ic_add_circle.xml
new file mode 100644
index 0000000..01d5f90
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add_circle.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_cancel.xml b/app/src/main/res/drawable/ic_cancel.xml
new file mode 100644
index 0000000..7d2b57e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cancel.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_cash_usd_outline.xml b/app/src/main/res/drawable/ic_cash_usd_outline.xml
new file mode 100644
index 0000000..604b40e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cash_usd_outline.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#000"
+ android:pathData="M20,18H4V6H20M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4M11,17H13V16H14A1,1 0 0,0 15,15V12A1,1 0 0,0 14,11H11V10H15V8H13V7H11V8H10A1,1 0 0,0 9,9V12A1,1 0 0,0 10,13H13V14H9V16H11V17Z" />
+</vector>
diff --git a/app/src/main/res/layout/history_payment_sent.xml b/app/src/main/res/layout/history_payment_sent.xml
new file mode 100644
index 0000000..0c39133
--- /dev/null
+++ b/app/src/main/res/layout/history_payment_sent.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="8dp">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/history_withdrawn"
+ app:tint="?android:colorControlNormal"
+ tools:ignore="ContentDescription" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:text="@string/history_event_payment_sent"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="20sp"
+ app:layout_constraintEnd_toStartOf="@+id/amountPaidWithFees"
+ app:layout_constraintStart_toEndOf="@+id/icon"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/amountPaidWithFees"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toEndOf="@+id/icon"
+ app:layout_constraintTop_toBottomOf="@+id/title"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="Lots of books" />
+
+ <TextView
+ android:id="@+id/amountPaidWithFees"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/red"
+ app:layout_constraintBottom_toTopOf="@+id/time"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="0.2 TESTKUDOS" />
+
+ <TextView
+ android:id="@+id/time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ tools:text="23 min ago" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/history_row.xml b/app/src/main/res/layout/history_row.xml
index 2ea6d9d..cab7f0f 100644
--- a/app/src/main/res/layout/history_row.xml
+++ b/app/src/main/res/layout/history_row.xml
@@ -1,24 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical"
android:layout_margin="15dp">
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/ic_account_balance"
+ app:tint="?android:colorControlNormal"
+ tools:ignore="ContentDescription" />
+
<TextView
android:id="@+id/title"
- android:layout_width="wrap_content"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:textSize="20sp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/icon"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:text="My History Event" />
+
+ <TextView
+ android:id="@+id/info"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:textSize="24sp"
- tools:text="My History Event"/>
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/time"
+ app:layout_constraintStart_toEndOf="@+id/icon"
+ app:layout_constraintTop_toBottomOf="@+id/title"
+ tools:text="TextView" />
<TextView
android:id="@+id/time"
- android:layout_width="match_parent"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
android:gravity="end"
android:textSize="14sp"
- tools:text="3 days ago"/>
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/title"
+ tools:text="3 days ago" />
-</LinearLayout> \ No newline at end of file
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/history_withdrawn.xml b/app/src/main/res/layout/history_withdrawn.xml
index 8290c37..e02046b 100644
--- a/app/src/main/res/layout/history_withdrawn.xml
+++ b/app/src/main/res/layout/history_withdrawn.xml
@@ -28,34 +28,44 @@
android:text="@string/history_event_withdrawn"
android:textColor="?android:textColorPrimary"
android:textSize="20sp"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="parent" />
<TextView
- android:id="@+id/amountWithdrawnRaw"
+ android:id="@+id/exchangeUrl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
- android:layout_marginEnd="8dp"
- app:layout_constraintBottom_toBottomOf="@+id/amountWithdrawnEffective"
- app:layout_constraintEnd_toStartOf="@+id/amountWithdrawnEffective"
- app:layout_constraintHorizontal_bias="1.0"
+ android:layout_marginTop="8dp"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/fee"
+ app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/icon"
- app:layout_constraintTop_toTopOf="@+id/amountWithdrawnEffective"
- tools:text="TESTKUDOS:10" />
+ app:layout_constraintTop_toBottomOf="@+id/title"
+ app:layout_constraintVertical_bias="0.0"
+ tools:text="http://taler.exchange.url" />
<TextView
- android:id="@+id/amountWithdrawnEffective"
+ android:id="@+id/fee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="8dp"
+ android:textColor="@color/red"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/amountWithdrawn"
+ tools:text="0.2 TESTKUDOS" />
+
+ <TextView
+ android:id="@+id/amountWithdrawn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/green"
android:textSize="16sp"
- android:textColor="?android:textColorPrimary"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/title"
- tools:text="TESTKUDOS:9.8" />
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintStart_toEndOf="@+id/title"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="10 TESTKUDOS" />
<TextView
android:id="@+id/time"
@@ -65,7 +75,7 @@
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/amountWithdrawnEffective"
+ app:layout_constraintTop_toBottomOf="@+id/fee"
tools:text="23 min ago" />
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 3b6dc88..0fe76cc 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -3,4 +3,7 @@
<color name="colorPrimary">#283593</color>
<color name="colorPrimaryDark">#1A237E</color>
<color name="colorAccent">#AE1010</color>
+
+ <color name="red">#C62828</color>
+ <color name="green">#558B2F</color>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 82b942b..1caf41e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -16,7 +16,15 @@
<string name="servicedesc">my service</string>
<string name="aiddescription">my aid</string>
+ <!-- HistoryEvents -->
+ <string name="history_event_exchange_added">Exchange Added</string>
+ <string name="history_event_exchange_updated">Exchange Updated</string>
+ <string name="history_event_reserve_balance_updated">Reserve Balance Updated</string>
+ <string name="history_event_payment_sent">Payment Made</string>
<string name="history_event_withdrawn">Withdraw</string>
+ <string name="history_event_order_accepted">Order Confirmed</string>
+ <string name="history_event_order_refused">Order Cancelled</string>
+ <string name="history_event_refreshed">Refresh</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
diff --git a/app/src/test/java/net/taler/wallet/history/HistoryEventTest.kt b/app/src/test/java/net/taler/wallet/history/HistoryEventTest.kt
index bd1ff9b..d6d68f1 100644
--- a/app/src/test/java/net/taler/wallet/history/HistoryEventTest.kt
+++ b/app/src/test/java/net/taler/wallet/history/HistoryEventTest.kt
@@ -1,8 +1,25 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 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.history
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
+import net.taler.wallet.history.RefreshReason.PAY
import net.taler.wallet.history.ReserveType.MANUAL
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -15,6 +32,13 @@ class HistoryEventTest {
private val timestamp = Random.nextLong()
private val exchangeBaseUrl = "https://exchange.test.taler.net/"
+ private val orderShortInfo = OrderShortInfo(
+ proposalId = "EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG",
+ orderId = "2019.364-01RAQ68DQ7AWR",
+ merchantBaseUrl = "https://backend.demo.taler.net/public/instances/FSF/",
+ amount = "KUDOS:0.5",
+ summary = "Essay: Foreword"
+ )
@Test
fun `test ExchangeAddedEvent`() {
@@ -141,6 +165,153 @@ class HistoryEventTest {
}
@Test
+ fun `test OrderShortInfo`() {
+ val json = """{
+ "amount": "KUDOS:0.5",
+ "orderId": "2019.364-01RAQ68DQ7AWR",
+ "merchantBaseUrl": "https:\/\/backend.demo.taler.net\/public\/instances\/FSF\/",
+ "proposalId": "EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG",
+ "summary": "Essay: Foreword"
+ }""".trimIndent()
+ val info: OrderShortInfo = mapper.readValue(json)
+
+ assertEquals("KUDOS:0.5", info.amount)
+ assertEquals("2019.364-01RAQ68DQ7AWR", info.orderId)
+ assertEquals("Essay: Foreword", info.summary)
+ }
+
+ @Test
+ fun `test HistoryOrderAcceptedEvent`() {
+ val json = """{
+ "type": "order-accepted",
+ "eventId": "order-accepted;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG",
+ "orderShortInfo": {
+ "amount": "${orderShortInfo.amount}",
+ "orderId": "${orderShortInfo.orderId}",
+ "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}",
+ "proposalId": "${orderShortInfo.proposalId}",
+ "summary": "${orderShortInfo.summary}"
+ },
+ "timestamp": {
+ "t_ms": $timestamp
+ }
+ }""".trimIndent()
+ val event: HistoryOrderAcceptedEvent = mapper.readValue(json)
+
+ assertEquals(orderShortInfo, event.orderShortInfo)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryOrderRefusedEvent`() {
+ val json = """{
+ "type": "order-refused",
+ "eventId": "order-refused;9RJGAYXKWX0Y3V37H66606SXSA7V2CV255EBFS4G1JSH6W1EG7F0",
+ "orderShortInfo": {
+ "amount": "${orderShortInfo.amount}",
+ "orderId": "${orderShortInfo.orderId}",
+ "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}",
+ "proposalId": "${orderShortInfo.proposalId}",
+ "summary": "${orderShortInfo.summary}"
+ },
+ "timestamp": {
+ "t_ms": $timestamp
+ }
+ }""".trimIndent()
+ val event: HistoryOrderRefusedEvent = mapper.readValue(json)
+
+ assertEquals(orderShortInfo, event.orderShortInfo)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryPaymentSentEvent`() {
+ val json = """{
+ "type": "payment-sent",
+ "eventId": "payment-sent;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG",
+ "orderShortInfo": {
+ "amount": "${orderShortInfo.amount}",
+ "orderId": "${orderShortInfo.orderId}",
+ "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}",
+ "proposalId": "${orderShortInfo.proposalId}",
+ "summary": "${orderShortInfo.summary}"
+ },
+ "replay": false,
+ "sessionId": "e4f436c4-3c5c-4aee-81d2-26e425c09520",
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "numCoins": 6,
+ "amountPaidWithFees": "KUDOS:0.6"
+ }""".trimIndent()
+ val event: HistoryPaymentSentEvent = mapper.readValue(json)
+
+ assertEquals(orderShortInfo, event.orderShortInfo)
+ assertEquals(false, event.replay)
+ assertEquals(6, event.numCoins)
+ assertEquals("KUDOS:0.6", event.amountPaidWithFees)
+ assertEquals("e4f436c4-3c5c-4aee-81d2-26e425c09520", event.sessionId)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryPaymentSentEvent without sessionId`() {
+ val json = """{
+ "type": "payment-sent",
+ "eventId": "payment-sent;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG",
+ "orderShortInfo": {
+ "amount": "${orderShortInfo.amount}",
+ "orderId": "${orderShortInfo.orderId}",
+ "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}",
+ "proposalId": "${orderShortInfo.proposalId}",
+ "summary": "${orderShortInfo.summary}"
+ },
+ "replay": true,
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "numCoins": 6,
+ "amountPaidWithFees": "KUDOS:0.6"
+ }""".trimIndent()
+ val event: HistoryPaymentSentEvent = mapper.readValue(json)
+
+ assertEquals(orderShortInfo, event.orderShortInfo)
+ assertEquals(true, event.replay)
+ assertEquals(6, event.numCoins)
+ assertEquals("KUDOS:0.6", event.amountPaidWithFees)
+ assertEquals(null, event.sessionId)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
+ fun `test HistoryRefreshedEvent`() {
+ val json = """{
+ "type": "refreshed",
+ "refreshGroupId": "8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640",
+ "eventId": "refreshed;8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640",
+ "timestamp": {
+ "t_ms": $timestamp
+ },
+ "refreshReason": "pay",
+ "amountRefreshedEffective": "KUDOS:0",
+ "amountRefreshedRaw": "KUDOS:1",
+ "numInputCoins": 6,
+ "numOutputCoins": 0,
+ "numRefreshedInputCoins": 1
+ }""".trimIndent()
+ val event: HistoryRefreshedEvent = mapper.readValue(json)
+
+ assertEquals("KUDOS:0", event.amountRefreshedEffective)
+ assertEquals("KUDOS:1", event.amountRefreshedRaw)
+ assertEquals(6, event.numInputCoins)
+ assertEquals(0, event.numOutputCoins)
+ assertEquals(1, event.numRefreshedInputCoins)
+ assertEquals("8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", event.refreshGroupId)
+ assertEquals(PAY, event.refreshReason)
+ assertEquals(timestamp, event.timestamp.ms)
+ }
+
+ @Test
fun `test list of events as History`() {
val builtIn = Random.nextBoolean()
val json = """[
diff --git a/app/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt b/app/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt
index b5687c6..208995a 100644
--- a/app/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt
+++ b/app/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt
@@ -1,3 +1,19 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 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.history
import com.fasterxml.jackson.databind.ObjectMapper