gnunet-svn
[Top][All Lists]
Advanced

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

[taler-taler-android] branch master updated: Factor out code from mercha


From: gnunet
Subject: [taler-taler-android] branch master updated: Factor out code from merchant-terminal into common library
Date: Wed, 18 Mar 2020 21:24:29 +0100

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

torsten-grote pushed a commit to branch master
in repository taler-android.

The following commit(s) were added to refs/heads/master by this push:
     new 82b8b57  Factor out code from merchant-terminal into common library
82b8b57 is described below

commit 82b8b57dc16112b859150696199774fcf06655e1
Author: Torsten Grote <address@hidden>
AuthorDate: Wed Mar 18 17:24:02 2020 -0300

    Factor out code from merchant-terminal into common library
---
 merchant-terminal/.gitlab-ci.yml                   |   4 +-
 merchant-terminal/build.gradle                     |  18 +-
 .../src/main/java/net/taler/merchantpos/Amount.kt  |  48 -----
 .../java/net/taler/merchantpos/MainActivity.kt     |   1 +
 .../main/java/net/taler/merchantpos/NfcManager.kt  | 233 ---------------------
 .../java/net/taler/merchantpos/QrCodeManager.kt    |  42 ----
 .../src/main/java/net/taler/merchantpos/Utils.kt   | 121 -----------
 .../merchantpos/config/ConfigFetcherFragment.kt    |   7 +-
 .../net/taler/merchantpos/config/MerchantConfig.kt |  45 ++++
 .../merchantpos/config/MerchantConfigFragment.kt   |   5 +-
 .../taler/merchantpos/history/HistoryManager.kt    |  11 +-
 .../merchantpos/history/MerchantHistoryFragment.kt |   6 +-
 .../taler/merchantpos/history/RefundFragment.kt    |   6 +-
 .../taler/merchantpos/history/RefundUriFragment.kt |   4 +-
 .../taler/merchantpos/order/CategoriesFragment.kt  |   1 +
 .../java/net/taler/merchantpos/order/LiveOrder.kt  |   4 +-
 .../merchantpos/order/{Definitions.kt => Order.kt} | 101 +--------
 .../net/taler/merchantpos/order/OrderFragment.kt   |   9 +-
 .../net/taler/merchantpos/order/OrderManager.kt    |   4 +-
 .../taler/merchantpos/order/OrderStateFragment.kt  |   5 +-
 .../taler/merchantpos/order/ProductsFragment.kt    |   1 +
 .../taler/merchantpos/payment/PaymentManager.kt    |   3 +-
 .../merchantpos/payment/ProcessPaymentFragment.kt  |  12 +-
 taler-kotlin-common/build.gradle                   |   7 +
 .../src/main/java/net/taler/common/Amount.kt       |  21 +-
 .../src/main/java/net/taler/common/AndroidUtils.kt |  48 ++++-
 .../main/java/net/taler/common/CombinedLiveData.kt |  51 +++++
 .../main/java/net/taler/common/ContractTerms.kt    |  61 ++++++
 .../src/main/java/net/taler/common/TalerUtils.kt   |  51 +++++
 29 files changed, 321 insertions(+), 609 deletions(-)

diff --git a/merchant-terminal/.gitlab-ci.yml b/merchant-terminal/.gitlab-ci.yml
index 467824c..9bd889b 100644
--- a/merchant-terminal/.gitlab-ci.yml
+++ b/merchant-terminal/.gitlab-ci.yml
@@ -2,7 +2,7 @@ merchant_test:
   stage: test
   only:
     changes:
-      - "merchant-terminal"
+      - merchant-terminal/**/*
   script: ./gradlew :merchant-terminal:lint :merchant-terminal:assembleRelease
   artifacts:
     paths:
@@ -15,7 +15,7 @@ merchant_deploy_nightly:
     refs:
       - master
     changes:
-      - "merchant-terminal"
+      - merchant-terminal/**/*
   needs: ["merchant_test"]
   script:
     # Ensure that key exists
