gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-android] branch master updated (b1ae959 -> c20a794)


From: gnunet
Subject: [taler-wallet-android] branch master updated (b1ae959 -> c20a794)
Date: Thu, 30 Jan 2020 18:40:08 +0100

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

torsten-grote pushed a change to branch master
in repository wallet-android.

    from b1ae959  bump version / switch history order
     new 8903798  De-serialize first history events using Jackson
     new 35ba17d  Use special layout for withdraw event in wallet history
     new 1ec4443  Deserialize and render more wallet history events
     new 208d7ab  Hide detailed history events by default
     new 1e7ed72  Use +/- prefix for ammounts and hide 0 ammounts
     new 00596b8  Implement more events and make ViewHolders more generic
     new 14f218f  Use official User-Facing Terminology
     new 1ec5fbd  Show Refresh events to the user if there's a fee associated
     new 2371ad0  Show history event JSON in debug builds
     new c20a794  Don't crash on and show unknown events

The 10 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .idea/misc.xml                                     |   2 +-
 .idea/vcs.xml                                      |   6 +
 app/build.gradle                                   |   7 +-
 app/src/main/java/net/taler/wallet/Amount.kt       | 124 ++++++
 .../main/java/net/taler/wallet/WalletHistory.kt    | 131 ------
 .../main/java/net/taler/wallet/WalletViewModel.kt  |  95 ++---
 .../java/net/taler/wallet/history/HistoryEvent.kt  | 440 ++++++++++++++++++++
 .../JsonDialogFragment.kt}                         |  38 +-
 .../net/taler/wallet/history/ReserveTransaction.kt |  58 +++
 .../java/net/taler/wallet/history/WalletHistory.kt | 113 +++++
 .../taler/wallet/history/WalletHistoryAdapter.kt   | 229 ++++++++++
 .../main/res/drawable/history_payment_aborted.xml  |   9 +
 app/src/main/res/drawable/history_refresh.xml      |  12 +
 app/src/main/res/drawable/history_refund.xml       |   9 +
 app/src/main/res/drawable/history_tip_accepted.xml |   9 +
 app/src/main/res/drawable/history_tip_declined.xml |   9 +
 app/src/main/res/drawable/history_withdrawn.xml    |   9 +
 .../{ic_menu_send.xml => ic_account_balance.xml}   |   2 +-
 .../{ic_menu_send.xml => ic_add_circle.xml}        |   2 +-
 .../{ic_home_black_24dp.xml => ic_cancel.xml}      |   2 +-
 app/src/main/res/drawable/ic_cash_usd_outline.xml  |   9 +
 .../{ic_home_black_24dp.xml => ic_directions.xml}  |   2 +-
 app/src/main/res/layout/fragment_json.xml          |  25 ++
 app/src/main/res/layout/fragment_show_history.xml  |  42 +-
 app/src/main/res/layout/history_payment.xml        |  72 ++++
 app/src/main/res/layout/history_receive.xml        |  90 ++++
 app/src/main/res/layout/history_row.xml            |  72 +++-
 app/src/main/res/menu/history.xml                  |  17 +-
 app/src/main/res/navigation/nav_graph.xml          |   2 +-
 app/src/main/res/values/colors.xml                 |   3 +
 app/src/main/res/values/strings.xml                |  20 +
 app/src/main/res/values/styles.xml                 |   5 +
 .../net/taler/wallet/history/HistoryEventTest.kt   | 459 +++++++++++++++++++++
 .../taler/wallet/history/ReserveTransactionTest.kt |  52 +++
 34 files changed, 1925 insertions(+), 251 deletions(-)
 create mode 100644 .idea/vcs.xml
 create mode 100644 app/src/main/java/net/taler/wallet/Amount.kt
 delete mode 100644 app/src/main/java/net/taler/wallet/WalletHistory.kt
 create mode 100644 app/src/main/java/net/taler/wallet/history/HistoryEvent.kt
 copy app/src/main/java/net/taler/wallet/{PaymentSuccessful.kt => 
history/JsonDialogFragment.kt} (55%)
 create mode 100644 
app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
 create mode 100644 app/src/main/java/net/taler/wallet/history/WalletHistory.kt
 create mode 100644 
app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
 create mode 100644 app/src/main/res/drawable/history_payment_aborted.xml
 create mode 100644 app/src/main/res/drawable/history_refresh.xml
 create mode 100644 app/src/main/res/drawable/history_refund.xml
 create mode 100644 app/src/main/res/drawable/history_tip_accepted.xml
 create mode 100644 app/src/main/res/drawable/history_tip_declined.xml
 create mode 100644 app/src/main/res/drawable/history_withdrawn.xml
 copy app/src/main/res/drawable/{ic_menu_send.xml => ic_account_balance.xml} 
(66%)
 copy app/src/main/res/drawable/{ic_menu_send.xml => ic_add_circle.xml} (64%)
 copy app/src/main/res/drawable/{ic_home_black_24dp.xml => ic_cancel.xml} (55%)
 create mode 100644 app/src/main/res/drawable/ic_cash_usd_outline.xml
 copy app/src/main/res/drawable/{ic_home_black_24dp.xml => ic_directions.xml} 
(51%)
 create mode 100644 app/src/main/res/layout/fragment_json.xml
 create mode 100644 app/src/main/res/layout/history_payment.xml
 create mode 100644 app/src/main/res/layout/history_receive.xml
 create mode 100644 
app/src/test/java/net/taler/wallet/history/HistoryEventTest.kt
 create mode 100644 
app/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt

diff --git a/.idea/misc.xml b/.idea/misc.xml
index b6ea2b1..7bfef59 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" 
project-jdk-name="JDK" project-jdk-type="JavaSDK">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" 
project-jdk-name="1.8" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>
   <component name="ProjectType">
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 5085681..c40ddef 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -45,14 +45,15 @@ dependencies {
     implementation project(":akono")
     implementation 'com.google.guava:guava:28.0-android'
 
-    def nav_version = "2.2.0-rc03"
+    def nav_version = "2.2.0-rc04"
     implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
     implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
 
     // ViewModel and LiveData
-    def lifecycle_version = "2.1.0"
+    def lifecycle_version = "2.2.0-rc03"
     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'
@@ -62,5 +63,5 @@ dependencies {
     implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1'
 
     // JSON parsing and serialization
-    implementation 'com.google.code.gson:gson:2.8.6'
+    implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.7"
 }
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..2b41be1
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/Amount.kt
@@ -0,0 +1,124 @@
+/*
+ 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 isZero(): Boolean {
+        return value == 0u && fraction == 0.0
+    }
+
+    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/WalletHistory.kt 
b/app/src/main/java/net/taler/wallet/WalletHistory.kt
deleted file mode 100644
index b9db0c1..0000000
--- a/app/src/main/java/net/taler/wallet/WalletHistory.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- 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
-
-
-import android.os.Bundle
-import android.view.*
-import androidx.fragment.app.Fragment
-import android.widget.TextView
-import androidx.lifecycle.ViewModelProviders
-import androidx.recyclerview.widget.DividerItemDecoration
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-
-class WalletHistoryAdapter(private var myDataset: HistoryResult) : 
RecyclerView.Adapter<WalletHistoryAdapter.MyViewHolder>() {
-
-    init {
-        setHasStableIds(false)
-    }
-
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
MyViewHolder {
-        val rowView = 
LayoutInflater.from(parent.context).inflate(R.layout.history_row, parent, false)
-        return MyViewHolder(rowView)
-    }
-
-    override fun getItemCount(): Int {
-        return myDataset.history.size
-    }
-
-    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
-        val h = myDataset.history[myDataset.history.size - position - 1]
-        val text = holder.rowView.findViewById<TextView>(R.id.history_text)
-        val subText = 
holder.rowView.findViewById<TextView>(R.id.history_subtext)
-        text.text = h.type
-        subText.text = h.timestamp.toString() + "\n" + h.detail.toString(1)
-    }
-
-    fun update(updatedHistory: HistoryResult) {
-        this.myDataset = updatedHistory
-        this.notifyDataSetChanged()
-    }
-
-    class MyViewHolder(val rowView: View) : RecyclerView.ViewHolder(rowView)
-}
-
-
-/**
- * Wallet history.
- *
- */
-class WalletHistory : Fragment() {
-
-    lateinit var model: WalletViewModel
-    lateinit var historyAdapter: WalletHistoryAdapter
-    lateinit var historyPlaceholder: View
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setHasOptionsMenu(true)
-
-        historyAdapter = WalletHistoryAdapter(HistoryResult(listOf()))
-
-        model = activity?.run {
-            ViewModelProviders.of(this)[WalletViewModel::class.java]
-        } ?: throw Exception("Invalid Activity")
-
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
-        activity?.menuInflater?.inflate(R.menu.history, menu)
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        return when (item.itemId) {
-            R.id.reload_history -> {
-                updateHistory()
-                true
-            }
-            else -> super.onOptionsItemSelected(item)
-        }
-    }
-
-    private fun updateHistory() {
-        model.getHistory {
-            if (it.history.size == 0) {
-                historyPlaceholder.visibility = View.VISIBLE
-            }
-            historyAdapter.update(it)
-        }
-    }
-
-    override fun onCreateView(
-        inflater: LayoutInflater, container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View? {
-        // Inflate the layout for this fragment
-        val view = inflater.inflate(R.layout.fragment_show_history, container, 
false)
-        val myLayoutManager = LinearLayoutManager(context)
-        val myItemDecoration = DividerItemDecoration(context, 
myLayoutManager.orientation)
-        view.findViewById<RecyclerView>(R.id.list_history).apply {
-            layoutManager = myLayoutManager
-            adapter = historyAdapter
-            addItemDecoration(myItemDecoration)
-        }
-
-        historyPlaceholder = 
view.findViewById<View>(R.id.list_history_placeholder)
-        historyPlaceholder.visibility = View.GONE
-
-        updateHistory()
-
-        return view
-    }
-
-    companion object {
-        const val TAG = "taler-wallet"
-    }
-}
diff --git a/app/src/main/java/net/taler/wallet/WalletViewModel.kt 
b/app/src/main/java/net/taler/wallet/WalletViewModel.kt
index b933bf1..ad41e77 100644
--- a/app/src/main/java/net/taler/wallet/WalletViewModel.kt
+++ b/app/src/main/java/net/taler/wallet/WalletViewModel.kt
@@ -18,41 +18,24 @@ package net.taler.wallet
 
 import android.app.Application
 import android.util.Log
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.MutableLiveData
-import com.google.android.material.snackbar.Snackbar
+import androidx.lifecycle.*
+import 
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import com.fasterxml.jackson.module.kotlin.readValue
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onStart
 import net.taler.wallet.backend.WalletBackendApi
+import net.taler.wallet.history.History
+import net.taler.wallet.history.HistoryEvent
 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)
 
 
