gnunet-svn
[Top][All Lists]
Advanced

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

[taler-merchant-terminal-android] branch master updated (1764700 -> 7eeb


From: gnunet
Subject: [taler-merchant-terminal-android] branch master updated (1764700 -> 7eebd07)
Date: Tue, 17 Mar 2020 16:30:44 +0100

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

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

    from 1764700  Allow configuration Uri with basic auth crentials in user info
     new e4750a5  If there's just one product, use its description as order 
summary
     new bca790d  Clean up history code and layout
     new 7eebd07  Add refund button to history items and allow to refund orders

The 3 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:
 .../java/net/taler/merchantpos/MainViewModel.kt    |   4 +
 .../java/net/taler/merchantpos/MerchantHistory.kt  | 190 ---------------------
 app/src/main/java/net/taler/merchantpos/Utils.kt   |  28 +++
 .../taler/merchantpos/history/HistoryManager.kt    | 106 ++++++++++++
 .../merchantpos/history/MerchantHistoryFragment.kt | 160 +++++++++++++++++
 .../taler/merchantpos/history/RefundFragment.kt    |  99 +++++++++++
 .../net/taler/merchantpos/history/RefundManager.kt | 111 ++++++++++++
 .../taler/merchantpos/history/RefundUriFragment.kt |  65 +++++++
 .../net/taler/merchantpos/order/Definitions.kt     |  10 +-
 app/src/main/res/drawable/ic_cash_refund.xml       |   9 +
 app/src/main/res/layout/fragment_refund.xml        | 122 +++++++++++++
 ...process_payment.xml => fragment_refund_uri.xml} |  49 ++----
 app/src/main/res/layout/history_row.xml            |  91 ----------
 app/src/main/res/layout/list_item_history.xml      |  97 +++++++++++
 app/src/main/res/navigation/nav_graph.xml          |  27 ++-
 app/src/main/res/values/strings.xml                |  14 +-
 16 files changed, 860 insertions(+), 322 deletions(-)
 delete mode 100644 app/src/main/java/net/taler/merchantpos/MerchantHistory.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/history/RefundManager.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
 create mode 100644 app/src/main/res/drawable/ic_cash_refund.xml
 create mode 100644 app/src/main/res/layout/fragment_refund.xml
 copy app/src/main/res/layout/{fragment_process_payment.xml => 
fragment_refund_uri.xml} (68%)
 delete mode 100644 app/src/main/res/layout/history_row.xml
 create mode 100644 app/src/main/res/layout/list_item_history.xml

diff --git a/app/src/main/java/net/taler/merchantpos/MainViewModel.kt 
b/app/src/main/java/net/taler/merchantpos/MainViewModel.kt
index 1bb57b9..3fe472d 100644
--- a/app/src/main/java/net/taler/merchantpos/MainViewModel.kt
+++ b/app/src/main/java/net/taler/merchantpos/MainViewModel.kt
@@ -24,6 +24,8 @@ import 
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PRO
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.module.kotlin.KotlinModule
 import net.taler.merchantpos.config.ConfigManager
+import net.taler.merchantpos.history.HistoryManager
+import net.taler.merchantpos.history.RefundManager
 import net.taler.merchantpos.order.OrderManager
 import net.taler.merchantpos.payment.PaymentManager
 
@@ -39,6 +41,8 @@ class MainViewModel(app: Application) : AndroidViewModel(app) 
{
         addConfigurationReceiver(orderManager)
     }
     val paymentManager = PaymentManager(configManager, queue, mapper)