diff --git a/merchant-terminal/build.gradle b/merchant-terminal/build.gradle
index 594cab3..dd2527e 100644
--- a/merchant-terminal/build.gradle
+++ b/merchant-terminal/build.gradle
@@ -5,7 +5,9 @@ apply plugin: "androidx.navigation.safeargs.kotlin"
 
 android {
     compileSdkVersion 29
-    buildToolsVersion "29.0.3"
+    //noinspection GradleDependency
+    buildToolsVersion "$build_tools_version"
+
     defaultConfig {
         applicationId "net.taler.merchantpos"
         minSdkVersion 26
@@ -14,6 +16,7 @@ android {
         versionName "1.0"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
+
     buildTypes {
         release {
             minifyEnabled true
@@ -43,9 +46,8 @@ android {
 }
 
 dependencies {
-    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-    implementation 'androidx.appcompat:appcompat:1.1.0'
-    implementation 'androidx.core:core-ktx:1.2.0'
+    implementation project(":taler-kotlin-common")
+
     implementation 'com.google.android.material:material:1.1.0'
     implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
     implementation "androidx.recyclerview:recyclerview:1.1.0"
@@ -55,17 +57,9 @@ dependencies {
     implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
     implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
 
-    // ViewModel and LiveData
-    def lifecycle_version = "2.2.0"
-    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
-    implementation 
"androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
-
     // HTTP Requests
     implementation 'com.android.volley:volley:1.1.1'
 
-    // QR codes
-    implementation 'com.google.zxing:core:3.4.0'
-
     implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
 
     // JSON parsing and serialization
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt
deleted file mode 100644
index 17ddd61..0000000
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt
+++ /dev/null
@@ -1,48 +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 org.json.JSONObject
-
-data class Amount(val currency: String, val amount: String) {
-    @Suppress("unused")
-    fun isZero(): Boolean {
-        return amount.toDouble() == 0.0
-    }
-
-    companion object {
-        private const val FRACTIONAL_BASE = 1e8
-
-        @Suppress("unused")
-        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])
-        }
-    }
-}
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt
index 0c6bdfa..d6e3747 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt
@@ -36,6 +36,7 @@ import androidx.navigation.ui.setupWithNavController
 import 
com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener
 import kotlinx.android.synthetic.main.activity_main.*
 import kotlinx.android.synthetic.main.app_bar_main.*
+import net.taler.common.NfcManager
 
 class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener {
 
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt
deleted file mode 100644
index 09c1470..0000000
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt
+++ /dev/null
@@ -1,233 +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.app.Activity
-import android.content.Context
-import android.nfc.NfcAdapter
-import android.nfc.NfcAdapter.FLAG_READER_NFC_A
-import android.nfc.NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
-import android.nfc.Tag
-import android.nfc.tech.IsoDep
-import android.util.Log
-import net.taler.merchantpos.Utils.hexStringToByteArray
-import org.json.JSONObject
-import java.io.ByteArrayOutputStream
-import java.net.URL
-import javax.net.ssl.HttpsURLConnection
-
-@Suppress("unused")
-private const val TALER_AID = "A0000002471001"
-
-class NfcManager : NfcAdapter.ReaderCallback {
-
-    companion object {
-        const val TAG = "taler-merchant"
-
-        /**
-         * Returns true if NFC is supported and false otherwise.
-         */
-        fun hasNfc(context: Context): Boolean {
-            return getNfcAdapter(context) != null
-        }
-
-        /**
-         * Enables NFC reader mode. Don't forget to call [stop] afterwards.
-         */
-        fun start(activity: Activity, nfcManager: NfcManager) {
-            getNfcAdapter(activity)?.enableReaderMode(activity, nfcManager, 
nfcManager.flags, null)
-        }
-
-        /**
-         * Disables NFC reader mode. Call after [start].
-         */
-        fun stop(activity: Activity) {
-            getNfcAdapter(activity)?.disableReaderMode(activity)
-        }
-
-        private fun getNfcAdapter(context: Context): NfcAdapter? {
-            return NfcAdapter.getDefaultAdapter(context)
-        }
-    }
-
-    private val flags = FLAG_READER_NFC_A or FLAG_READER_SKIP_NDEF_CHECK
-
-    private var tagString: String? = null
-    private var currentTag: IsoDep? = null
-
-    fun setTagString(tagString: String) {
-        this.tagString = tagString
-    }
-
-    override fun onTagDiscovered(tag: Tag?) {
-
-        Log.v(TAG, "tag discovered")
-
-        val isoDep = IsoDep.get(tag)
-        isoDep.connect()
-
-        currentTag = isoDep
-
-        isoDep.transceive(apduSelectFile())
-
-        val tagString: String? = tagString
-        if (tagString != null) {
-            isoDep.transceive(apduPutTalerData(1, tagString.toByteArray()))
-        }
-
-        // FIXME: use better pattern for sleeps in between requests
-        // -> start with fast polling, poll more slowly if no requests are 
coming
-
-        while (true) {
-            try {
-                val reqFrame = isoDep.transceive(apduGetData())
-                if (reqFrame.size < 2) {
-                    Log.v(TAG, "request frame too small")
-                    break
-                }
-                val req = ByteArray(reqFrame.size - 2)
-                if (req.isEmpty()) {
-                    continue
-                }
-                reqFrame.copyInto(req, 0, 0, reqFrame.size - 2)
-                val jsonReq = JSONObject(req.toString(Charsets.UTF_8))
-                val reqId = jsonReq.getInt("id")
-                Log.v(TAG, "got request $jsonReq")
-                val jsonInnerReq = jsonReq.getJSONObject("request")
-                val method = jsonInnerReq.getString("method")
-                val urlStr = jsonInnerReq.getString("url")
-                Log.v(TAG, "url '$urlStr'")
-                Log.v(TAG, "method '$method'")
-                val url = URL(urlStr)
-                val conn: HttpsURLConnection = url.openConnection() as 
HttpsURLConnection
-                conn.setRequestProperty("Accept", "application/json")
-                conn.connectTimeout = 5000
-                conn.doInput = true
-                when (method) {
-                    "get" -> {
-                        conn.requestMethod = "GET"
-                    }
-                    "postJson" -> {
-                        conn.requestMethod = "POST"
-                        conn.doOutput = true
-                        conn.setRequestProperty("Content-Type", 
"application/json; utf-8")
-                        val body = jsonInnerReq.getString("body")
-                        
conn.outputStream.write(body.toByteArray(Charsets.UTF_8))
-                    }
-                    else -> {
-                        throw Exception("method not supported")
-                    }
-                }
-                Log.v(TAG, "connecting")
-                conn.connect()
-                Log.v(TAG, "connected")
-
-                val statusCode = conn.responseCode
-                val tunnelResp = JSONObject()
-                tunnelResp.put("id", reqId)
-                tunnelResp.put("status", conn.responseCode)
-
-                if (statusCode == 200) {
-                    val stream = conn.inputStream
-                    val httpResp = stream.buffered().readBytes()
-                    tunnelResp.put("responseJson", 
JSONObject(httpResp.toString(Charsets.UTF_8)))
-                }
-
-                Log.v(TAG, "sending: $tunnelResp")
-
-                isoDep.transceive(apduPutTalerData(2, 
tunnelResp.toString().toByteArray()))
-            } catch (e: Exception) {
-                Log.v(TAG, "exception during NFC loop: $e")
-                break
-            }
-        }
-
-        isoDep.close()
-    }
-
-    private fun writeApduLength(stream: ByteArrayOutputStream, size: Int) {
-        when {
-            size == 0 -> {
-                // No size field needed!
-            }
-            size <= 255 -> // One byte size field
-                stream.write(size)
-            size <= 65535 -> {
-                stream.write(0)
-                // FIXME: is this supposed to be little or big endian?
-                stream.write(size and 0xFF)
-                stream.write((size ushr 8) and 0xFF)
-            }
-            else -> throw Error("payload too big")
-        }
-    }
-
-    private fun apduSelectFile(): ByteArray {
-        return hexStringToByteArray("00A4040007A0000002471001")
-    }
-
-    private fun apduPutData(payload: ByteArray): ByteArray {
-        val stream = ByteArrayOutputStream()
-
-        // Class
-        stream.write(0x00)
-
-        // Instruction 0xDA = put data
-        stream.write(0xDA)
-
-        // Instruction parameters
-        // (proprietary encoding)
-        stream.write(0x01)
-        stream.write(0x00)
-
-        writeApduLength(stream, payload.size)
-
-        stream.write(payload)
-
-        return stream.toByteArray()
-    }
-
-    private fun apduPutTalerData(talerInst: Int, payload: ByteArray): 
ByteArray {
-        val realPayload = ByteArrayOutputStream()
-        realPayload.write(talerInst)
-        realPayload.write(payload)
-        return apduPutData(realPayload.toByteArray())
-    }
-
-    private fun apduGetData(): ByteArray {
-        val stream = ByteArrayOutputStream()
-
-        // Class
-        stream.write(0x00)
-
-        // Instruction 0xCA = get data
-        stream.write(0xCA)
-
-        // Instruction parameters
-        // (proprietary encoding)
-        stream.write(0x01)
-        stream.write(0x00)
-
-        // Max expected response size, two
-        // zero bytes denotes 65536
-        stream.write(0x0)
-        stream.write(0x0)
-
-        return stream.toByteArray()
-    }
-
-}
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt
deleted file mode 100644
index 595e7ac..0000000
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt
+++ /dev/null
@@ -1,42 +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.graphics.Bitmap
-import android.graphics.Bitmap.Config.RGB_565
-import android.graphics.Color.BLACK
-import android.graphics.Color.WHITE
-import com.google.zxing.BarcodeFormat.QR_CODE
-import com.google.zxing.qrcode.QRCodeWriter
-
-object QrCodeManager {
-
-    fun makeQrCode(text: String, size: Int = 256): Bitmap {
-        val qrCodeWriter = QRCodeWriter()
-        val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size)
-        val height = bitMatrix.height
-        val width = bitMatrix.width
-        val bmp = Bitmap.createBitmap(width, height, RGB_565)
-        for (x in 0 until width) {
-            for (y in 0 until height) {
-                bmp.setPixel(x, y, if (bitMatrix.get(x, y)) BLACK else WHITE)
-            }
-        }
-        return bmp
-    }
-
-}
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt
index a0c30d6..578debf 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt
@@ -16,86 +16,12 @@
 
 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
 
-object Utils {
-
-    private const val HEX_CHARS = "0123456789ABCDEF"
-
-    fun hexStringToByteArray(data: String): ByteArray {
-        val result = ByteArray(data.length / 2)
-
-        for (i in data.indices step 2) {
-            val firstIndex = HEX_CHARS.indexOf(data[i])
-            val secondIndex = HEX_CHARS.indexOf(data[i + 1])
-
-            val octet = firstIndex.shl(4).or(secondIndex)
-            result[i.shr(1)] = octet.toByte()
-        }
-        return result
-    }
-
-
-    private val HEX_CHARS_ARRAY = HEX_CHARS.toCharArray()
-
-    @Suppress("unused")
-    fun toHex(byteArray: ByteArray): String {
-        val result = StringBuffer()
-
-        byteArray.forEach {
-            val octet = it.toInt()
-            val firstIndex = (octet and 0xF0).ushr(4)
-            val secondIndex = octet and 0x0F
-            result.append(HEX_CHARS_ARRAY[firstIndex])
-            result.append(HEX_CHARS_ARRAY[secondIndex])
-        }
-        return result.toString()
-    }
-
-}
-
-fun View.fadeIn(endAction: () -> Unit = {}) {
-    if (visibility == VISIBLE) return
-    alpha = 0f
-    visibility = VISIBLE
-    animate().alpha(1f).withEndAction {
-        if (context != null) endAction.invoke()
-    }.start()
-}
-
-fun View.fadeOut(endAction: () -> Unit = {}) {
-    if (visibility == INVISIBLE) return
-    animate().alpha(0f).withEndAction {
-        if (context == null) return@withEndAction
-        visibility = INVISIBLE
-        alpha = 1f
-        endAction.invoke()
-    }.start()
-}
-
 fun topSnackbar(view: View, text: CharSequence, @Duration duration: Int) {
     make(view, text, duration)
         .setAnimationMode(ANIMATION_MODE_FADE)
@@ -106,50 +32,3 @@ fun topSnackbar(view: View, text: CharSequence, @Duration 
duration: Int) {
 fun topSnackbar(view: View, @StringRes resId: Int, @Duration duration: Int) {
     topSnackbar(view, view.resources.getText(resId), duration)
 }
-
-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>,
-    private val combine: (data1: T?, data2: K?) -> S
-) : MediatorLiveData<S>() {
-
-    private var data1: T? = null
-    private var data2: K? = null
-
-    init {
-        super.addSource(source1) { t ->
-            data1 = t
-            value = combine(data1, data2)
-        }
-        super.addSource(source2) { k ->
-            data2 = k
-            value = combine(data1, data2)
-        }
-    }
-
-    override fun <S : Any?> addSource(source: LiveData<S>, onChanged: 
Observer<in S>) {
-        throw UnsupportedOperationException()
-    }
-
-    override fun <T : Any?> removeSource(toRemote: LiveData<T>) {
-        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/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
index c370e33..c0c87dc 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
@@ -23,14 +23,13 @@ import android.view.ViewGroup
 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.Snackbar
 import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
+import net.taler.common.navigate
 import net.taler.merchantpos.MainViewModel
 import net.taler.merchantpos.R
 import 
net.taler.merchantpos.config.ConfigFetcherFragmentDirections.Companion.actionConfigFetcherToMerchantSettings
 import 
net.taler.merchantpos.config.ConfigFetcherFragmentDirections.Companion.actionConfigFetcherToOrder
-import net.taler.merchantpos.navigate
 
 class ConfigFetcherFragment : Fragment() {
 
@@ -52,7 +51,7 @@ class ConfigFetcherFragment : Fragment() {
                 null -> return@Observer
                 is ConfigUpdateResult.Error -> onNetworkError(result.msg)
                 is ConfigUpdateResult.Success -> {
-                    actionConfigFetcherToOrder().navigate(findNavController())
+                    navigate(actionConfigFetcherToOrder())
                 }
             }
         })
@@ -60,7 +59,7 @@ class ConfigFetcherFragment : Fragment() {
 
     private fun onNetworkError(msg: String) {
         Snackbar.make(view!!, msg, LENGTH_SHORT).show()
-        actionConfigFetcherToMerchantSettings().navigate(findNavController())
+        navigate(actionConfigFetcherToMerchantSettings())
     }
 
 }
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
index 2050e28..8141f0f 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
@@ -17,7 +17,13 @@
 package net.taler.merchantpos.config
 
 import android.net.Uri
+import com.fasterxml.jackson.annotation.JsonIgnore
 import com.fasterxml.jackson.annotation.JsonProperty
+import net.taler.common.Amount
+import net.taler.common.ContractProduct
+import net.taler.common.Product
+import net.taler.common.TalerUtils
+import java.util.*
 
 data class Config(
     val configUrl: String,
@@ -45,3 +51,42 @@ data class MerchantConfig(
         return uriBuilder.toString()
     }
 }
+
+data class Category(
+    val id: Int,
+    val name: String,
+    @JsonProperty("name_i18n")
+    val nameI18n: Map<String, String>?
+) {
+    var selected: Boolean = false
+    val localizedName: String get() = TalerUtils.getLocalizedString(nameI18n, 
name)
+}
+
+data class ConfigProduct(
+    @JsonIgnore
+    val id: String = UUID.randomUUID().toString(),
+    override val productId: String?,
+    override val description: String,
+    override val descriptionI18n: Map<String, String>?,
+    override val price: String,
+    override val location: String?,
+    override val image: String?,
+    val categories: List<Int>,
+    @JsonIgnore
+    val quantity: Int = 0
+) : Product() {
+    val priceAsDouble by lazy { Amount.fromString(price).amount.toDouble() }
+
+    fun toContractProduct() = ContractProduct(
+        productId = productId,
+        description = description,
+        descriptionI18n = descriptionI18n,
+        price = price,
+        location = location,
+        image = image,
+        quantity = quantity
+    )
+
+    override fun equals(other: Any?) = other is ConfigProduct && id == other.id
+    override fun hashCode() = id.hashCode()
+}
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
index aad1c93..a584af8 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
@@ -28,14 +28,13 @@ import android.view.ViewGroup
 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_merchant_config.*
+import net.taler.common.navigate
 import net.taler.merchantpos.MainViewModel
 import net.taler.merchantpos.R
 import 
net.taler.merchantpos.config.MerchantConfigFragmentDirections.Companion.actionSettingsToOrder
-import net.taler.merchantpos.navigate
 import net.taler.merchantpos.topSnackbar
 
 /**
@@ -149,7 +148,7 @@ class MerchantConfigFragment : Fragment() {
         onResultReceived()
         updateView()
         topSnackbar(view!!, getString(R.string.config_changed, currency), 
LENGTH_LONG)
-        actionSettingsToOrder().navigate(findNavController())
+        navigate(actionSettingsToOrder())
     }
 
     private fun onError(msg: String) {
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
index faee226..fc3f93a 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
@@ -24,22 +24,15 @@ 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.common.Amount
+import net.taler.common.Timestamp
 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,
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
index 0c53f71..afa925d 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
@@ -35,14 +35,14 @@ 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.common.exhaustive
+import net.taler.common.navigate
+import net.taler.common.toRelativeTime
 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 {
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
index 1797cea..aa2489a 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
@@ -28,15 +28,15 @@ 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.common.fadeIn
+import net.taler.common.fadeOut
+import net.taler.common.navigate
 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() {
 
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
index f2bd569..6e5b96d 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
@@ -25,9 +25,9 @@ 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.common.NfcManager.Companion.hasNfc
+import net.taler.common.QrCodeManager.makeQrCode
 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() {
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
index 34b97c0..e935d4f 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
@@ -31,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView.Adapter
 import kotlinx.android.synthetic.main.fragment_categories.*
 import net.taler.merchantpos.MainViewModel
 import net.taler.merchantpos.R
+import net.taler.merchantpos.config.Category
 import net.taler.merchantpos.order.CategoryAdapter.CategoryViewHolder
 
 interface CategorySelectionListener {
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt
index ff6061a..847326b 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt
@@ -20,7 +20,9 @@ import androidx.annotation.UiThread
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Transformations
-import net.taler.merchantpos.CombinedLiveData
+import net.taler.common.CombinedLiveData
+import net.taler.merchantpos.config.Category
+import net.taler.merchantpos.config.ConfigProduct
 import net.taler.merchantpos.order.RestartState.DISABLED
 import net.taler.merchantpos.order.RestartState.ENABLED
 import net.taler.merchantpos.order.RestartState.UNDO
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Definitions.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt
similarity index 55%
rename from 
merchant-terminal/src/main/java/net/taler/merchantpos/order/Definitions.kt
rename to merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt
index 63eda17..5954e63 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Definitions.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt
@@ -16,105 +16,8 @@
 
 package net.taler.merchantpos.order
 
-import androidx.core.os.LocaleListCompat
-import com.fasterxml.jackson.annotation.JsonIgnore
-import com.fasterxml.jackson.annotation.JsonInclude
-import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL
-import com.fasterxml.jackson.annotation.JsonProperty
-import net.taler.merchantpos.Amount
-import java.util.*
-import java.util.Locale.LanguageRange
-import kotlin.collections.ArrayList
-import kotlin.collections.HashMap
-
-data class Category(
-    val id: Int,
-    val name: String,
-    @JsonProperty("name_i18n")
-    val nameI18n: Map<String, String>?
-) {
-    var selected: Boolean = false
-    val localizedName: String get() = getLocalizedString(nameI18n, name)
-}
-
-@JsonInclude(NON_NULL)
-abstract class Product {
-    @get:JsonProperty("product_id")
-    abstract val productId: String?
-    abstract val description: String
-    @get:JsonProperty("description_i18n")
-    abstract val descriptionI18n: Map<String, String>?
-    abstract val price: String
-    @get:JsonProperty("delivery_location")
-    abstract val location: String?
-    abstract val image: String?
-    @get:JsonIgnore
-    val localizedDescription: String
-        get() = getLocalizedString(descriptionI18n, description)
-}
-
-data class ConfigProduct(
-    @JsonIgnore
-    val id: String = UUID.randomUUID().toString(),
-    override val productId: String?,
-    override val description: String,
-    override val descriptionI18n: Map<String, String>?,
-    override val price: String,
-    override val location: String?,
-    override val image: String?,
-    val categories: List<Int>,
-    @JsonIgnore
-    val quantity: Int = 0
-) : Product() {
-    val priceAsDouble by lazy { Amount.fromString(price).amount.toDouble() }
-
-    override fun equals(other: Any?) = other is ConfigProduct && id == other.id
-    override fun hashCode() = id.hashCode()
-}
-
-data class ContractProduct(
-    override val productId: String?,
-    override val description: String,
-    override val descriptionI18n: Map<String, String>?,
-    override val price: String,
-    override val location: String?,
-    override val image: String?,
-    val quantity: Int
-) : Product() {
-    constructor(product: ConfigProduct) : this(
-        product.productId,
-        product.description,
-        product.descriptionI18n,
-        product.price,
-        product.location,
-        product.image,
-        product.quantity
-    )
-}
-
-private fun getLocalizedString(map: Map<String, String>?, default: String): 
String {
-    // just return the default, if it is the only element
-    if (map == null) return default
-    // create a priority list of language ranges from system locales
-    val locales = LocaleListCompat.getDefault()
-    val priorityList = ArrayList<LanguageRange>(locales.size())
-    for (i in 0 until locales.size()) {
-        priorityList.add(LanguageRange(locales[i].toLanguageTag()))
-    }
-    // create a list of locales available in the given map
-    val availableLocales = map.keys.mapNotNull {
-        if (it == "_") return@mapNotNull null
-        val list = it.split("_")
-        when (list.size) {
-            1 -> Locale(list[0])
-            2 -> Locale(list[0], list[1])
-            3 -> Locale(list[0], list[1], list[2])
-            else -> null
-        }
-    }
-    val match = Locale.lookup(priorityList, availableLocales)
-    return match?.toString()?.let { map[it] } ?: default
-}
+import net.taler.merchantpos.config.Category
+import net.taler.merchantpos.config.ConfigProduct
 
 data class Order(val id: Int, val availableCategories: Map<Int, Category>) {
     val products = ArrayList<ConfigProduct>()
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
index 49f7cf2..ad6cd87 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
@@ -23,12 +23,11 @@ import android.view.ViewGroup
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
 import androidx.lifecycle.Observer
-import androidx.navigation.fragment.findNavController
 import androidx.transition.TransitionManager.beginDelayedTransition
 import kotlinx.android.synthetic.main.fragment_order.*
+import net.taler.common.navigate
 import net.taler.merchantpos.MainViewModel
 import net.taler.merchantpos.R
-import net.taler.merchantpos.navigate
 import 
net.taler.merchantpos.order.OrderFragmentDirections.Companion.actionGlobalConfigFetcher
 import 
net.taler.merchantpos.order.OrderFragmentDirections.Companion.actionOrderToMerchantSettings
 import 
net.taler.merchantpos.order.OrderFragmentDirections.Companion.actionOrderToProcessPayment
@@ -65,9 +64,9 @@ class OrderFragment : Fragment() {
     override fun onStart() {
         super.onStart()
         if (!viewModel.configManager.config.isValid()) {
-            actionOrderToMerchantSettings().navigate(findNavController())
+            navigate(actionOrderToMerchantSettings())
         } else if (viewModel.configManager.merchantConfig?.currency == null) {
-            actionGlobalConfigFetcher().navigate(findNavController())
+            navigate(actionGlobalConfigFetcher())
         }
     }
 
@@ -108,7 +107,7 @@ class OrderFragment : Fragment() {
         completeButton.setOnClickListener {
             val order = liveOrder.order.value ?: return@setOnClickListener
             paymentManager.createPayment(order)
-            actionOrderToProcessPayment().navigate(findNavController())
+            navigate(actionOrderToProcessPayment())
         }
     }
 
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt
index 48ddc57..a30c264 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt
@@ -24,8 +24,10 @@ import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Transformations.map
 import com.fasterxml.jackson.core.type.TypeReference
 import com.fasterxml.jackson.databind.ObjectMapper
-import net.taler.merchantpos.Amount.Companion.fromString
+import net.taler.common.Amount.Companion.fromString
 import net.taler.merchantpos.R
+import net.taler.merchantpos.config.Category
+import net.taler.merchantpos.config.ConfigProduct
 import net.taler.merchantpos.config.ConfigurationReceiver
 import net.taler.merchantpos.order.RestartState.ENABLED
 import org.json.JSONObject
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
index 1b70016..a90334b 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
@@ -37,10 +37,11 @@ import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView.Adapter
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import kotlinx.android.synthetic.main.fragment_order_state.*
+import net.taler.common.fadeIn
+import net.taler.common.fadeOut
 import net.taler.merchantpos.MainViewModel
 import net.taler.merchantpos.R
-import net.taler.merchantpos.fadeIn
-import net.taler.merchantpos.fadeOut
+import net.taler.merchantpos.config.ConfigProduct
 import net.taler.merchantpos.order.OrderAdapter.OrderLineLookup
 import net.taler.merchantpos.order.OrderAdapter.OrderViewHolder
 
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
index 4704ad0..d4da73f 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
@@ -31,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import kotlinx.android.synthetic.main.fragment_products.*
 import net.taler.merchantpos.MainViewModel
 import net.taler.merchantpos.R
+import net.taler.merchantpos.config.ConfigProduct
 import net.taler.merchantpos.order.ProductAdapter.ProductViewHolder
 
 interface ProductSelectionListener {
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
index 7f15816..4cfb069 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
@@ -30,7 +30,6 @@ import com.android.volley.VolleyError
 import com.fasterxml.jackson.databind.ObjectMapper
 import net.taler.merchantpos.config.ConfigManager
 import net.taler.merchantpos.config.MerchantRequest
-import net.taler.merchantpos.order.ContractProduct
 import net.taler.merchantpos.order.Order
 import org.json.JSONArray
 import org.json.JSONObject
@@ -103,7 +102,7 @@ class PaymentManager(
     }
 
     private fun Order.getProductsJson(): JSONArray {
-        val contractProducts = products.map { ContractProduct(it) }
+        val contractProducts = products.map { it.toContractProduct() }
         val productsStr = mapper.writeValueAsString(contractProducts)
         return JSONArray(productsStr)
     }
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
index 24f67f1..1d61894 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
@@ -27,13 +27,13 @@ import androidx.lifecycle.Observer
 import androidx.navigation.fragment.findNavController
 import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
 import kotlinx.android.synthetic.main.fragment_process_payment.*
+import net.taler.common.NfcManager.Companion.hasNfc
+import net.taler.common.QrCodeManager.makeQrCode
+import net.taler.common.fadeIn
+import net.taler.common.fadeOut
+import net.taler.common.navigate
 import net.taler.merchantpos.MainViewModel
-import net.taler.merchantpos.NfcManager.Companion.hasNfc
-import net.taler.merchantpos.QrCodeManager.makeQrCode
 import net.taler.merchantpos.R
-import net.taler.merchantpos.fadeIn
-import net.taler.merchantpos.fadeOut
-import net.taler.merchantpos.navigate
 import 
net.taler.merchantpos.payment.ProcessPaymentFragmentDirections.Companion.actionProcessPaymentToPaymentSuccess
 import net.taler.merchantpos.topSnackbar
 
@@ -69,7 +69,7 @@ class ProcessPaymentFragment : Fragment() {
         }
         if (payment.paid) {
             model.orderManager.onOrderPaid(payment.order.id)
-            
actionProcessPaymentToPaymentSuccess().navigate(findNavController())
+            navigate(actionProcessPaymentToPaymentSuccess())
             return
         }
         payIntroView.fadeIn()
diff --git a/taler-kotlin-common/build.gradle b/taler-kotlin-common/build.gradle
index d7c9362..1d45a54 100644
--- a/taler-kotlin-common/build.gradle
+++ b/taler-kotlin-common/build.gradle
@@ -47,10 +47,17 @@ dependencies {
     implementation 'androidx.appcompat:appcompat:1.1.0'
     implementation 'androidx.core:core-ktx:1.2.0'
 
+    // Navigation
+    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
+    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
+
     // ViewModel and LiveData
     def lifecycle_version = "2.2.0"
     implementation 
"androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
 
     // QR codes
     implementation 'com.google.zxing:core:3.4.0'  // needs minSdkVersion 24+
+
+    // JSON parsing and serialization
+    implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2"
 }
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt 
b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
index 428ddef..0389db1 100644
--- a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
+++ b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
@@ -16,13 +16,14 @@
 
 package net.taler.common
 
+import org.json.JSONObject
+
 data class Amount(val currency: String, val amount: String) {
 
     companion object {
-
+        private const val FRACTIONAL_BASE = 1e8
         private val SIGNED_REGEX = Regex("""([+\-])(\w+):([0-9.]+)""")
 
-        @Suppress("unused")
         fun fromString(strAmount: String): Amount {
             val components = strAmount.split(":")
             return Amount(components[0], components[1])
@@ -38,6 +39,22 @@ data class Amount(val currency: String, val amount: String) {
             // only display as many digits as required to precisely render the 
balance
             return Amount(currency, amountStr.removeSuffix(".0"))
         }
+
+        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 isZero(): Boolean {
+        return amount.toDouble() == 0.0
     }
 
     override fun toString() = "$amount $currency"
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt 
b/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt
index 2fafdf2..fc04da5 100644
--- a/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt
+++ b/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt
@@ -19,35 +19,65 @@ package net.taler.common
 import android.content.Context
 import android.content.Context.CONNECTIVITY_SERVICE
 import android.net.ConnectivityManager
-import android.net.NetworkCapabilities
-import android.os.Build
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.os.Build.VERSION.SDK_INT
+import android.text.format.DateUtils
+import android.text.format.DateUtils.DAY_IN_MILLIS
+import android.text.format.DateUtils.FORMAT_ABBREV_MONTH
+import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE
+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.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import androidx.fragment.app.Fragment
+import androidx.navigation.NavDirections
+import androidx.navigation.fragment.findNavController
 
 fun View.fadeIn(endAction: () -> Unit = {}) {
-    if (visibility == View.VISIBLE) return
+    if (visibility == VISIBLE) return
     alpha = 0f
-    visibility = View.VISIBLE
+    visibility = VISIBLE
     animate().alpha(1f).withEndAction {
-        endAction.invoke()
+        if (context != null) endAction.invoke()
     }.start()
 }
 
 fun View.fadeOut(endAction: () -> Unit = {}) {
-    if (visibility == View.INVISIBLE) return
+    if (visibility == INVISIBLE) return
     animate().alpha(0f).withEndAction {
-        visibility = View.INVISIBLE
+        if (context == null) return@withEndAction
+        visibility = INVISIBLE
         alpha = 1f
         endAction.invoke()
     }.start()
 }
 
+/**
+ * Use this with 'when' expressions when you need it to handle all 
possibilities/branches.
+ */
+val <T> T.exhaustive: T
+    get() = this
+
 fun Context.isOnline(): Boolean {
     val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
-    return if (Build.VERSION.SDK_INT < 29) {
+    return if (SDK_INT < 29) {
         @Suppress("DEPRECATION")
         cm.activeNetworkInfo?.isConnected == true
     } else {
         val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: 
return false
-        capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+        capabilities.hasCapability(NET_CAPABILITY_INTERNET)
     }
 }
+
+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
+        DateUtils.formatDateTime(context, this, flags)
+    } else DateUtils.getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, 
FORMAT_ABBREV_RELATIVE)
+}
diff --git 
a/taler-kotlin-common/src/main/java/net/taler/common/CombinedLiveData.kt 
b/taler-kotlin-common/src/main/java/net/taler/common/CombinedLiveData.kt
new file mode 100644
index 0000000..4e7016b
--- /dev/null
+++ b/taler-kotlin-common/src/main/java/net/taler/common/CombinedLiveData.kt
@@ -0,0 +1,51 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.Observer
+
+class CombinedLiveData<T, K, S>(
+    source1: LiveData<T>,
+    source2: LiveData<K>,
+    private val combine: (data1: T?, data2: K?) -> S
+) : MediatorLiveData<S>() {
+
+    private var data1: T? = null
+    private var data2: K? = null
+
+    init {
+        super.addSource(source1) { t ->
+            data1 = t
+            value = combine(data1, data2)
+        }
+        super.addSource(source2) { k ->
+            data2 = k
+            value = combine(data1, data2)
+        }
+    }
+
+    override fun <S : Any?> addSource(source: LiveData<S>, onChanged: 
Observer<in S>) {
+        throw UnsupportedOperationException()
+    }
+
+    override fun <T : Any?> removeSource(toRemote: LiveData<T>) {
+        throw UnsupportedOperationException()
+    }
+
+}
diff --git 
a/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt 
b/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt
new file mode 100644
index 0000000..1e70b6f
--- /dev/null
+++ b/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt
@@ -0,0 +1,61 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import androidx.annotation.RequiresApi
+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.JsonInclude.Include.NON_NULL
+import com.fasterxml.jackson.annotation.JsonProperty
+import net.taler.common.TalerUtils.getLocalizedString
+
+@JsonInclude(NON_NULL)
+abstract class Product {
+    @get:JsonProperty("product_id")
+    abstract val productId: String?
+    abstract val description: String
+
+    @get:JsonProperty("description_i18n")
+    abstract val descriptionI18n: Map<String, String>?
+    abstract val price: String
+
+    @get:JsonProperty("delivery_location")
+    abstract val location: String?
+    abstract val image: String?
+
+    @get:JsonIgnore
+    val localizedDescription: String
+        @RequiresApi(26)
+        get() = getLocalizedString(descriptionI18n, description)
+}
+
+data class ContractProduct(
+    override val productId: String?,
+    override val description: String,
+    override val descriptionI18n: Map<String, String>?,
+    override val price: String,
+    override val location: String?,
+    override val image: String?,
+    val quantity: Int
+) : Product()
+
+@JsonInclude(NON_EMPTY)
+class Timestamp(
+    @JsonProperty("t_ms")
+    val ms: Long
+)
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/TalerUtils.kt 
b/taler-kotlin-common/src/main/java/net/taler/common/TalerUtils.kt
new file mode 100644
index 0000000..cb1622e
--- /dev/null
+++ b/taler-kotlin-common/src/main/java/net/taler/common/TalerUtils.kt
@@ -0,0 +1,51 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import androidx.annotation.RequiresApi
+import androidx.core.os.LocaleListCompat
+import java.util.*
+import kotlin.collections.ArrayList
+
+object TalerUtils {
+
+    @RequiresApi(26)
+    fun getLocalizedString(map: Map<String, String>?, default: String): String 
{
+        // just return the default, if it is the only element
+        if (map == null) return default
+        // create a priority list of language ranges from system locales
+        val locales = LocaleListCompat.getDefault()
+        val priorityList = ArrayList<Locale.LanguageRange>(locales.size())
+        for (i in 0 until locales.size()) {
+            priorityList.add(Locale.LanguageRange(locales[i].toLanguageTag()))
+        }
+        // create a list of locales available in the given map
+        val availableLocales = map.keys.mapNotNull {
+            if (it == "_") return@mapNotNull null
+            val list = it.split("_")
+            when (list.size) {
+                1 -> Locale(list[0])
+                2 -> Locale(list[0], list[1])
+                3 -> Locale(list[0], list[1], list[2])
+                else -> null
+            }
+        }
+        val match = Locale.lookup(priorityList, availableLocales)
+        return match?.toString()?.let { map[it] } ?: default
+    }
+
+}

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



reply via email to

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