@@ -84,6 +67,7 @@ open class WithdrawStatus {
         val tosText: String,
         val tosEtag: String
     ) : WithdrawStatus()
+
     class Success : WithdrawStatus()
     data class ReceivedDetails(
         val talerWithdrawUri: String,
@@ -94,16 +78,6 @@ open class WithdrawStatus {
     data class Withdrawing(val talerWithdrawUri: String) : WithdrawStatus()
 }
 
-open class HistoryResult(
-    val history: List<HistoryEntry>
-)
-
-open class HistoryEntry(
-    val detail: JSONObject,
-    val type: String,
-    val timestamp: JSONObject
-)
-
 open class PendingOperationInfo(
     val type: String,
     val detail: JSONObject
@@ -114,6 +88,7 @@ open class PendingOperations(
 )
 
 
+@Suppress("EXPERIMENTAL_API_USAGE")
 class WalletViewModel(val app: Application) : AndroidViewModel(app) {
     private var initialized = false
 
@@ -137,6 +112,18 @@ class WalletViewModel(val app: Application) : 
AndroidViewModel(app) {
         value = PendingOperations(listOf())
     }
 
+    private val mHistoryProgress = MutableLiveData<Boolean>()
+    val historyProgress: LiveData<Boolean> = mHistoryProgress
+
+    val historyShowAll = MutableLiveData<Boolean>()
+
+    val history: LiveData<History> = historyShowAll.switchMap { showAll ->
+        loadHistory(showAll)
+            .onStart { mHistoryProgress.postValue(true) }
+            .onCompletion { mHistoryProgress.postValue(false) }
+            .asLiveData(Dispatchers.IO)
+    }
+
     private var activeGetBalance = 0
     private var activeGetPending = 0
 
@@ -144,6 +131,9 @@ class WalletViewModel(val app: Application) : 
AndroidViewModel(app) {
     private var currentWithdrawRequestId = 0
 
     private val walletBackendApi = WalletBackendApi(app)
+    private val mapper = ObjectMapper()
+        .registerModule(KotlinModule())
+        .configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
 
     fun init() {
         if (initialized) {
@@ -220,24 +210,27 @@ class WalletViewModel(val app: Application) : 
AndroidViewModel(app) {
         }
     }
 
-    fun getHistory(cb: (r: HistoryResult) -> Unit) {
+    private fun loadHistory(showAll: Boolean) = callbackFlow {
+        mHistoryProgress.postValue(true)
         walletBackendApi.sendRequest("getHistory", null) { isError, result ->
             if (isError) {
+                // TODO show error message in [WalletHistory] fragment
+                close()
                 return@sendRequest
             }
-            val historyEntries = mutableListOf<HistoryEntry>()
-            val historyList = result.getJSONArray("history")
-            for (i in 0 until historyList.length()) {
-                val h = historyList.getJSONObject(i)
-                Log.v(TAG, "got history entry $h")
-                val type = h.getString("type")
-                Log.v(TAG, "got history entry type $type")
-                val detail = h
-                val timestamp = h.getJSONObject("timestamp")
-                historyEntries.add(HistoryEntry(detail, type, timestamp))
+            val history = History()
+            val json = result.getJSONArray("history")
+            for (i in 0 until json.length()) {
+                val event: HistoryEvent = mapper.readValue(json.getString(i))
+                event.json = json.getJSONObject(i)
+                history.add(event)
             }
-            cb(HistoryResult(historyEntries))
+            history.reverse()  // show latest first
+            mHistoryProgress.postValue(false)
+            offer(if (showAll) history else history.filter { it.showToUser } 
as History)
+            close()
         }
+        awaitClose()
     }
 
     fun withdrawTestkudos() {
diff --git a/app/src/main/java/net/taler/wallet/history/HistoryEvent.kt 
b/app/src/main/java/net/taler/wallet/history/HistoryEvent.kt
new file mode 100644
index 0000000..787b430
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/history/HistoryEvent.kt
@@ -0,0 +1,440 @@
+/*
+ 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.ParsedAmount.Companion.parseAmount
+import net.taler.wallet.R
+import org.json.JSONObject
+
+enum class ReserveType {
+    /**
+     * Manually created.
+     */
+    @JsonProperty("manual")
+    MANUAL,
+    /**
+     * Withdrawn from a bank that has "tight" Taler integration
+     */
+    @JsonProperty("taler-bank-withdraw")
+    TALER_BANK_WITHDRAW,
+}
+
+@JsonInclude(NON_EMPTY)
+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)
+class Timestamp(
+    @JsonProperty("t_ms")
+    val ms: Long
+)
+
+@JsonInclude(NON_EMPTY)
+class ReserveShortInfo(
+    /**
+     * The exchange that the reserve will be at.
+     */
+    val exchangeBaseUrl: String,
+    /**
+     * Key to query more details
+     */
+    val reservePub: String,
+    /**
+     * Detail about how the reserve has been created.
+     */
+    val reserveCreationDetail: ReserveCreationDetail
+)
+
+typealias History = ArrayList<HistoryEvent>
+
+@JsonTypeInfo(
+    use = NAME,
+    include = PROPERTY,
+    property = "type",
+    defaultImpl = HistoryUnknownEvent::class
+)
+/** missing:
+AuditorComplaintSent = "auditor-complained-sent",
+AuditorComplaintProcessed = "auditor-complaint-processed",
+AuditorTrustAdded = "auditor-trust-added",
+AuditorTrustRemoved = "auditor-trust-removed",
+ExchangeTermsAccepted = "exchange-terms-accepted",
+ExchangePolicyChanged = "exchange-policy-changed",
+ExchangeTrustAdded = "exchange-trust-added",
+ExchangeTrustRemoved = "exchange-trust-removed",
+FundsDepositedToSelf = "funds-deposited-to-self",
+FundsRecouped = "funds-recouped",
+ReserveCreated = "reserve-created",
+ */
+@JsonSubTypes(
+    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 = HistoryOrderAcceptedEvent::class, name = "order-accepted"),
+    Type(value = HistoryOrderRefusedEvent::class, name = "order-refused"),
+    Type(value = HistoryOrderRedirectedEvent::class, name = 
"order-redirected"),
+    Type(value = HistoryPaymentSentEvent::class, name = "payment-sent"),
+    Type(value = HistoryPaymentAbortedEvent::class, name = "payment-aborted"),
+    Type(value = HistoryTipAcceptedEvent::class, name = "tip-accepted"),
+    Type(value = HistoryTipDeclinedEvent::class, name = "tip-declined"),
+    Type(value = HistoryRefundedEvent::class, name = "refund"),
+    Type(value = HistoryRefreshedEvent::class, name = "refreshed")
+)
+@JsonIgnoreProperties(
+    value = [
+        "eventId"
+    ]
+)
+abstract class HistoryEvent(
+    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,
+    open val showToUser: Boolean = false
+) {
+    open lateinit var json: JSONObject
+}
+
+
+class HistoryUnknownEvent(timestamp: Timestamp) : HistoryEvent(timestamp) {
+    override val title = R.string.history_event_unknown
+}
+
+@JsonTypeName("exchange-added")
+class ExchangeAddedEvent(
+    timestamp: Timestamp,
+    val exchangeBaseUrl: String,
+    val builtIn: Boolean
+) : HistoryEvent(timestamp) {
+    override val title = R.string.history_event_exchange_added
+}
+
+@JsonTypeName("exchange-updated")
+class ExchangeUpdatedEvent(
+    timestamp: Timestamp,
+    val exchangeBaseUrl: String
+) : HistoryEvent(timestamp) {
+    override val title = R.string.history_event_exchange_updated
+}
+
+
+@JsonTypeName("reserve-balance-updated")
+class ReserveBalanceUpdatedEvent(
+    timestamp: Timestamp,
+    val newHistoryTransactions: List<ReserveTransaction>,
+    /**
+     * Condensed information about the reserve.
+     */
+    val reserveShortInfo: ReserveShortInfo,
+    /**
+     * Amount currently left in the reserve.
+     */
+    val amountReserveBalance: String,
+    /**
+     * Amount we expected to be in the reserve at that time,
+     * considering ongoing withdrawals from that reserve.
+     */
+    val amountExpected: String
+) : HistoryEvent(timestamp) {
+    override val title = R.string.history_event_reserve_balance_updated
+}
+
+@JsonTypeName("withdrawn")
+class HistoryWithdrawnEvent(
+    timestamp: Timestamp,
+    /**
+     * Exchange that was withdrawn from.
+     */
+    val exchangeBaseUrl: String,
+    /**
+     * Unique identifier for the withdrawal session, can be used to
+     * query more detailed information from the wallet.
+     */
+    val withdrawSessionId: String,
+    val withdrawalSource: WithdrawalSource,
+    /**
+     * Amount that has been subtracted from the reserve's balance
+     * for this withdrawal.
+     */
+    val amountWithdrawnRaw: String,
+    /**
+     * Amount that actually was added to the wallet's balance.
+     */
+    val amountWithdrawnEffective: String
+) : HistoryEvent(timestamp) {
+    override val layout = R.layout.history_receive
+    override val title = R.string.history_event_withdrawn
+    override val icon = R.drawable.history_withdrawn
+    override val showToUser = true
+}
+
+@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
+    override val title = R.string.history_event_payment_sent
+    override val icon = R.drawable.ic_cash_usd_outline
+    override val showToUser = true
+}
+
+@JsonTypeName("payment-aborted")
+class HistoryPaymentAbortedEvent(
+    timestamp: Timestamp,
+    /**
+     * Condensed info about the order that we already paid for.
+     */
+    val orderShortInfo: OrderShortInfo,
+    /**
+     * Amount that was lost due to refund and refreshing fees.
+     */
+    val amountLost: String
+) : HistoryEvent(timestamp) {
+    override val layout = R.layout.history_payment
+    override val title = R.string.history_event_payment_aborted
+    override val icon = R.drawable.history_payment_aborted
+    override val showToUser = true
+}
+
+@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 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()
+}
+
+@JsonTypeName("order-redirected")
+class HistoryOrderRedirectedEvent(
+    timestamp: Timestamp,
+    /**
+     * Condensed info about the new order that contains a
+     * product (identified by the fulfillment URL) that we've already paid for.
+     */
+    val newOrderShortInfo: OrderShortInfo,
+    /**
+     * Condensed info about the order that we already paid for.
+     */
+    val alreadyPaidOrderShortInfo: OrderShortInfo
+) : HistoryEvent(timestamp) {
+    override val icon = R.drawable.ic_directions
+    override val title = R.string.history_event_order_redirected
+}
+
+@JsonTypeName("tip-accepted")
+class HistoryTipAcceptedEvent(
+    timestamp: Timestamp,
+    /**
+     * Unique identifier for the tip to query more information.
+     */
+    val tipId: String,
+    /**
+     * Raw amount of the tip, without extra fees that apply.
+     */
+    val tipRaw: String
+) : HistoryEvent(timestamp) {
+    override val icon = R.drawable.history_tip_accepted
+    override val title = R.string.history_event_tip_accepted
+    override val layout = R.layout.history_receive
+    override val showToUser = true
+}
+
+@JsonTypeName("tip-declined")
+class HistoryTipDeclinedEvent(
+    timestamp: Timestamp,
+    /**
+     * Unique identifier for the tip to query more information.
+     */
+    val tipId: String,
+    /**
+     * Raw amount of the tip, without extra fees that apply.
+     */
+    val tipAmount: String
+) : HistoryEvent(timestamp) {
+    override val icon = R.drawable.history_tip_declined
+    override val title = R.string.history_event_tip_declined
+    override val layout = R.layout.history_receive
+    override val showToUser = true
+}
+
+@JsonTypeName("refund")
+class HistoryRefundedEvent(
+    timestamp: Timestamp,
+    val orderShortInfo: OrderShortInfo,
+    /**
+     * Unique identifier for this refund.
+     * (Identifies multiple refund permissions that were obtained at once.)
+     */
+    val refundGroupId: String,
+    /**
+     * Part of the refund that couldn't be applied because
+     * the refund permissions were expired.
+     */
+    val amountRefundedInvalid: String,
+    /**
+     * Amount that has been refunded by the merchant.
+     */
+    val amountRefundedRaw: String,
+    /**
+     * Amount will be added to the wallet's balance after fees and refreshing.
+     */
+    val amountRefundedEffective: String
+) : HistoryEvent(timestamp) {
+    override val icon = R.drawable.history_refund
+    override val title = R.string.history_event_refund
+    override val layout = R.layout.history_receive
+    override val showToUser = true
+}
+
+@JsonTypeInfo(
+    use = NAME,
+    include = PROPERTY,
+    property = "type"
+)
+@JsonSubTypes(
+    Type(value = WithdrawalSourceReserve::class, name = "reserve")
+)
+abstract class WithdrawalSource
+
+@JsonTypeName("tip")
+class WithdrawalSourceTip(
+    val tipId: String
+) : WithdrawalSource()
+
+@JsonTypeName("reserve")
+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/PaymentSuccessful.kt 
b/app/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt
similarity index 55%
copy from app/src/main/java/net/taler/wallet/PaymentSuccessful.kt
copy to app/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt
index 6332c39..a9ed514 100644
--- a/app/src/main/java/net/taler/wallet/PaymentSuccessful.kt
+++ b/app/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt
@@ -14,33 +14,37 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-
-package net.taler.wallet
+package net.taler.wallet.history
 
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
-import android.widget.Button
-import androidx.fragment.app.Fragment
-import androidx.navigation.findNavController
+import androidx.fragment.app.DialogFragment
+import kotlinx.android.synthetic.main.fragment_json.*
+import net.taler.wallet.R
 
+class JsonDialogFragment : DialogFragment() {
 
-/**
- * Fragment that shows the success message for a payment.
- *
- */
-class PaymentSuccessful : Fragment() {
+    companion object {
+        fun new(json: String): JsonDialogFragment {
+            return JsonDialogFragment().apply {
+                arguments = Bundle().apply { putString("json", json) }
+            }
+        }
+    }
 
     override fun onCreateView(
-        inflater: LayoutInflater, container: ViewGroup?,
+        inflater: LayoutInflater,
+        container: ViewGroup?,
         savedInstanceState: Bundle?
     ): View? {
-        // Inflate the layout for this fragment
-        val view = inflater.inflate(R.layout.fragment_payment_successful, 
container, false)
-        view.findViewById<Button>(R.id.button_success_back).setOnClickListener 
{
-            activity!!.findNavController(R.id.nav_host_fragment).navigateUp()
-        }
-        return view
+        return inflater.inflate(R.layout.fragment_json, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        val json = arguments!!.getString("json")
+        jsonView.text = json
     }
+
 }
diff --git a/app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt 
b/app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
new file mode 100644
index 0000000..f4cfcb8
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
@@ -0,0 +1,58 @@
+/*
+ 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
+import com.fasterxml.jackson.annotation.JsonSubTypes
+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
+
+
+@JsonTypeInfo(
+    use = NAME,
+    include = PROPERTY,
+    property = "type"
+)
+@JsonSubTypes(
+    JsonSubTypes.Type(value = ReserveDepositTransaction::class, name = 
"DEPOSIT")
+)
+abstract class ReserveTransaction
+
+
+@JsonTypeName("DEPOSIT")
+class ReserveDepositTransaction(
+    /**
+     * Amount withdrawn.
+     */
+    val amount: String,
+    /**
+     * Sender account payto://-URL
+     */
+    @JsonProperty("sender_account_url")
+    val senderAccountUrl: String,
+    /**
+     * Transfer details uniquely identifying the transfer.
+     */
+    @JsonProperty("wire_reference")
+    val wireReference: String,
+    /**
+     * Timestamp of the incoming wire transfer.
+     */
+    val timestamp: Timestamp
+) : ReserveTransaction()
diff --git a/app/src/main/java/net/taler/wallet/history/WalletHistory.kt 
b/app/src/main/java/net/taler/wallet/history/WalletHistory.kt
new file mode 100644
index 0000000..5652e66
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/history/WalletHistory.kt
@@ -0,0 +1,113 @@
+/*
+ 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.os.Bundle
+import android.view.*
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import kotlinx.android.synthetic.main.fragment_show_history.*
+import net.taler.wallet.R
+import net.taler.wallet.WalletViewModel
+
+interface OnEventClickListener {
+    fun onEventClicked(event: HistoryEvent)
+}
+
+/**
+ * Wallet history.
+ *
+ */
+class WalletHistory : Fragment(), OnEventClickListener {
+
+    lateinit var model: WalletViewModel
+    private lateinit var showAllItem: MenuItem
+    private val historyAdapter = WalletHistoryAdapter(this)
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setHasOptionsMenu(true)
+
+        model = activity?.run {
+            ViewModelProvider(this)[WalletViewModel::class.java]
+        } ?: throw Exception("Invalid Activity")
+
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.history, menu)
+        showAllItem = menu.findItem(R.id.show_all_history)
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return when (item.itemId) {
+            R.id.show_all_history -> {
+                item.isChecked = !item.isChecked
+                model.historyShowAll.value = item.isChecked
+                true
+            }
+            R.id.reload_history -> {
+                model.historyShowAll.value = showAllItem.isChecked
+                true
+            }
+            else -> super.onOptionsItemSelected(item)
+        }
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater, container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_show_history, container, 
false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        historyList.apply {
+            val myLayoutManager = LinearLayoutManager(context)
+            val myItemDecoration = DividerItemDecoration(context, 
myLayoutManager.orientation)
+            layoutManager = myLayoutManager
+            adapter = historyAdapter
+            addItemDecoration(myItemDecoration)
+        }
+
+        model.historyProgress.observe(this, Observer { show ->
+            historyProgressBar.visibility = if (show) VISIBLE else INVISIBLE
+        })
+        model.history.observe(this, Observer { history ->
+            historyEmptyState.visibility = if (history.isEmpty()) VISIBLE else 
INVISIBLE
+            historyAdapter.update(history)
+        })
+
+        // kicks off initial load, needs to be adapted if showAll state is 
ever saved
+        if (savedInstanceState == null) model.historyShowAll.value = false
+    }
+
+    override fun onEventClicked(event: HistoryEvent) {
+        JsonDialogFragment.new(event.json.toString(4))
+            .show(parentFragmentManager, null)
+    }
+
+    companion object {
+        const val TAG = "taler-wallet"
+    }
+}
diff --git a/app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt 
b/app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
new file mode 100644
index 0000000..7df8d0c
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
@@ -0,0 +1,229 @@
+/*
+ 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.graphics.Paint.STRIKE_THRU_TEXT_FLAG
+import android.text.format.DateUtils.*
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.CallSuper
+import androidx.core.net.toUri
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import net.taler.wallet.BuildConfig
+import net.taler.wallet.ParsedAmount
+import net.taler.wallet.ParsedAmount.Companion.parseAmount
+import net.taler.wallet.R
+
+
+internal class WalletHistoryAdapter(
+    private val listener: OnEventClickListener,
+    private var history: History = History()
+) : Adapter<WalletHistoryAdapter.HistoryEventViewHolder>() {
+
+    init {
+        setHasStableIds(false)
+    }
+
+    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_receive -> HistoryReceiveViewHolder(view)
+            R.layout.history_payment -> HistoryPaymentViewHolder(view)
+            else -> GenericHistoryEventViewHolder(view)
+        }
+    }
+
+    override fun getItemCount(): Int = history.size
+
+    override fun onBindViewHolder(holder: HistoryEventViewHolder, position: 
Int) {
+        val event = history[position]
+        holder.bind(event)
+    }
+
+    fun update(updatedHistory: History) {
+        this.history = updatedHistory
+        this.notifyDataSetChanged()
+    }
+
+    internal abstract inner class HistoryEventViewHolder(protected val v: 
View) : ViewHolder(v) {
+
+        private val icon: ImageView = v.findViewById(R.id.icon)
+        protected val title: TextView = v.findViewById(R.id.title)
+        private val time: TextView = v.findViewById(R.id.time)
+
+        @CallSuper
+        open fun bind(event: HistoryEvent) {
+            if (BuildConfig.DEBUG) {  // doesn't produce recycling issues, no 
need to cover all cases
+                v.setOnClickListener { listener.onEventClicked(event) }
+            } else {
+                v.background = null
+            }
+            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)
+        }
+
+        private fun getRelativeTime(timestamp: Long): CharSequence {
+            val now = System.currentTimeMillis()
+            return if (now - timestamp > DAY_IN_MILLIS * 2) {
+                formatDateTime(
+                    v.context,
+                    timestamp,
+                    FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or 
FORMAT_ABBREV_MONTH or FORMAT_NO_YEAR
+                )
+            } else {
+                getRelativeTimeSpanString(timestamp, now, MINUTE_IN_MILLIS, 
FORMAT_ABBREV_RELATIVE)
+            }
+        }
+
+    }
+
+    internal inner 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 HistoryOrderRedirectedEvent -> 
event.newOrderShortInfo.summary
+                else -> ""
+            }
+        }
+
+    }
+
+    internal inner class HistoryReceiveViewHolder(v: View) : 
HistoryEventViewHolder(v) {
+
+        private val summary: TextView = v.findViewById(R.id.summary)
+        private val amountWithdrawn: TextView = 
v.findViewById(R.id.amountWithdrawn)
+        private val feeLabel: TextView = v.findViewById(R.id.feeLabel)
+        private val fee: TextView = v.findViewById(R.id.fee)
+
+        override fun bind(event: HistoryEvent) {
+            super.bind(event)
+            when (event) {
+                is HistoryWithdrawnEvent -> bind(event)
+                is HistoryRefundedEvent -> bind(event)
+                is HistoryTipAcceptedEvent -> bind(event)
+                is HistoryTipDeclinedEvent -> bind(event)
+            }
+        }
+
+        private fun bind(event: HistoryWithdrawnEvent) {
+            title.text = getHostname(event.exchangeBaseUrl)
+            summary.setText(event.title)
+
+            val parsedEffective = parseAmount(event.amountWithdrawnEffective)
+            val parsedRaw = parseAmount(event.amountWithdrawnRaw)
+            showAmounts(parsedEffective, parsedRaw)
+        }
+
+        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)
+        }
+
+        private fun bind(event: HistoryTipAcceptedEvent) {
+            title.setText(event.title)
+            summary.text = null
+            val amount = parseAmount(event.tipRaw)
+            showAmounts(amount, amount)
+        }
+
+        private fun bind(event: HistoryTipDeclinedEvent) {
+            title.setText(event.title)
+            summary.text = null
+            val amount = parseAmount(event.tipAmount)
+            showAmounts(amount, amount)
+            amountWithdrawn.paintFlags = amountWithdrawn.paintFlags or 
STRIKE_THRU_TEXT_FLAG
+        }
+
+        private fun showAmounts(effective: ParsedAmount, raw: ParsedAmount) {
+            amountWithdrawn.text = "+$raw"
+            val calculatedFee = raw - effective
+            if (calculatedFee.isZero()) {
+                fee.visibility = GONE
+                feeLabel.visibility = GONE
+            } else {
+                fee.text = "-$calculatedFee"
+                fee.visibility = VISIBLE
+                feeLabel.visibility = VISIBLE
+            }
+            amountWithdrawn.paintFlags = fee.paintFlags
+        }
+
+        private fun getHostname(url: String): String {
+            return url.toUri().host!!
+        }
+
+    }
+
+    internal inner class HistoryPaymentViewHolder(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)
+            summary.setText(event.title)
+            when (event) {
+                is HistoryPaymentSentEvent -> bind(event)
+                is HistoryPaymentAbortedEvent -> bind(event)
+                is HistoryRefreshedEvent -> bind(event)
+            }
+        }
+
+        private fun bind(event: HistoryPaymentSentEvent) {
+            title.text = event.orderShortInfo.summary
+            amountPaidWithFees.text = 
"-${parseAmount(event.amountPaidWithFees)}"
+        }
+
+        private fun bind(event: HistoryPaymentAbortedEvent) {
+            title.text = event.orderShortInfo.summary
+            amountPaidWithFees.text = "-${parseAmount(event.amountLost)}"
+        }
+
+        private fun bind(event: HistoryRefreshedEvent) {
+            title.text = ""
+            val fee =
+                parseAmount(event.amountRefreshedRaw) - 
parseAmount(event.amountRefreshedEffective)
+            if (fee.isZero()) amountPaidWithFees.text = null
+            else amountPaidWithFees.text = "-$fee"
+        }
+
+    }
+
+}
diff --git a/app/src/main/res/drawable/history_payment_aborted.xml 
b/app/src/main/res/drawable/history_payment_aborted.xml
new file mode 100644
index 0000000..ffe74a5
--- /dev/null
+++ b/app/src/main/res/drawable/history_payment_aborted.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="M15.46 18.12L16.88 19.54L19 17.41L21.12 
19.54L22.54 18.12L20.41 16L22.54 13.88L21.12 12.46L19 14.59L16.88 12.46L15.46 
13.88L17.59 16M14.97 11.62C14.86 10.28 13.58 8.97 12 9C10.3 9.04 9 10.3 9 12C9 
13.7 10.3 14.94 12 15C12.39 15 12.77 14.92 13.14 14.77C13.41 13.67 13.86 12.63 
14.97 11.62M13 16H7C7 14.9 6.1 14 5 14V10C6.1 10 7 9.1 7 8H17C17 9.1 17.9 10 19 
10V10.05C19.67 10.06 20.34 10.18 21 10.4V6H3V18H13.32C13.1 17.33 13 16.66 13 
16Z" />
+</vector>
diff --git a/app/src/main/res/drawable/history_refresh.xml 
b/app/src/main/res/drawable/history_refresh.xml
new file mode 100644
index 0000000..58d11dd
--- /dev/null
+++ b/app/src/main/res/drawable/history_refresh.xml
@@ -0,0 +1,12 @@
+<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="#FF000000"
+            android:pathData="M14.97,11.62C14.86,10.28 13.58,8.97 
12,9c-1.7,0.04 -3,1.3 -3,3 0,1.7 1.3,2.94 3,3 0.39,0 0.77,-0.08 1.14,-0.23 
0.27,-1.1 0.72,-2.14 1.83,-3.15M13,16H7C7,14.9 6.1,14 5,14V10C6.1,10 7,9.1 
7,8h10c0,1.1 0.9,2 2,2v0.05c0.67,0.01 1.34,0.13 2,0.35V6H3V18H13.32C13.1,17.33 
13,16.66 13,16Z" />
+    <path
+            android:fillColor="#FF000000"
+            android:pathData="M19,12 L16.75,14.25 19,16.5V15c1.38,0 2.5,1.12 
2.5,2.5 0,0.4 -0.09,0.78 -0.26,1.12l1.09,1.09C22.75,19.08 23,18.32 
23,17.5c0,-2.21 -1.79,-4 -4,-4V12m-3.33,3.29C15.25,15.92 15,16.68 
15,17.5c0,2.21 1.79,4 4,4V23L21.25,20.75 19,18.5V20c-1.38,0 -2.5,-1.12 
-2.5,-2.5 0,-0.4 0.09,-0.78 0.26,-1.12z" />
+</vector>
diff --git a/app/src/main/res/drawable/history_refund.xml 
b/app/src/main/res/drawable/history_refund.xml
new file mode 100644
index 0000000..0d2a946
--- /dev/null
+++ b/app/src/main/res/drawable/history_refund.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="M3,11H21V23H3V11M12,15A2,2 0 0,1 14,17A2,2 0 0,1 
12,19A2,2 0 0,1 10,17A2,2 0 0,1 12,15M7,13A2,2 0 0,1 5,15V19A2,2 0 0,1 
7,21H17A2,2 0 0,1 19,19V15A2,2 0 0,1 
17,13H7M17,5V10H15.5V6.5H9.88L12.3,8.93L11.24,10L7,5.75L11.24,1.5L12.3,2.57L9.88,5H17Z"
 />
+</vector>
diff --git a/app/src/main/res/drawable/history_tip_accepted.xml 
b/app/src/main/res/drawable/history_tip_accepted.xml
new file mode 100644
index 0000000..b4a0934
--- /dev/null
+++ b/app/src/main/res/drawable/history_tip_accepted.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="M18,21L15,18L18,15V17H22V19H18V21M10,4A4,4 0 0,1 
14,8A4,4 0 0,1 10,12A4,4 0 0,1 6,8A4,4 0 0,1 10,4M10,14C11.15,14 12.25,14.12 
13.24,14.34C12.46,15.35 12,16.62 12,18C12,18.7 12.12,19.37 
12.34,20H2V18C2,15.79 5.58,14 10,14Z" />
+</vector>
diff --git a/app/src/main/res/drawable/history_tip_declined.xml 
b/app/src/main/res/drawable/history_tip_declined.xml
new file mode 100644
index 0000000..6d490f9
--- /dev/null
+++ b/app/src/main/res/drawable/history_tip_declined.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="M10 4A4 4 0 0 0 6 8A4 4 0 0 0 10 12A4 4 0 0 0 14 
8A4 4 0 0 0 10 4M17.5 13C15 13 13 15 13 17.5C13 20 15 22 17.5 22C20 22 22 20 22 
17.5C22 15 20 13 17.5 13M10 14C5.58 14 2 15.79 2 18V20H11.5A6.5 6.5 0 0 1 11 
17.5A6.5 6.5 0 0 1 11.95 14.14C11.32 14.06 10.68 14 10 14M17.5 14.5C19.16 14.5 
20.5 15.84 20.5 17.5C20.5 18.06 20.35 18.58 20.08 19L16 14.92C16.42 14.65 16.94 
14.5 17.5 14.5M14.92 16L19 20.08C18.58 20.35 18.06 20.5 17.5 20.5C15.84 20.5 
14.5 19.16 14.5 17.5 [...]
+</vector>
diff --git a/app/src/main/res/drawable/history_withdrawn.xml 
b/app/src/main/res/drawable/history_withdrawn.xml
new file mode 100644
index 0000000..f6bb884
--- /dev/null
+++ b/app/src/main/res/drawable/history_withdrawn.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="M3 0V3H0V5H3V8H5V5H8V3H5V0H3M9 3V6H6V9H3V19C3 
20.1 3.89 21 5 21H19C20.11 21 21 20.11 21 19V18H12C10.9 18 10 17.11 10 16V8C10 
6.9 10.89 6 12 6H21V5C21 3.9 20.11 3 19 3H9M12 8V16H22V8H12M16 10.5C16.83 10.5 
17.5 11.17 17.5 12C17.5 12.83 16.83 13.5 16 13.5C15.17 13.5 14.5 12.83 14.5 
12C14.5 11.17 15.17 10.5 16 10.5Z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_menu_send.xml 
b/app/src/main/res/drawable/ic_account_balance.xml
similarity index 66%
copy from app/src/main/res/drawable/ic_menu_send.xml
copy to app/src/main/res/drawable/ic_account_balance.xml
index 9745066..03224e2 100644
--- a/app/src/main/res/drawable/ic_menu_send.xml
+++ b/app/src/main/res/drawable/ic_account_balance.xml
@@ -5,5 +5,5 @@
         android:viewportHeight="24.0">
     <path
             android:fillColor="#FF000000"
-            android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
+            
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_menu_send.xml 
b/app/src/main/res/drawable/ic_add_circle.xml
similarity index 64%
copy from app/src/main/res/drawable/ic_menu_send.xml
copy to app/src/main/res/drawable/ic_add_circle.xml
index 9745066..01d5f90 100644
--- a/app/src/main/res/drawable/ic_menu_send.xml
+++ b/app/src/main/res/drawable/ic_add_circle.xml
@@ -5,5 +5,5 @@
         android:viewportHeight="24.0">
     <path
             android:fillColor="#FF000000"
-            android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
+            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_home_black_24dp.xml 
b/app/src/main/res/drawable/ic_cancel.xml
similarity index 55%
copy from app/src/main/res/drawable/ic_home_black_24dp.xml
copy to app/src/main/res/drawable/ic_cancel.xml
index 70fb291..7d2b57e 100644
--- a/app/src/main/res/drawable/ic_home_black_24dp.xml
+++ b/app/src/main/res/drawable/ic_cancel.xml
@@ -5,5 +5,5 @@
         android:viewportHeight="24.0">
     <path
         android:fillColor="#FF000000"
-        android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
+        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/drawable/ic_home_black_24dp.xml 
b/app/src/main/res/drawable/ic_directions.xml
similarity index 51%
copy from app/src/main/res/drawable/ic_home_black_24dp.xml
copy to app/src/main/res/drawable/ic_directions.xml
index 70fb291..739dd20 100644
--- a/app/src/main/res/drawable/ic_home_black_24dp.xml
+++ b/app/src/main/res/drawable/ic_directions.xml
@@ -5,5 +5,5 @@
         android:viewportHeight="24.0">
     <path
         android:fillColor="#FF000000"
-        android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
+        android:pathData="M21.71,11.29l-9,-9c-0.39,-0.39 -1.02,-0.39 
-1.41,0l-9,9c-0.39,0.39 -0.39,1.02 0,1.41l9,9c0.39,0.39 1.02,0.39 
1.41,0l9,-9c0.39,-0.38 0.39,-1.01 0,-1.41zM14,14.5V12h-4v3H8v-4c0,-0.55 0.45,-1 
1,-1h5V7.5l3.5,3.5 -3.5,3.5z"/>
 </vector>
diff --git a/app/src/main/res/layout/fragment_json.xml 
b/app/src/main/res/layout/fragment_json.xml
new file mode 100644
index 0000000..fe40156
--- /dev/null
+++ b/app/src/main/res/layout/fragment_json.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView 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="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+    <TextView
+            android:id="@+id/jsonView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:layout_marginTop="8dp"
+            android:layout_marginEnd="8dp"
+            android:layout_marginBottom="8dp"
+            android:fontFamily="monospace"
+            android:textSize="12sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="[JSON]" />
+
+</ScrollView>
diff --git a/app/src/main/res/layout/fragment_show_history.xml 
b/app/src/main/res/layout/fragment_show_history.xml
index 4ed11ac..dc93889 100644
--- a/app/src/main/res/layout/fragment_show_history.xml
+++ b/app/src/main/res/layout/fragment_show_history.xml
@@ -1,29 +1,31 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.core.widget.NestedScrollView
-        xmlns:android="http://schemas.android.com/apk/res/android";
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android";
         xmlns:tools="http://schemas.android.com/tools";
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_margin="15dp">
+        android:layout_height="match_parent">
 
-    <LinearLayout
+    <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/historyList"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:orientation="vertical">
+            android:scrollbars="vertical" />
 
-        <androidx.recyclerview.widget.RecyclerView
-                android:id="@+id/list_history"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:scrollbars="vertical"/>
+    <TextView
+            android:id="@+id/historyEmptyState"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:text="@string/history_empty"
+            android:visibility="invisible"
+            tools:visibility="visible" />
 
-        <TextView
-                android:id="@+id/list_history_placeholder"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center"
-                android:text="The wallet history is empty"
-                tools:visibility="gone"/>
-    </LinearLayout>
+    <ProgressBar
+            android:id="@+id/historyProgressBar"
+            style="?android:progressBarStyleLarge"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:visibility="invisible"
+            tools:visibility="visible" />
 
-</androidx.core.widget.NestedScrollView>
\ No newline at end of file
+</FrameLayout>
diff --git a/app/src/main/res/layout/history_payment.xml 
b/app/src/main/res/layout/history_payment.xml
new file mode 100644
index 0000000..62bfa79
--- /dev/null
+++ b/app/src/main/res/layout/history_payment.xml
@@ -0,0 +1,72 @@
+<?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:background="?android:selectableItemBackground"
+        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"
+            style="@style/HistoryTitle"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:layout_marginEnd="8dp"
+            app:layout_constraintEnd_toStartOf="@+id/amountPaidWithFees"
+            app:layout_constraintStart_toEndOf="@+id/icon"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="Lots of books with very long titles" />
+
+    <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="@string/history_event_payment_sent" />
+
+    <TextView
+            android:id="@+id/amountPaidWithFees"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@color/red"
+            android:textSize="16sp"
+            app:layout_constraintBottom_toTopOf="@+id/time"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_bias="0.0"
+            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_receive.xml 
b/app/src/main/res/layout/history_receive.xml
new file mode 100644
index 0000000..adb6ecd
--- /dev/null
+++ b/app/src/main/res/layout/history_receive.xml
@@ -0,0 +1,90 @@
+<?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:background="?android:selectableItemBackground"
+        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"
+            style="@style/HistoryTitle"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:layout_marginEnd="8dp"
+            android:text="@string/history_event_withdrawn"
+            app:layout_constraintEnd_toStartOf="@+id/amountWithdrawn"
+            app:layout_constraintStart_toEndOf="@+id/icon"
+            app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+            android:id="@+id/summary"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:layout_marginTop="8dp"
+            android:layout_marginEnd="8dp"
+            android:layout_marginBottom="8dp"
+            app:layout_constrainedWidth="true"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/feeLabel"
+            app:layout_constraintStart_toEndOf="@+id/icon"
+            app:layout_constraintTop_toBottomOf="@+id/title"
+            tools:text="http://taler.quite-long-exchange.url"; />
+
+    <TextView
+            android:id="@+id/feeLabel"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="2dp"
+            android:text="@string/history_fee_label"
+            app:layout_constraintEnd_toStartOf="@+id/fee"
+            app:layout_constraintTop_toTopOf="@+id/fee" />
+
+    <TextView
+            android:id="@+id/fee"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            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"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="10 TESTKUDOS" />
+
+    <TextView
+            android:id="@+id/time"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="8dp"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/fee"
+            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 669fbd1..51647ee 100644
--- a/app/src/main/res/layout/history_row.xml
+++ b/app/src/main/res/layout/history_row.xml
@@ -1,22 +1,58 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android";
-              xmlns:tools="http://schemas.android.com/tools";
-              android:orientation="vertical"
-              android:layout_width="match_parent"
-              android:layout_height="wrap_content">
+<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:background="?android:selectableItemBackground"
+        android:layout_margin="15dp">
 
-        <TextView
-                  android:layout_width="wrap_content"
-                  android:layout_height="wrap_content"
-                  android:id="@+id/history_text"
-                  android:textSize="24sp" tools:text="My History Event">
-        </TextView>
+    <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:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:id="@+id/history_subtext"
-                android:textSize="14sp" tools:text="My History Event">
-        </TextView>
+    <TextView
+            style="@style/HistoryTitle"
+            android:id="@+id/title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            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" />
 
-</LinearLayout>
\ No newline at end of file
+    <TextView
+            android:id="@+id/info"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            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="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="8dp"
+            android:gravity="end"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/title"
+            tools:text="3 days ago" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/menu/history.xml 
b/app/src/main/res/menu/history.xml
index c8fb3e7..8c2aa69 100644
--- a/app/src/main/res/menu/history.xml
+++ b/app/src/main/res/menu/history.xml
@@ -1,8 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android";
-      xmlns:app="http://schemas.android.com/apk/res-auto";>
-    <item android:id="@+id/reload_history"
-          android:title="Reload History"
-          android:orderInCategory="100"
-          app:showAsAction="never"/>
+        xmlns:app="http://schemas.android.com/apk/res-auto";>
+    <item
+            android:id="@+id/show_all_history"
+            android:checkable="true"
+            android:checked="false"
+            android:title="@string/history_show_all"
+            app:showAsAction="never" />
+    <item
+            android:id="@+id/reload_history"
+            android:orderInCategory="100"
+            android:title="@string/history_reload"
+            app:showAsAction="never" />
 </menu>
diff --git a/app/src/main/res/navigation/nav_graph.xml 
b/app/src/main/res/navigation/nav_graph.xml
index 88d64f7..f958b9c 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -43,7 +43,7 @@
             tools:layout="@layout/fragment_settings" />
     <fragment
             android:id="@+id/walletHistory"
-            android:name="net.taler.wallet.WalletHistory"
+            android:name="net.taler.wallet.history.WalletHistory"
             android:label="History"
             tools:layout="@layout/fragment_show_history" />
     <fragment
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 cfd2213..39fd3a6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -16,6 +16,26 @@
     <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</string>
+    <string name="history_event_payment_aborted">Payment Aborted</string>
+    <string name="history_event_withdrawn">Withdraw</string>
+    <string name="history_event_order_accepted">Purchase Confirmed</string>
+    <string name="history_event_order_refused">Purchase Cancelled</string>
+    <string name="history_event_tip_accepted">Tip Accepted</string>
+    <string name="history_event_tip_declined">Tip Declined</string>
+    <string name="history_event_order_redirected">Purchase Redirected</string>
+    <string name="history_event_refund">Refund</string>
+    <string name="history_event_refreshed">Obtained change</string>
+    <string name="history_event_unknown">Unknown Event</string>
+    <string name="history_fee_label">Fee:</string>
+    <string name="history_show_all">Show All</string>
+    <string name="history_reload">Reload History</string>
+    <string name="history_empty">The wallet history is empty</string>
+
     <!-- TODO: Remove or change this placeholder text -->
     <string name="hello_blank_fragment">Hello blank fragment</string>
 </resources>
diff --git a/app/src/main/res/values/styles.xml 
b/app/src/main/res/values/styles.xml
index 16dbab3..f2eca8b 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -14,4 +14,9 @@
     <style name="AppTheme.AppBarOverlay" 
parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
     <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/>
 
+    <style name="HistoryTitle">
+        <item name="android:textSize">17sp</item>
+        <item name="android:textColor">?android:textColorPrimary</item>
+    </style>
+
 </resources>
diff --git a/app/src/test/java/net/taler/wallet/history/HistoryEventTest.kt 
b/app/src/test/java/net/taler/wallet/history/HistoryEventTest.kt
new file mode 100644
index 0000000..361d2ec
--- /dev/null
+++ b/app/src/test/java/net/taler/wallet/history/HistoryEventTest.kt
@@ -0,0 +1,459 @@
+/*
+ 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
+import org.junit.Test
+import kotlin.random.Random
+
+class HistoryEventTest {
+
+    private val mapper = ObjectMapper().registerModule(KotlinModule())
+
+    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`() {
+        val builtIn = Random.nextBoolean()
+        val json = """{
+            "type": "exchange-added",
+            "builtIn": $builtIn,
+            "eventId": 
"exchange-added;https%3A%2F%2Fexchange.test.taler.net%2F",
+            "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/",
+            "timestamp": {
+                "t_ms": $timestamp
+            }
+        }""".trimIndent()
+        val event: ExchangeAddedEvent = mapper.readValue(json)
+
+        assertEquals(builtIn, event.builtIn)
+        assertEquals(exchangeBaseUrl, event.exchangeBaseUrl)
+        assertEquals(timestamp, event.timestamp.ms)
+    }
+
+    @Test
+    fun `test ExchangeUpdatedEvent`() {
+        val json = """{
+            "type": "exchange-updated",
+            "eventId": 
"exchange-updated;https%3A%2F%2Fexchange.test.taler.net%2F",
+            "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/",
+            "timestamp": {
+                "t_ms": $timestamp
+            }
+        }""".trimIndent()
+        val event: ExchangeUpdatedEvent = mapper.readValue(json)
+
+        assertEquals(exchangeBaseUrl, event.exchangeBaseUrl)
+        assertEquals(timestamp, event.timestamp.ms)
+    }
+
+    @Test
+    fun `test ReserveShortInfo`() {
+        val json = """{
+            "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/",
+            "reserveCreationDetail": {
+                "type": "manual"
+            },
+            "reservePub": 
"BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G"
+        }""".trimIndent()
+        val info: ReserveShortInfo = mapper.readValue(json)
+
+        assertEquals(exchangeBaseUrl, info.exchangeBaseUrl)
+        assertEquals(MANUAL, info.reserveCreationDetail.type)
+        assertEquals("BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G", 
info.reservePub)
+    }
+
+    @Test
+    fun `test ReserveBalanceUpdatedEvent`() {
+        val json = """{
+            "type": "reserve-balance-updated",
+            "eventId": 
"reserve-balance-updated;K0H10Q6HB9WH0CKHQQMNH5C6GA7A9AR1E2XSS9G1KG3ZXMBVT26G",
+            "amountExpected": "TESTKUDOS:23",
+            "amountReserveBalance": "TESTKUDOS:10",
+            "timestamp": {
+                "t_ms": $timestamp
+            },
+            "newHistoryTransactions": [
+                {
+                    "amount": "TESTKUDOS:10",
+                    "sender_account_url": 
"payto:\/\/x-taler-bank\/bank.test.taler.net\/894",
+                    "timestamp": {
+                        "t_ms": $timestamp
+                    },
+                    "wire_reference": "00000000004TR",
+                    "type": "DEPOSIT"
+                }
+            ],
+            "reserveShortInfo": {
+                "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/",
+                "reserveCreationDetail": {
+                    "type": "manual"
+                },
+                "reservePub": 
"BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G"
+            }
+        }""".trimIndent()
+        val event: ReserveBalanceUpdatedEvent = mapper.readValue(json)
+
+        assertEquals(timestamp, event.timestamp.ms)
+        assertEquals("TESTKUDOS:23", event.amountExpected)
+        assertEquals("TESTKUDOS:10", event.amountReserveBalance)
+        assertEquals(1, event.newHistoryTransactions.size)
+        assertTrue(event.newHistoryTransactions[0] is 
ReserveDepositTransaction)
+        assertEquals(exchangeBaseUrl, event.reserveShortInfo.exchangeBaseUrl)
+    }
+
+    @Test
+    fun `test HistoryWithdrawnEvent`() {
+        val json = """{
+            "type": "withdrawn",
+            "withdrawSessionId": 
"974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0",
+            "eventId": 
"withdrawn;974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0",
+            "amountWithdrawnEffective": "TESTKUDOS:9.8",
+            "amountWithdrawnRaw": "TESTKUDOS:10",
+            "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/",
+            "timestamp": {
+                "t_ms": $timestamp
+            },
+            "withdrawalSource": {
+                "type": "reserve",
+                "reservePub": 
"BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G"
+            }
+        }""".trimIndent()
+        val event: HistoryWithdrawnEvent = mapper.readValue(json)
+
+        assertEquals(
+            "974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0",
+            event.withdrawSessionId
+        )
+        assertEquals("TESTKUDOS:9.8", event.amountWithdrawnEffective)
+        assertEquals("TESTKUDOS:10", event.amountWithdrawnRaw)
+        assertTrue(event.withdrawalSource is WithdrawalSourceReserve)
+        assertEquals(
+            "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G",
+            (event.withdrawalSource as WithdrawalSourceReserve).reservePub
+        )
+        assertEquals(exchangeBaseUrl, event.exchangeBaseUrl)
+        assertEquals(timestamp, event.timestamp.ms)
+    }
+
+    @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 HistoryPaymentAbortedEvent`() {
+        val json = """{
+            "type": "payment-aborted",
+            "eventId": 
"payment-sent;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+            "orderShortInfo": {
+                "amount": "${orderShortInfo.amount}",
+                "orderId": "${orderShortInfo.orderId}",
+                "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}",
+                "proposalId": "${orderShortInfo.proposalId}",
+                "summary": "${orderShortInfo.summary}"
+            },
+            "timestamp": {
+              "t_ms": $timestamp
+            },
+            "amountLost": "KUDOS:0.1"
+          }""".trimIndent()
+        val event: HistoryPaymentAbortedEvent = mapper.readValue(json)
+
+        assertEquals(orderShortInfo, event.orderShortInfo)
+        assertEquals("KUDOS:0.1", event.amountLost)
+        assertEquals(timestamp, event.timestamp.ms)
+    }
+
+    @Test
+    fun `test HistoryTipAcceptedEvent`() {
+        val json = """{
+            "type": "tip-accepted",
+            "timestamp": {
+              "t_ms": $timestamp
+            },
+            "eventId": 
"tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+            "tipId": 
"tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+            "tipRaw": "KUDOS:4"
+          }""".trimIndent()
+        val event: HistoryTipAcceptedEvent = mapper.readValue(json)
+
+        
assertEquals("tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
 event.tipId)
+        assertEquals("KUDOS:4", event.tipRaw)
+        assertEquals(timestamp, event.timestamp.ms)
+    }
+
+    @Test
+    fun `test HistoryTipDeclinedEvent`() {
+        val json = """{
+            "type": "tip-declined",
+            "timestamp": {
+              "t_ms": $timestamp
+            },
+            "eventId": 
"tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+            "tipId": 
"tip-accepted;998724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+            "tipAmount": "KUDOS:4"
+          }""".trimIndent()
+        val event: HistoryTipDeclinedEvent = mapper.readValue(json)
+
+        
assertEquals("tip-accepted;998724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
 event.tipId)
+        assertEquals("KUDOS:4", event.tipAmount)
+        assertEquals(timestamp, event.timestamp.ms)
+    }
+
+    @Test
+    fun `test HistoryRefundedEvent`() {
+        val json = """{
+            "type": "refund",
+            "eventId": 
"refund;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+            "refundGroupId": "refund;998724",
+            "orderShortInfo": {
+                "amount": "${orderShortInfo.amount}",
+                "orderId": "${orderShortInfo.orderId}",
+                "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}",
+                "proposalId": "${orderShortInfo.proposalId}",
+                "summary": "${orderShortInfo.summary}"
+            },
+            "timestamp": {
+              "t_ms": $timestamp
+            },
+            "amountRefundedRaw": "KUDOS:1.0",
+            "amountRefundedInvalid": "KUDOS:0.5",
+            "amountRefundedEffective": "KUDOS:0.4"
+          }""".trimIndent()
+        val event: HistoryRefundedEvent = mapper.readValue(json)
+
+        assertEquals("refund;998724", event.refundGroupId)
+        assertEquals("KUDOS:1.0", event.amountRefundedRaw)
+        assertEquals("KUDOS:0.5", event.amountRefundedInvalid)
+        assertEquals("KUDOS:0.4", event.amountRefundedEffective)
+        assertEquals(orderShortInfo, event.orderShortInfo)
+        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 HistoryOrderRedirectedEvent`() {
+        val json = """{
+            "type": "order-redirected",
+            "eventId": 
"order-redirected;621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G",
+            "alreadyPaidOrderShortInfo": {
+              "amount": "KUDOS:0.5",
+              "orderId": "2019.354-01P25CD66P8NG",
+              "merchantBaseUrl": 
"https://backend.demo.taler.net/public/instances/FSF/";,
+              "proposalId": 
"898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0",
+              "summary": "Essay: 1. The Free Software Definition"
+            },
+            "newOrderShortInfo": {
+              "amount": "KUDOS:0.5",
+              "orderId": "2019.364-01M4QH6KPMJY4",
+              "merchantBaseUrl": 
"https://backend.demo.taler.net/public/instances/FSF/";,
+              "proposalId": 
"621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G",
+              "summary": "Essay: 1. The Free Software Definition"
+            },
+            "timestamp": {
+              "t_ms": $timestamp
+            }
+          }""".trimIndent()
+        val event: HistoryOrderRedirectedEvent = mapper.readValue(json)
+
+        assertEquals("898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", 
event.alreadyPaidOrderShortInfo.proposalId)
+        assertEquals("https://backend.demo.taler.net/public/instances/FSF/";, 
event.alreadyPaidOrderShortInfo.merchantBaseUrl)
+        assertEquals("2019.354-01P25CD66P8NG", 
event.alreadyPaidOrderShortInfo.orderId)
+        assertEquals("KUDOS:0.5", event.alreadyPaidOrderShortInfo.amount)
+        assertEquals("Essay: 1. The Free Software Definition", 
event.alreadyPaidOrderShortInfo.summary)
+
+        assertEquals("621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", 
event.newOrderShortInfo.proposalId)
+        assertEquals("https://backend.demo.taler.net/public/instances/FSF/";, 
event.newOrderShortInfo.merchantBaseUrl)
+        assertEquals("2019.364-01M4QH6KPMJY4", event.newOrderShortInfo.orderId)
+        assertEquals("KUDOS:0.5", event.newOrderShortInfo.amount)
+        assertEquals("Essay: 1. The Free Software Definition", 
event.newOrderShortInfo.summary)
+
+        assertEquals(timestamp, event.timestamp.ms)
+    }
+
+    @Test
+    fun `test HistoryUnknownEvent`() {
+        val json = """{
+            "type": "does not exist",
+            "timestamp": {
+              "t_ms": $timestamp
+            },
+            "eventId": 
"does-not-exist;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0"
+          }""".trimIndent()
+        val event: HistoryEvent = mapper.readValue(json)
+
+        assertEquals(HistoryUnknownEvent::class.java, event.javaClass)
+        assertEquals(timestamp, event.timestamp.ms)
+    }
+
+}
diff --git 
a/app/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt 
b/app/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt
new file mode 100644
index 0000000..208995a
--- /dev/null
+++ b/app/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt
@@ -0,0 +1,52 @@
+/*
+ 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 org.junit.Assert.assertEquals
+import org.junit.Test
+import kotlin.random.Random
+
+class ReserveTransactionTest {
+
+    private val mapper = ObjectMapper().registerModule(KotlinModule())
+
+    private val timestamp = Random.nextLong()
+
+    @Test
+    fun `test ExchangeAddedEvent`() {
+        val senderAccountUrl = "payto://x-taler-bank/bank.test.taler.net/894"
+        val json = """{
+            "amount": "TESTKUDOS:10",
+            "sender_account_url": 
"payto:\/\/x-taler-bank\/bank.test.taler.net\/894",
+            "timestamp": {
+                "t_ms": $timestamp
+            },
+            "wire_reference": "00000000004TR",
+            "type": "DEPOSIT"
+        }""".trimIndent()
+        val transaction: ReserveDepositTransaction = mapper.readValue(json)
+
+        assertEquals("TESTKUDOS:10", transaction.amount)
+        assertEquals(senderAccountUrl, transaction.senderAccountUrl)
+        assertEquals("00000000004TR", transaction.wireReference)
+        assertEquals(timestamp, transaction.timestamp.ms)
+    }
+
+}

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



reply via email to

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