+    val historyManager = HistoryManager(configManager, queue, mapper)
+    val refundManager = RefundManager(configManager, queue)
 
     override fun onCleared() {
         queue.cancelAll { !it.isCanceled }
diff --git a/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt 
b/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt
deleted file mode 100644
index 997a1e6..0000000
--- a/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2020 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under 
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
- * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-package net.taler.merchantpos
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.TextView
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.MutableLiveData
-import androidx.navigation.fragment.findNavController
-import androidx.recyclerview.widget.DividerItemDecoration
-import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView.Adapter
-import androidx.recyclerview.widget.RecyclerView.ViewHolder
-import com.android.volley.Request.Method.GET
-import com.android.volley.RequestQueue
-import com.android.volley.Response.ErrorListener
-import com.android.volley.Response.Listener
-import com.android.volley.toolbox.Volley
-import com.google.android.material.snackbar.Snackbar
-import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
-import kotlinx.android.synthetic.main.fragment_merchant_history.*
-import net.taler.merchantpos.HistoryItemAdapter.HistoryItemViewHolder
-import 
net.taler.merchantpos.MerchantHistoryDirections.Companion.actionGlobalMerchantSettings
-import net.taler.merchantpos.config.MerchantRequest
-import org.json.JSONObject
-import java.time.Instant
-import java.time.ZoneId
-import java.time.format.DateTimeFormatter
-import java.time.format.FormatStyle.SHORT
-import java.util.*
-
-/**
- * Fragment to display the merchant's payment history,
- * received from the backend.
- */
-class MerchantHistory : Fragment() {
-
-    companion object {
-        const val TAG = "taler-merchant"
-    }
-
-    private lateinit var queue: RequestQueue
-    private val model: MainViewModel by activityViewModels()
-    private val historyListAdapter = HistoryItemAdapter(listOf())
-
-    private val isLoading = MutableLiveData<Boolean>().apply { value = false }
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        queue = Volley.newRequestQueue(context)
-    }
-
-    override fun onCreateView(
-        inflater: LayoutInflater, container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View? {
-        return inflater.inflate(R.layout.fragment_merchant_history, container, 
false)
-    }
-
-    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        list_history.apply {
-            layoutManager = LinearLayoutManager(requireContext())
-            addItemDecoration(DividerItemDecoration(context, VERTICAL))
-            adapter = historyListAdapter
-        }
-
-        swipeRefresh.isRefreshing = false
-        swipeRefresh.setOnRefreshListener {
-            Log.v(TAG, "refreshing!")
-            fetchHistory()
-        }
-
-        this.isLoading.observe(viewLifecycleOwner, androidx.lifecycle.Observer 
{ loading ->
-            Log.v(TAG, "setting refreshing to $loading")
-            swipeRefresh.isRefreshing = loading
-        })
-    }
-
-    override fun onStart() {
-        super.onStart()
-        if (model.configManager.merchantConfig?.instance == null) {
-            actionGlobalMerchantSettings().navigate(findNavController())
-        } else {
-            fetchHistory()
-        }
-    }
-
-    private fun fetchHistory() {
-        isLoading.value = true
-        val merchantConfig = model.configManager.merchantConfig!!
-        val params = mapOf("instance" to merchantConfig.instance)
-        val req = MerchantRequest(GET, merchantConfig, "history", params, null,
-            Listener { onHistoryResponse(it) },
-            ErrorListener { onNetworkError() })
-        queue.add(req)
-    }
-
-    private fun onHistoryResponse(body: JSONObject) {
-        this.isLoading.value = false
-        Log.v(TAG, "got history response $body")
-        // TODO use jackson instead of manual parsing
-        val data = arrayListOf<HistoryItem>()
-        val historyJson = body.getJSONArray("history")
-        for (i in 0 until historyJson.length()) {
-            val item = historyJson.getJSONObject(i)
-            val orderId = item.getString("order_id")
-            val summary = item.getString("summary")
-            val timestampObj = item.getJSONObject("timestamp")
-            val timestamp = Instant.ofEpochSecond(timestampObj.getLong("t_ms"))
-            val amount = Amount.fromString(item.getString("amount"))
-            data.add(HistoryItem(orderId, amount, summary, timestamp))
-        }
-        historyListAdapter.setData(data)
-    }
-
-    private fun onNetworkError() {
-        this.isLoading.value = false
-        Snackbar.make(view!!, R.string.error_network, LENGTH_SHORT).show()
-    }
-
-}
-
-data class HistoryItem(
-    val orderId: String,
-    val amount: Amount,
-    val summary: String,
-    val timestamp: Instant
-)
-
-class HistoryItemAdapter(private var items: List<HistoryItem>) : 
Adapter<HistoryItemViewHolder>() {
-
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
HistoryItemViewHolder {
-        val v = 
LayoutInflater.from(parent.context).inflate(R.layout.history_row, parent, false)
-        return HistoryItemViewHolder(v)
-    }
-
-    override fun getItemCount() = items.size
-
-    override fun onBindViewHolder(holder: HistoryItemViewHolder, position: 
Int) {
-        holder.bind(items[position])
-    }
-
-    fun setData(items: List<HistoryItem>) {
-        this.items = items
-        this.notifyDataSetChanged()
-    }
-
-    class HistoryItemViewHolder(v: View) : ViewHolder(v) {
-
-        private val summaryTextView: TextView = 
v.findViewById(R.id.text_history_summary)
-        private val amountTextView: TextView = 
v.findViewById(R.id.text_history_amount)
-        private val timestampTextView: TextView = 
v.findViewById(R.id.text_history_time)
-        private val orderIdTextView: TextView = 
v.findViewById(R.id.text_history_order_id)
-        private val formatter: DateTimeFormatter = 
DateTimeFormatter.ofLocalizedDateTime(SHORT)
-            .withLocale(Locale.getDefault())
-            .withZone(ZoneId.systemDefault())
-
-        fun bind(item: HistoryItem) {
-            summaryTextView.text = item.summary
-            val amount = item.amount
-            @SuppressLint("SetTextI18n")
-            amountTextView.text = "${amount.amount} ${amount.currency}"
-            timestampTextView.text = formatter.format(item.timestamp)
-            orderIdTextView.text = item.orderId
-        }
-    }
-
-}
diff --git a/app/src/main/java/net/taler/merchantpos/Utils.kt 
b/app/src/main/java/net/taler/merchantpos/Utils.kt
index 2f6d4f8..a0c30d6 100644
--- a/app/src/main/java/net/taler/merchantpos/Utils.kt
+++ b/app/src/main/java/net/taler/merchantpos/Utils.kt
@@ -16,15 +16,27 @@
 
 package net.taler.merchantpos
 
+import android.content.Context
+import android.text.format.DateUtils.DAY_IN_MILLIS
+import android.text.format.DateUtils.FORMAT_ABBREV_MONTH
+import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE
+import android.text.format.DateUtils.FORMAT_NO_YEAR
+import android.text.format.DateUtils.FORMAT_SHOW_DATE
+import android.text.format.DateUtils.FORMAT_SHOW_TIME
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import android.text.format.DateUtils.formatDateTime
+import android.text.format.DateUtils.getRelativeTimeSpanString
 import android.view.View
 import android.view.View.INVISIBLE
 import android.view.View.VISIBLE
 import androidx.annotation.StringRes
+import androidx.fragment.app.Fragment
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MediatorLiveData
 import androidx.lifecycle.Observer
 import androidx.navigation.NavController
 import androidx.navigation.NavDirections
+import androidx.navigation.fragment.findNavController
 import 
com.google.android.material.snackbar.BaseTransientBottomBar.ANIMATION_MODE_FADE
 import com.google.android.material.snackbar.BaseTransientBottomBar.Duration
 import com.google.android.material.snackbar.Snackbar.make
@@ -97,6 +109,16 @@ fun topSnackbar(view: View, @StringRes resId: Int, 
@Duration duration: Int) {
 
 fun NavDirections.navigate(nav: NavController) = nav.navigate(this)
 
+fun Fragment.navigate(directions: NavDirections) = 
findNavController().navigate(directions)
+
+fun Long.toRelativeTime(context: Context): CharSequence {
+    val now = System.currentTimeMillis()
+    return if (now - this > DAY_IN_MILLIS * 2) {
+        val flags = FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or 
FORMAT_ABBREV_MONTH or FORMAT_NO_YEAR
+        formatDateTime(context, this, flags)
+    } else getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, 
FORMAT_ABBREV_RELATIVE)
+}
+
 class CombinedLiveData<T, K, S>(
     source1: LiveData<T>,
     source2: LiveData<K>,
@@ -125,3 +147,9 @@ class CombinedLiveData<T, K, S>(
         throw UnsupportedOperationException()
     }
 }
+
+/**
+ * Use this with 'when' expressions when you need it to handle all 
possibilities/branches.
+ */
+val <T> T.exhaustive: T
+    get() = this
diff --git a/app/src/main/java/net/taler/merchantpos/history/HistoryManager.kt 
b/app/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
new file mode 100644
index 0000000..594e7cc
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
@@ -0,0 +1,106 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.history
+
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.android.volley.Request.Method.GET
+import com.android.volley.Request.Method.POST
+import com.android.volley.RequestQueue
+import com.android.volley.Response.ErrorListener
+import com.android.volley.Response.Listener
+import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import net.taler.merchantpos.Amount
+import net.taler.merchantpos.config.ConfigManager
+import net.taler.merchantpos.config.MerchantRequest
+import org.json.JSONObject
+
+@JsonInclude(NON_EMPTY)
+class Timestamp(
+    @JsonProperty("t_ms")
+    val ms: Long
+)
+
+data class HistoryItem(
+    @JsonProperty("order_id")
+    val orderId: String,
+    @JsonProperty("amount")
+    val amountStr: String,
+    val summary: String,
+    val timestamp: Timestamp
+) {
+    @get:JsonIgnore
+    val amount: Amount by lazy { Amount.fromString(amountStr) }
+
+    @get:JsonIgnore
+    val time = timestamp.ms
+}
+
+sealed class HistoryResult {
+    object Error : HistoryResult()
+    class Success(val items: List<HistoryItem>) : HistoryResult()
+}
+
+class HistoryManager(
+    private val configManager: ConfigManager,
+    private val queue: RequestQueue,
+    private val mapper: ObjectMapper
+) {
+
+    private val mIsLoading = MutableLiveData(false)
+    val isLoading: LiveData<Boolean> = mIsLoading
+
+    private val mItems = MutableLiveData<HistoryResult>()
+    val items: LiveData<HistoryResult> = mItems
+
+    @UiThread
+    internal fun fetchHistory() {
+        mIsLoading.value = true
+        val merchantConfig = configManager.merchantConfig!!
+        val params = mapOf("instance" to merchantConfig.instance)
+        val req = MerchantRequest(GET, merchantConfig, "history", params, null,
+            Listener { onHistoryResponse(it) },
+            ErrorListener { onHistoryError() })
+        queue.add(req)
+    }
+
+    @UiThread
+    private fun onHistoryResponse(body: JSONObject) {
+        mIsLoading.value = false
+        val items = arrayListOf<HistoryItem>()
+        val historyJson = body.getJSONArray("history")
+        for (i in 0 until historyJson.length()) {
+            val historyItem: HistoryItem = 
mapper.readValue(historyJson.getString(i))
+            items.add(historyItem)
+        }
+        mItems.value = HistoryResult.Success(items)
+    }
+
+    @UiThread
+    private fun onHistoryError() {
+        mIsLoading.value = false
+        mItems.value = HistoryResult.Error
+    }
+
+}
diff --git 
a/app/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt 
b/app/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
new file mode 100644
index 0000000..0c53f71
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
@@ -0,0 +1,160 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.history
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
+import kotlinx.android.synthetic.main.fragment_merchant_history.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.exhaustive
+import net.taler.merchantpos.history.HistoryItemAdapter.HistoryItemViewHolder
+import 
net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionGlobalMerchantSettings
+import 
net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionNavHistoryToRefundFragment
+import net.taler.merchantpos.navigate
+import net.taler.merchantpos.toRelativeTime
+import java.util.*
+
+private interface RefundClickListener {
+    fun onRefundClicked(item: HistoryItem)
+}
+
+/**
+ * Fragment to display the merchant's payment history, received from the 
backend.
+ */
+class MerchantHistoryFragment : Fragment(), RefundClickListener {
+
+    companion object {
+        const val TAG = "taler-merchant"
+    }
+
+    private val model: MainViewModel by activityViewModels()
+    private val historyManager by lazy { model.historyManager }
+    private val refundManager by lazy { model.refundManager }
+
+    private val historyListAdapter = HistoryItemAdapter(this)
+
+    override fun onCreateView(
+        inflater: LayoutInflater, container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_merchant_history, container, 
false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        list_history.apply {
+            layoutManager = LinearLayoutManager(requireContext())
+            addItemDecoration(DividerItemDecoration(context, VERTICAL))
+            adapter = historyListAdapter
+        }
+
+        swipeRefresh.setOnRefreshListener {
+            Log.v(TAG, "refreshing!")
+            historyManager.fetchHistory()
+        }
+        historyManager.isLoading.observe(viewLifecycleOwner, Observer { 
loading ->
+            Log.v(TAG, "setting refreshing to $loading")
+            swipeRefresh.isRefreshing = loading
+        })
+        historyManager.items.observe(viewLifecycleOwner, Observer { result ->
+            when (result) {
+                is HistoryResult.Error -> onError()
+                is HistoryResult.Success -> 
historyListAdapter.setData(result.items)
+            }.exhaustive
+        })
+    }
+
+    override fun onStart() {
+        super.onStart()
+        if (model.configManager.merchantConfig?.instance == null) {
+            navigate(actionGlobalMerchantSettings())
+        } else {
+            historyManager.fetchHistory()
+        }
+    }
+
+    private fun onError() {
+        Snackbar.make(view!!, R.string.error_network, LENGTH_SHORT).show()
+    }
+
+    override fun onRefundClicked(item: HistoryItem) {
+        refundManager.startRefund(item)
+        navigate(actionNavHistoryToRefundFragment())
+    }
+
+}
+
+private class HistoryItemAdapter(private val listener: RefundClickListener) :
+    Adapter<HistoryItemViewHolder>() {
+
+    private val items = ArrayList<HistoryItem>()
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
HistoryItemViewHolder {
+        val v =
+            
LayoutInflater.from(parent.context).inflate(R.layout.list_item_history, parent, 
false)
+        return HistoryItemViewHolder(v)
+    }
+
+    override fun getItemCount() = items.size
+
+    override fun onBindViewHolder(holder: HistoryItemViewHolder, position: 
Int) {
+        holder.bind(items[position])
+    }
+
+    fun setData(items: List<HistoryItem>) {
+        this.items.clear()
+        this.items.addAll(items)
+        this.notifyDataSetChanged()
+    }
+
+    private inner class HistoryItemViewHolder(private val v: View) : 
ViewHolder(v) {
+
+        private val orderSummaryView: TextView = 
v.findViewById(R.id.orderSummaryView)
+        private val orderAmountView: TextView = 
v.findViewById(R.id.orderAmountView)
+        private val orderTimeView: TextView = 
v.findViewById(R.id.orderTimeView)
+        private val orderIdView: TextView = v.findViewById(R.id.orderIdView)
+        private val refundButton: ImageButton = 
v.findViewById(R.id.refundButton)
+
+        fun bind(item: HistoryItem) {
+            orderSummaryView.text = item.summary
+            val amount = item.amount
+            @SuppressLint("SetTextI18n")
+            orderAmountView.text = "${amount.amount} ${amount.currency}"
+            orderIdView.text = v.context.getString(R.string.history_ref_no, 
item.orderId)
+            orderTimeView.text = item.time.toRelativeTime(v.context)
+            refundButton.setOnClickListener { listener.onRefundClicked(item) }
+        }
+
+    }
+
+}
diff --git a/app/src/main/java/net/taler/merchantpos/history/RefundFragment.kt 
b/app/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
new file mode 100644
index 0000000..1797cea
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
@@ -0,0 +1,99 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.history
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.StringRes
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.android.synthetic.main.fragment_refund.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.fadeIn
+import net.taler.merchantpos.fadeOut
+import 
net.taler.merchantpos.history.RefundFragmentDirections.Companion.actionRefundFragmentToRefundUriFragment
+import net.taler.merchantpos.history.RefundResult.Error
+import net.taler.merchantpos.history.RefundResult.PastDeadline
+import net.taler.merchantpos.history.RefundResult.Success
+import net.taler.merchantpos.navigate
+
+class RefundFragment : Fragment() {
+
+    private val model: MainViewModel by activityViewModels()
+    private val refundManager by lazy { model.refundManager }
+
+    override fun onCreateView(
+        inflater: LayoutInflater, container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_refund, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        val item = refundManager.toBeRefunded ?: throw IllegalStateException()
+        amountInputView.setText(item.amount.amount)
+        currencyView.text = item.amount.currency
+        abortButton.setOnClickListener { findNavController().navigateUp() }
+        refundButton.setOnClickListener { onRefundButtonClicked(item) }
+
+        refundManager.refundResult.observe(viewLifecycleOwner, Observer { 
result ->
+            onRefundResultChanged(result)
+        })
+    }
+
+    private fun onRefundButtonClicked(item: HistoryItem) {
+        val inputAmount = amountInputView.text.toString().toDouble()
+        if (inputAmount > item.amount.amount.toDouble()) {
+            amountView.error = getString(R.string.refund_error_max_amount, 
item.amount.amount)
+            return
+        }
+        if (inputAmount <= 0.0) {
+            amountView.error = getString(R.string.refund_error_zero)
+            return
+        }
+        amountView.error = null
+        refundButton.fadeOut()
+        progressBar.fadeIn()
+        refundManager.refund(item, inputAmount, 
reasonInputView.text.toString())
+    }
+
+    private fun onRefundResultChanged(result: RefundResult?): Any = when 
(result) {
+        Error -> onError(R.string.refund_error_backend)
+        PastDeadline -> onError(R.string.refund_error_deadline)
+        is Success -> {
+            progressBar.fadeOut()
+            refundButton.fadeIn()
+            navigate(actionRefundFragmentToRefundUriFragment())
+        }
+        null -> { // no-op
+        }
+    }
+
+    private fun onError(@StringRes res: Int) {
+        Snackbar.make(view!!, res, LENGTH_LONG).show()
+        progressBar.fadeOut()
+        refundButton.fadeIn()
+    }
+
+}
diff --git a/app/src/main/java/net/taler/merchantpos/history/RefundManager.kt 
b/app/src/main/java/net/taler/merchantpos/history/RefundManager.kt
new file mode 100644
index 0000000..270b3b8
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/history/RefundManager.kt
@@ -0,0 +1,111 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.history
+
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.android.volley.Request.Method.POST
+import com.android.volley.RequestQueue
+import com.android.volley.Response.ErrorListener
+import com.android.volley.Response.Listener
+import net.taler.merchantpos.config.ConfigManager
+import net.taler.merchantpos.config.MerchantRequest
+import org.json.JSONObject
+
+sealed class RefundResult {
+    object Error : RefundResult()
+    object PastDeadline : RefundResult()
+    class Success(
+        val refundUri: String,
+        val item: HistoryItem,
+        val amount: Double,
+        val reason: String
+    ) : RefundResult()
+}
+
+class RefundManager(
+    private val configManager: ConfigManager,
+    private val queue: RequestQueue
+) {
+
+    var toBeRefunded: HistoryItem? = null
+        private set
+
+    private val mRefundResult = MutableLiveData<RefundResult>()
+    internal val refundResult: LiveData<RefundResult> = mRefundResult
+
+    @UiThread
+    internal fun startRefund(item: HistoryItem) {
+        toBeRefunded = item
+        mRefundResult.value = null
+    }
+
+    @UiThread
+    internal fun refund(item: HistoryItem, amount: Double, reason: String) {
+        val merchantConfig = configManager.merchantConfig!!
+        val refundRequest = mapOf(
+            "order_id" to item.orderId,
+            "refund" to "${item.amount.currency}:$amount",
+            "reason" to reason
+        )
+        val body = JSONObject(refundRequest)
+        val req = MerchantRequest(POST, merchantConfig, "refund", null, body,
+            Listener { onRefundResponse(it, item, amount, reason) },
+            ErrorListener { onRefundError() }
+        )
+        queue.add(req)
+    }
+
+    @UiThread
+    private fun onRefundResponse(
+        json: JSONObject,
+        item: HistoryItem,
+        amount: Double,
+        reason: String
+    ) {
+        if (!json.has("contract_terms")) {
+            Log.e("TEST", "json: $json")
+            onRefundError()
+            return
+        }
+
+        val contractTerms = json.getJSONObject("contract_terms")
+        val refundDeadline = if (contractTerms.has("refund_deadline")) {
+            contractTerms.getJSONObject("refund_deadline").getLong("t_ms")
+        } else null
+        val autoRefund = contractTerms.has("auto_refund")
+        val refundUri = json.getString("taler_refund_uri")
+
+        Log.e("TEST", "refundDeadline: $refundDeadline")
+        if (refundDeadline != null) Log.e(
+            "TEST",
+            "refundDeadline passed: ${System.currentTimeMillis() > 
refundDeadline}"
+        )
+        Log.e("TEST", "autoRefund: $autoRefund")
+        Log.e("TEST", "refundUri: $refundUri")
+
+        mRefundResult.value = RefundResult.Success(refundUri, item, amount, 
reason)
+    }
+
+    @UiThread
+    private fun onRefundError() {
+        mRefundResult.value = RefundResult.Error
+    }
+
+}
diff --git 
a/app/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt 
b/app/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
new file mode 100644
index 0000000..f2bd569
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
@@ -0,0 +1,65 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.history
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import kotlinx.android.synthetic.main.fragment_refund_uri.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.NfcManager.Companion.hasNfc
+import net.taler.merchantpos.QrCodeManager.makeQrCode
+import net.taler.merchantpos.R
+
+class RefundUriFragment : Fragment() {
+
+    private val model: MainViewModel by activityViewModels()
+    private val refundManager by lazy { model.refundManager }
+
+    override fun onCreateView(
+        inflater: LayoutInflater, container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_refund_uri, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        val result = refundManager.refundResult.value
+        if (result !is RefundResult.Success) throw IllegalStateException()
+
+        refundQrcodeView.setImageBitmap(makeQrCode(result.refundUri))
+
+        val introRes =
+            if (hasNfc(requireContext())) R.string.refund_intro_nfc else 
R.string.refund_intro
+        refundIntroView.setText(introRes)
+
+        @SuppressLint("SetTextI18n")
+        refundAmountView.text = "${result.amount} 
${result.item.amount.currency}"
+
+        refundRefView.text =
+            getString(R.string.refund_order_ref, result.item.orderId, 
result.reason)
+
+        cancelRefundButton.setOnClickListener { 
findNavController().navigateUp() }
+    }
+
+}
diff --git a/app/src/main/java/net/taler/merchantpos/order/Definitions.kt 
b/app/src/main/java/net/taler/merchantpos/order/Definitions.kt
index b22413c..63eda17 100644
--- a/app/src/main/java/net/taler/merchantpos/order/Definitions.kt
+++ b/app/src/main/java/net/taler/merchantpos/order/Definitions.kt
@@ -120,9 +120,12 @@ data class Order(val id: Int, val availableCategories: 
Map<Int, Category>) {
     val products = ArrayList<ConfigProduct>()
     val title: String = id.toString()
     val summary: String
-        get() = getCategoryQuantities().map { (category: Category, quantity: 
Int) ->
-            "$quantity x ${category.localizedName}"
-        }.joinToString()
+        get() {
+            if (products.size == 1) return products[0].description
+            return getCategoryQuantities().map { (category: Category, 
quantity: Int) ->
+                "$quantity x ${category.localizedName}"
+            }.joinToString()
+        }
     val total: Double
         get() {
             var total = 0.0
@@ -175,6 +178,7 @@ data class Order(val id: Int, val availableCategories: 
Map<Int, Category>) {
      */
     val summaryI18n: Map<String, String>?
         get() {
+            if (products.size == 1) return products[0].descriptionI18n
             val categoryQuantities = getCategoryQuantities()
             // get all available locales
             val availableLocales = categoryQuantities.mapNotNull { (category, 
_) ->
diff --git a/app/src/main/res/drawable/ic_cash_refund.xml 
b/app/src/main/res/drawable/ic_cash_refund.xml
new file mode 100644
index 0000000..7359ca3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cash_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="#000000"
+            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/layout/fragment_refund.xml 
b/app/src/main/res/layout/fragment_refund.xml
new file mode 100644
index 0000000..5a78cdd
--- /dev/null
+++ b/app/src/main/res/layout/fragment_refund.xml
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ This file is part of GNU Taler
+  ~ (C) 2020 Taler Systems S.A.
+  ~
+  ~ GNU Taler is free software; you can redistribute it and/or modify it under 
the
+  ~ terms of the GNU General Public License as published by the Free Software
+  ~ Foundation; either version 3, or (at your option) any later version.
+  ~
+  ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT 
ANY
+  ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+  ~ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License along 
with
+  ~ GNU Taler; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
+  -->
+
+<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="match_parent"
+        tools:context=".history.RefundFragment">
+
+    <com.google.android.material.textfield.TextInputLayout
+            android:id="@+id/amountView"
+            
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp"
+            android:hint="@string/refund_amount"
+            app:boxBackgroundMode="outline"
+            app:endIconMode="clear_text"
+            app:endIconTint="?attr/colorControlNormal"
+            app:layout_constraintBottom_toTopOf="@+id/reasonView"
+            app:layout_constraintEnd_toStartOf="@+id/currencyView"
+            app:layout_constraintHorizontal_chainStyle="packed"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_chainStyle="spread">
+
+        <com.google.android.material.textfield.TextInputEditText
+                android:id="@+id/amountInputView"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:ems="6"
+                android:inputType="numberDecimal"
+                tools:text="23.42" />
+
+    </com.google.android.material.textfield.TextInputLayout>
+
+    <TextView
+            android:id="@+id/currencyView"
+            android:layout_width="wrap_content"
+            android:layout_height="0dp"
+            android:layout_marginStart="8dp"
+            android:gravity="start|center_vertical"
+            app:layout_constraintBottom_toBottomOf="@+id/amountView"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/amountView"
+            app:layout_constraintTop_toTopOf="@+id/amountView"
+            tools:text="TESTKUDOS" />
+
+    <com.google.android.material.textfield.TextInputLayout
+            android:id="@+id/reasonView"
+            
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp"
+            android:hint="@string/refund_reason"
+            app:endIconMode="clear_text"
+            app:layout_constraintBottom_toTopOf="@+id/abortButton"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintHorizontal_bias="0.5"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/amountView">
+
+        <com.google.android.material.textfield.TextInputEditText
+                android:id="@+id/reasonInputView"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                
android:inputType="textAutoComplete|textAutoCorrect|textMultiLine" />
+
+    </com.google.android.material.textfield.TextInputLayout>
+
+    <Button
+            android:id="@+id/abortButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp"
+            android:backgroundTint="@color/red"
+            android:text="@string/refund_abort"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/refundButton"
+            app:layout_constraintHorizontal_bias="0.76"
+            app:layout_constraintHorizontal_chainStyle="spread_inside"
+            app:layout_constraintStart_toStartOf="parent" />
+
+    <Button
+            android:id="@+id/refundButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp"
+            android:backgroundTint="@color/green"
+            android:text="@string/refund_confirm"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintHorizontal_bias="0.5"
+            app:layout_constraintStart_toEndOf="@+id/abortButton" />
+
+    <ProgressBar
+            android:id="@+id/progressBar"
+            style="?android:attr/progressBarStyle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:visibility="invisible"
+            app:layout_constraintBottom_toBottomOf="@+id/refundButton"
+            app:layout_constraintEnd_toEndOf="@+id/refundButton"
+            app:layout_constraintStart_toStartOf="@+id/refundButton"
+            app:layout_constraintTop_toTopOf="@+id/refundButton"
+            tools:visibility="visible" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/fragment_process_payment.xml 
b/app/src/main/res/layout/fragment_refund_uri.xml
similarity index 68%
copy from app/src/main/res/layout/fragment_process_payment.xml
copy to app/src/main/res/layout/fragment_refund_uri.xml
index 6cd8ea1..8447d28 100644
--- a/app/src/main/res/layout/fragment_process_payment.xml
+++ b/app/src/main/res/layout/fragment_refund_uri.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
   ~ This file is part of GNU Taler
   ~ (C) 2020 Taler Systems S.A.
   ~
@@ -23,28 +22,16 @@
         tools:context=".payment.ProcessPaymentFragment">
 
     <ImageView
-            android:id="@+id/qrcodeView"
+            android:id="@+id/refundQrcodeView"
             android:layout_width="0dp"
             android:layout_height="0dp"
             android:layout_margin="32dp"
-            android:visibility="invisible"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toStartOf="@+id/guideline"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toTopOf="parent"
             tools:ignore="ContentDescription"
-            tools:src="@tools:sample/avatars"
-            tools:visibility="visible" />
-
-    <ProgressBar
-            android:id="@+id/progressBar"
-            style="?android:attr/progressBarStyleLarge"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            app:layout_constraintBottom_toBottomOf="@+id/qrcodeView"
-            app:layout_constraintEnd_toEndOf="@+id/qrcodeView"
-            app:layout_constraintStart_toStartOf="@+id/qrcodeView"
-            app:layout_constraintTop_toTopOf="@+id/qrcodeView" />
+            tools:src="@tools:sample/avatars" />
 
     <androidx.constraintlayout.widget.Guideline
             android:id="@+id/guideline"
@@ -54,54 +41,50 @@
             app:layout_constraintGuide_percent="0.54" />
 
     <TextView
-            android:id="@+id/payIntroView"
+            android:id="@+id/refundIntroView"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_margin="16dp"
-            android:text="@string/payment_intro_nfc"
+            android:text="@string/refund_intro_nfc"
             android:textAlignment="center"
             android:textSize="24sp"
-            android:visibility="invisible"
-            app:layout_constraintBottom_toTopOf="@+id/amountView"
+            app:layout_constraintBottom_toTopOf="@+id/refundAmountView"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="@+id/guideline"
             app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintVertical_chainStyle="spread"
-            tools:visibility="visible" />
+            app:layout_constraintVertical_chainStyle="spread" />
 
     <TextView
-            android:id="@+id/amountView"
+            android:id="@+id/refundAmountView"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_margin="16dp"
             android:textAppearance="@style/TextAppearance.AppCompat.Headline"
-            app:layout_constraintBottom_toTopOf="@+id/orderRefView"
+            app:layout_constraintBottom_toTopOf="@+id/refundRefView"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="@+id/guideline"
-            app:layout_constraintTop_toBottomOf="@+id/payIntroView"
+            app:layout_constraintTop_toBottomOf="@+id/refundIntroView"
             tools:text="10.49 TESTKUDOS" />
 
     <TextView
-            android:id="@+id/orderRefView"
+            android:id="@+id/refundRefView"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_margin="16dp"
             android:textAlignment="center"
-            android:visibility="invisible"
-            app:layout_constraintBottom_toTopOf="@id/cancelPaymentButton"
+            app:layout_constraintBottom_toTopOf="@id/cancelRefundButton"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="@+id/guideline"
-            app:layout_constraintTop_toBottomOf="@+id/amountView"
-            tools:text="@string/payment_order_ref"
-            tools:visibility="visible" />
+            app:layout_constraintTop_toBottomOf="@+id/refundAmountView"
+            tools:text="@string/refund_order_ref" />
 
     <Button
-            android:id="@+id/cancelPaymentButton"
+            android:id="@+id/cancelRefundButton"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_margin="16dp"
             android:backgroundTint="@color/red"
-            android:text="@string/payment_cancel"
+            android:text="@string/refund_abort"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintHorizontal_bias="0.0"
diff --git a/app/src/main/res/layout/history_row.xml 
b/app/src/main/res/layout/history_row.xml
deleted file mode 100644
index e68e5a0..0000000
--- a/app/src/main/res/layout/history_row.xml
+++ /dev/null
@@ -1,91 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ This file is part of GNU Taler
-  ~ (C) 2020 Taler Systems S.A.
-  ~
-  ~ GNU Taler is free software; you can redistribute it and/or modify it under 
the
-  ~ terms of the GNU General Public License as published by the Free Software
-  ~ Foundation; either version 3, or (at your option) any later version.
-  ~
-  ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT 
ANY
-  ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
-  ~ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-  ~
-  ~ You should have received a copy of the GNU General Public License along 
with
-  ~ GNU Taler; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
-  -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android";
-        xmlns:tools="http://schemas.android.com/tools";
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginStart="16dp"
-        android:layout_marginTop="8dp"
-        android:layout_marginEnd="16dp"
-        android:layout_marginBottom="8dp"
-        android:orientation="vertical">
-
-    <TextView
-            android:id="@+id/text_history_summary"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textSize="24sp"
-            android:textStyle="bold"
-            tools:text="One Cappuccino" />
-
-    <TextView
-            android:id="@+id/text_history_amount"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textSize="20sp"
-            android:textStyle="bold"
-            tools:text="1 Euro" />
-
-    <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="horizontal">
-
-        <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/history_received_at"
-                android:textAllCaps="false"
-                android:textSize="20sp"
-                android:textStyle="italic" />
-
-
-        <TextView
-                android:id="@+id/text_history_time"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:textSize="20sp"
-                tools:text="2019-08-31 14:25" />
-
-    </LinearLayout>
-
-
-    <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="horizontal">
-
-        <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/history_ref_no"
-                android:textAllCaps="false"
-                android:textSize="20sp"
-                android:textStyle="italic" />
-
-        <TextView
-                android:id="@+id/text_history_order_id"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:textAllCaps="false"
-                android:textSize="20sp"
-                android:textStyle="italic"
-                tools:text="2019.242-014B6QPF1M1HC5TY" />
-    </LinearLayout>
-
-</LinearLayout>
diff --git a/app/src/main/res/layout/list_item_history.xml 
b/app/src/main/res/layout/list_item_history.xml
new file mode 100644
index 0000000..fe485ba
--- /dev/null
+++ b/app/src/main/res/layout/list_item_history.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ This file is part of GNU Taler
+  ~ (C) 2020 Taler Systems S.A.
+  ~
+  ~ GNU Taler is free software; you can redistribute it and/or modify it under 
the
+  ~ terms of the GNU General Public License as published by the Free Software
+  ~ Foundation; either version 3, or (at your option) any later version.
+  ~
+  ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT 
ANY
+  ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+  ~ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License along 
with
+  ~ GNU Taler; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
+  -->
+
+<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:padding="16dp">
+
+    <TextView
+            android:id="@+id/orderSummaryView"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:textColor="?android:attr/textColorPrimary"
+            android:textSize="20sp"
+            android:textStyle="bold"
+            app:layout_constraintEnd_toStartOf="@+id/orderAmountView"
+            app:layout_constraintHorizontal_bias="1.0"
+            app:layout_constraintHorizontal_chainStyle="spread_inside"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="One Cappuccino or another name that can be so long 
that it spans more than one line" />
+
+    <TextView
+            android:id="@+id/orderAmountView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:layout_marginEnd="16dp"
+            android:textColor="?android:attr/textColorPrimary"
+            android:textSize="20sp"
+            android:textStyle="bold"
+            app:layout_constraintBottom_toBottomOf="@+id/orderSummaryView"
+            app:layout_constraintEnd_toStartOf="@+id/refundButton"
+            app:layout_constraintStart_toEndOf="@+id/orderSummaryView"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_bias="0.0"
+            tools:text="23.42 TESTKUDOS" />
+
+    <TextView
+            android:id="@+id/orderIdView"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="8dp"
+            android:text="@string/history_ref_no"
+            android:textAllCaps="false"
+            android:textSize="20sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/orderTimeView"
+            app:layout_constraintHorizontal_bias="0.5"
+            app:layout_constraintHorizontal_chainStyle="spread_inside"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/orderSummaryView" />
+
+    <TextView
+            android:id="@+id/orderTimeView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:layout_marginTop="8dp"
+            android:layout_marginEnd="16dp"
+            android:textSize="20sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/refundButton"
+            app:layout_constraintStart_toEndOf="@+id/orderIdView"
+            app:layout_constraintTop_toBottomOf="@+id/orderAmountView"
+            app:layout_constraintVertical_bias="1.0"
+            tools:text="3 hrs. ago" />
+
+    <ImageButton
+            android:id="@+id/refundButton"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:backgroundTint="?colorPrimary"
+            android:contentDescription="@string/history_refund"
+            android:tint="?attr/colorOnPrimary"
+            android:visibility="gone"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/ic_cash_refund" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/navigation/nav_graph.xml 
b/app/src/main/res/navigation/nav_graph.xml
index f00d21c..2e337f2 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
   ~ This file is part of GNU Taler
   ~ (C) 2020 Taler Systems S.A.
   ~
@@ -55,9 +54,29 @@
 
     <fragment
             android:id="@+id/nav_history"
-            android:name="net.taler.merchantpos.MerchantHistory"
+            
android:name="net.taler.merchantpos.history.MerchantHistoryFragment"
             android:label="@string/history_label"
-            tools:layout="@layout/fragment_merchant_history" />
+            tools:layout="@layout/fragment_merchant_history">
+        <action
+                android:id="@+id/action_nav_history_to_refundFragment"
+                app:destination="@id/refundFragment" />
+    </fragment>
+
+    <fragment
+            android:id="@+id/refundFragment"
+            android:name="net.taler.merchantpos.history.RefundFragment"
+            android:label="@string/history_refund"
+            tools:layout="@layout/fragment_refund">
+        <action
+                android:id="@+id/action_refundFragment_to_refundUriFragment"
+                app:destination="@id/refundUriFragment" />
+    </fragment>
+
+    <fragment
+            android:id="@+id/refundUriFragment"
+            android:name="net.taler.merchantpos.history.RefundUriFragment"
+            android:label="@string/history_refund"
+            tools:layout="@layout/fragment_refund_uri" />
 
     <fragment
             android:id="@+id/nav_settings"
diff --git a/app/src/main/res/values/strings.xml 
b/app/src/main/res/values/strings.xml
index 740a080..77c7e03 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -47,7 +47,19 @@
 
     <string name="history_label">Payment History</string>
     <string name="history_received_at">Received at</string>
-    <string name="history_ref_no">Ref. No:</string>
+    <string name="history_ref_no">Ref. No: %s</string>
+    <string name="history_refund">Refund Order</string>
+    <string name="refund_amount">Amount</string>
+    <string name="refund_reason">Refund reason</string>
+    <string name="refund_abort">Abort</string>
+    <string name="refund_confirm">Give Refund</string>
+    <string name="refund_error_max_amount">Greater than order amount of 
%s</string>
+    <string name="refund_error_zero">Needs to be positive amount</string>
+    <string name="refund_error_backend">Error processing refund</string>
+    <string name="refund_error_deadline">Refund deadline has passed</string>
+    <string name="refund_intro_nfc">Please scan QR Code or use NFC to give 
refund</string>
+    <string name="refund_intro">Please scan QR Code to give refund</string>
+    <string name="refund_order_ref">Order Reference: %1$s\n\n%2$s</string>
 
     <string name="error_network">Network Error</string>
 

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



reply via email to

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