gnunet-svn
[Top][All Lists]
Advanced

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

[taler-merchant-terminal-android] 04/19: Fetch merchant config from cent


From: gnunet
Subject: [taler-merchant-terminal-android] 04/19: Fetch merchant config from central configuration JSON
Date: Fri, 21 Feb 2020 18:59:57 +0100

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

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

commit 77ee9bc073e596ef1d90cdb4edc54b68df01a4f6
Author: Torsten Grote <address@hidden>
AuthorDate: Fri Jan 31 13:10:51 2020 -0300

    Fetch merchant config from central configuration JSON
---
 .../java/net/taler/merchantpos/CreatePayment.kt    |  22 +--
 .../java/net/taler/merchantpos/MainActivity.kt     |  17 +-
 .../java/net/taler/merchantpos/MainViewModel.kt    |  50 ++++++
 .../java/net/taler/merchantpos/MerchantHistory.kt  |   5 +-
 .../java/net/taler/merchantpos/MerchantSettings.kt | 128 --------------
 .../net/taler/merchantpos/PosTerminalViewModel.kt  |  18 --
 .../java/net/taler/merchantpos/ProcessPayment.kt   |  13 +-
 .../net/taler/merchantpos/config/ConfigManager.kt  | 141 ++++++++++++++++
 .../merchantpos/{ => config}/MerchantConfig.kt     |  15 +-
 .../merchantpos/config/MerchantConfigFragment.kt   | 101 +++++++++++
 .../MerchantRequest.kt}                            |   6 +-
 .../taler/merchantpos/order/CategoriesFragment.kt  |   8 +-
 .../net/taler/merchantpos/order/OrderFragment.kt   |  10 +-
 .../order/{OrderViewModel.kt => OrderManager.kt}   |  54 ++----
 .../taler/merchantpos/order/OrderStateFragment.kt  |   8 +-
 .../taler/merchantpos/order/ProductsFragment.kt    |   8 +-
 .../main/res/layout/fragment_merchant_settings.xml | 187 +++++++++++----------
 app/src/main/res/layout/fragment_order.xml         |  12 +-
 app/src/main/res/navigation/nav_graph.xml          |   2 +-
 app/src/main/res/values/colors.xml                 |   1 +
 app/src/main/res/values/strings.xml                |  11 ++
 21 files changed, 492 insertions(+), 325 deletions(-)

diff --git a/app/src/main/java/net/taler/merchantpos/CreatePayment.kt 
b/app/src/main/java/net/taler/merchantpos/CreatePayment.kt
index f92bac7..02a2ae7 100644
--- a/app/src/main/java/net/taler/merchantpos/CreatePayment.kt
+++ b/app/src/main/java/net/taler/merchantpos/CreatePayment.kt
@@ -1,6 +1,5 @@
 package net.taler.merchantpos
 
-import android.annotation.SuppressLint
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
@@ -17,6 +16,7 @@ import com.android.volley.Response
 import com.android.volley.VolleyError
 import com.android.volley.toolbox.Volley
 import com.google.android.material.snackbar.Snackbar
+import net.taler.merchantpos.config.MerchantRequest
 import org.json.JSONObject
 
 
@@ -25,7 +25,7 @@ import org.json.JSONObject
  */
 class CreatePayment : Fragment() {
     private lateinit var queue: RequestQueue
-    private val model: PosTerminalViewModel by activityViewModels()
+    private val model: MainViewModel by activityViewModels()
 
     private var paused: Boolean = false
 
@@ -40,7 +40,6 @@ class CreatePayment : Fragment() {
         this.paused = false
 
         val textView = 
view!!.findViewById<TextView>(R.id.text_create_payment_amount_label)
-        @SuppressLint("SetTextI18n")
         textView.text = "Amount (${model.merchantConfig!!.currency})"
     }
 
@@ -65,7 +64,7 @@ class CreatePayment : Fragment() {
 
         val reqBody = JSONObject().also { it.put("order", order) }
 
-        val req = MerchantInternalRequest(
+        val req = MerchantRequest(
             Request.Method.POST,
             model.merchantConfig!!,
             "order",
@@ -88,13 +87,14 @@ class CreatePayment : Fragment() {
         val params = mapOf("order_id" to orderId, "instance" to 
merchantConfig.instance)
         model.activeOrderId = orderId
 
-        val req = MerchantInternalRequest(Request.Method.GET,
-            model.merchantConfig!!,
-            "check-payment",
-            params,
-            null,
-            Response.Listener { onCheckPayment(it) },
-            Response.ErrorListener { onNetworkError(it) })
+        val req =
+            MerchantRequest(Request.Method.GET,
+                model.merchantConfig!!,
+                "check-payment",
+                params,
+                null,
+                Response.Listener { onCheckPayment(it) },
+                Response.ErrorListener { onNetworkError(it) })
         queue.add(req)
     }
 
diff --git a/app/src/main/java/net/taler/merchantpos/MainActivity.kt 
b/app/src/main/java/net/taler/merchantpos/MainActivity.kt
index 8cc2788..6d2e614 100644
--- a/app/src/main/java/net/taler/merchantpos/MainActivity.kt
+++ b/app/src/main/java/net/taler/merchantpos/MainActivity.kt
@@ -1,6 +1,5 @@
 package net.taler.merchantpos
 
-import android.content.Context
 import android.nfc.NfcAdapter
 import android.nfc.Tag
 import android.nfc.tech.IsoDep
@@ -143,7 +142,7 @@ class MainActivity : AppCompatActivity(), 
NavigationView.OnNavigationItemSelecte
         const val TAG = "taler-merchant"
     }
 
-    private val model: PosTerminalViewModel by viewModels()
+    private val model: MainViewModel by viewModels()
     private var nfcAdapter: NfcAdapter? = null
 
     private var currentTag: IsoDep? = null
@@ -273,19 +272,7 @@ class MainActivity : AppCompatActivity(), 
NavigationView.OnNavigationItemSelecte
                     R.id.merchantHistory
                 ), drawerLayout
             )
-
-        findViewById<Toolbar>(R.id.toolbar)
-            .setupWithNavController(navController, appBarConfiguration)
-
-        val prefs = getSharedPreferences("taler-merchant-terminal", 
Context.MODE_PRIVATE)
-
-        val baseUrl = prefs.getString("merchantBackendUrl", 
"https://backend.test.taler.net";)
-        val instance = prefs.getString("merchantBackendInstance", "default")
-        val apiKey = prefs.getString("merchantBackendApiKey", "sandbox")
-        val currency = prefs.getString("merchantBackendCurrency", "TESTKUDOS")
-
-        model.merchantConfig =
-            MerchantConfig(baseUrl!!, instance!!, apiKey!!, currency!!)
+        toolbar.setupWithNavController(navController, appBarConfiguration)
     }
 
     override fun onBackPressed() {
diff --git a/app/src/main/java/net/taler/merchantpos/MainViewModel.kt 
b/app/src/main/java/net/taler/merchantpos/MainViewModel.kt
new file mode 100644
index 0000000..c202f5f
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/MainViewModel.kt
@@ -0,0 +1,50 @@
+package net.taler.merchantpos
+
+import android.app.Application
+import android.text.Editable
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.android.volley.toolbox.Volley
+import 
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import net.taler.merchantpos.config.ConfigManager
+import net.taler.merchantpos.order.OrderManager
+
+class MainViewModel(app: Application) : AndroidViewModel(app) {
+
+    private val mapper = ObjectMapper()
+        .registerModule(KotlinModule())
+        .configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
+    private val queue = Volley.newRequestQueue(app)
+
+    val orderManager = OrderManager(mapper)
+    val configManager = ConfigManager(app, viewModelScope, mapper, 
queue).apply {
+        addConfigurationReceiver(orderManager)
+    }
+
+    val merchantConfig
+        get() = configManager.merchantConfig
+
+    var activeSubject: Editable? = null
+    var activeOrderId: String? = null
+    var activeAmount: String? = null
+    var activeTalerPayUri: String? = null
+
+    init {
+        if (configManager.merchantConfig == null) {
+            configManager.fetchConfig(configManager.config, false)
+        }
+    }
+
+    override fun onCleared() {
+        queue.cancelAll { !it.isCanceled }
+    }
+
+    fun activeAmountPretty(): String? {
+        val amount = activeAmount ?: return null
+        val components = amount.split(":")
+        return "${components[1]} ${components[0]}"
+    }
+
+}
diff --git a/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt 
b/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt
index c389c5f..167bb9d 100644
--- a/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt
+++ b/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt
@@ -21,6 +21,7 @@ import com.android.volley.Response
 import com.android.volley.VolleyError
 import com.android.volley.toolbox.Volley
 import com.google.android.material.snackbar.Snackbar
+import net.taler.merchantpos.config.MerchantRequest
 import org.json.JSONObject
 import java.time.Instant
 import java.time.ZoneId
@@ -87,7 +88,7 @@ fun parseTalerTimestamp(s: String): Instant {
  */
 class MerchantHistory : Fragment() {
     private lateinit var queue: RequestQueue
-    private val model: PosTerminalViewModel by activityViewModels()
+    private val model: MainViewModel by activityViewModels()
     private val historyListAdapter = MyAdapter(listOf())
 
     private val isLoading = MutableLiveData<Boolean>().apply { value = false }
@@ -125,7 +126,7 @@ class MerchantHistory : Fragment() {
     private fun fetchHistory() {
         isLoading.value = true
         val instance = model.merchantConfig!!.instance
-        val req = MerchantInternalRequest(
+        val req = MerchantRequest(
             Request.Method.GET,
             model.merchantConfig!!,
             "history",
diff --git a/app/src/main/java/net/taler/merchantpos/MerchantSettings.kt 
b/app/src/main/java/net/taler/merchantpos/MerchantSettings.kt
deleted file mode 100644
index a8f6aa5..0000000
--- a/app/src/main/java/net/taler/merchantpos/MerchantSettings.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-package net.taler.merchantpos
-
-import android.content.Context
-import android.os.Bundle
-import androidx.fragment.app.Fragment
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Button
-import android.widget.EditText
-import android.widget.TextView
-import androidx.lifecycle.ViewModelProviders
-import com.android.volley.Request
-import com.android.volley.RequestQueue
-import com.android.volley.Response
-import com.android.volley.VolleyError
-import com.android.volley.toolbox.Volley
-import com.google.android.material.snackbar.Snackbar
-import org.json.JSONObject
-
-
-/**
- * Fragment that displays merchant settings.
- */
-class MerchantSettings : Fragment() {
-
-    private lateinit var queue: RequestQueue
-    private lateinit var model: PosTerminalViewModel
-
-    private var newConfig: MerchantConfig? = null
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        model = activity?.run {
-            ViewModelProviders.of(this)[PosTerminalViewModel::class.java]
-        } ?: throw Exception("Invalid Activity")
-
-        queue = Volley.newRequestQueue(context)
-    }
-
-    private fun reset(view: View) {
-        val backendUrlEdit = 
view.findViewById<EditText>(R.id.edit_settings_backend_url)
-        backendUrlEdit.setText(model.merchantConfig!!.baseUrl, 
TextView.BufferType.EDITABLE)
-
-        val backendInstanceEdit = 
view.findViewById<EditText>(R.id.edit_settings_instance)
-        backendInstanceEdit.setText(model.merchantConfig!!.instance, 
TextView.BufferType.EDITABLE)
-
-        val backendApiKeyEdit = 
view.findViewById<EditText>(R.id.edit_settings_apikey)
-        backendApiKeyEdit.setText(model.merchantConfig!!.apiKey, 
TextView.BufferType.EDITABLE)
-
-        val currencyView = 
view.findViewById<TextView>(R.id.text_settings_currency)
-        currencyView.text = model.merchantConfig!!.currency
-    }
-
-
-    override fun onCreateView(
-        inflater: LayoutInflater, container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View? {
-        val view = inflater.inflate(R.layout.fragment_merchant_settings, 
container, false)
-
-        reset(view)
-
-        val buttonApply = view.findViewById<Button>(R.id.button_settings_apply)
-        buttonApply.setOnClickListener {
-
-            val backendUrlEdit = 
view.findViewById<EditText>(R.id.edit_settings_backend_url)
-            val backendInstanceEdit = 
view.findViewById<EditText>(R.id.edit_settings_instance)
-            val backendApiKeyEdit = 
view.findViewById<EditText>(R.id.edit_settings_apikey)
-
-            val config = MerchantConfig(
-                backendUrlEdit.text.toString(),
-                backendInstanceEdit.text.toString(),
-                backendApiKeyEdit.text.toString(),
-                "UNKNOWN"
-            )
-
-            newConfig = config
-
-            val req = MerchantInternalRequest(
-                Request.Method.GET,
-                config,
-                "config",
-                mapOf("instance" to config.instance),
-                null,
-                Response.Listener { onConfigReceived(it) },
-                Response.ErrorListener { onNetworkError(it) })
-
-            queue.add(req)
-
-        }
-
-        val buttonReset = view.findViewById<Button>(R.id.button_settings_reset)
-        buttonReset.setOnClickListener {
-            reset(view)
-        }
-
-        return view
-    }
-
-    private fun onConfigReceived(it: JSONObject) {
-        val currency = it.getString("currency")
-        val mySnackbar =
-            Snackbar.make(view!!, "Changed to new ${currency} merchant", 
Snackbar.LENGTH_SHORT)
-
-        val config = this.newConfig!!.copy(currency = currency)
-        this.newConfig = null
-        model.merchantConfig = config
-
-        val currencyView = 
view!!.findViewById<TextView>(R.id.text_settings_currency)
-        currencyView.text = currency
-
-        mySnackbar.show()
-
-        val prefs = activity!!.getSharedPreferences("taler-merchant-terminal", 
Context.MODE_PRIVATE)
-        prefs.edit().putString("merchantBackendUrl", config.baseUrl)
-            .putString("merchantBackendInstance", config.instance)
-            .putString("merchantBackendApiKey", config.apiKey)
-            .putString("merchantBackendCurrency", config.currency).apply()
-    }
-
-    private fun onNetworkError(it: VolleyError) {
-        val mySnackbar =
-            Snackbar.make(view!!, "Error: Invalid Configuration", 
Snackbar.LENGTH_SHORT)
-        mySnackbar.show()
-    }
-}
diff --git a/app/src/main/java/net/taler/merchantpos/PosTerminalViewModel.kt 
b/app/src/main/java/net/taler/merchantpos/PosTerminalViewModel.kt
deleted file mode 100644
index a6548e4..0000000
--- a/app/src/main/java/net/taler/merchantpos/PosTerminalViewModel.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package net.taler.merchantpos
-
-import android.text.Editable
-import androidx.lifecycle.ViewModel
-
-class PosTerminalViewModel : ViewModel() {
-    var activeSubject: Editable? = null
-    var merchantConfig: MerchantConfig? = null
-    var activeOrderId: String? = null
-    var activeAmount: String? = null
-    var activeTalerPayUri: String? = null
-
-    fun activeAmountPretty(): String? {
-        val amount = activeAmount ?: return null
-        val components = amount.split(":")
-        return "${components[1]} ${components[0]}"
-    }
-}
diff --git a/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt 
b/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt
index d78d873..d556b6f 100644
--- a/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt
+++ b/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt
@@ -22,6 +22,7 @@ import com.google.android.material.snackbar.Snackbar
 import com.google.zxing.BarcodeFormat
 import com.google.zxing.common.BitMatrix
 import com.google.zxing.qrcode.QRCodeWriter
+import net.taler.merchantpos.config.MerchantRequest
 import org.json.JSONObject
 
 
@@ -38,7 +39,7 @@ class ProcessPayment : Fragment() {
 
     private var paused: Boolean = true
     private lateinit var queue: RequestQueue
-    private val model: PosTerminalViewModel by activityViewModels()
+    private val model: MainViewModel by activityViewModels()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -69,8 +70,14 @@ class ProcessPayment : Fragment() {
         }
         //Log.v("taler-merchant", "checkig if payment happened")
         val params = mapOf("order_id" to model.activeOrderId!!, "instance" to 
model.merchantConfig!!.instance)
-        var req = MerchantInternalRequest(Request.Method.GET, 
model.merchantConfig!!, "check-payment", params, null,
-            Response.Listener { onCheckPayment(it) }, Response.ErrorListener { 
onNetworkError(it) })
+        var req =
+            MerchantRequest(Request.Method.GET,
+                model.merchantConfig!!,
+                "check-payment",
+                params,
+                null,
+                Response.Listener { onCheckPayment(it) },
+                Response.ErrorListener { onNetworkError(it) })
         queue.add(req)
         val handler = Handler()
         handler.postDelayed({
diff --git a/app/src/main/java/net/taler/merchantpos/config/ConfigManager.kt 
b/app/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
new file mode 100644
index 0000000..f6d1d30
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
@@ -0,0 +1,141 @@
+package net.taler.merchantpos.config
+
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+import android.util.Base64.NO_WRAP
+import android.util.Base64.encodeToString
+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.RequestQueue
+import com.android.volley.Response.ErrorListener
+import com.android.volley.Response.Listener
+import com.android.volley.VolleyError
+import com.android.volley.toolbox.JsonObjectRequest
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.json.JSONObject
+
+private const val SETTINGS_NAME = "taler-merchant-terminal"
+
+private const val SETTINGS_CONFIG_URL = "configUrl"
+private const val SETTINGS_USERNAME = "username"
+private const val SETTINGS_PASSWORD = "password"
+
+private val TAG = ConfigManager::class.java.simpleName
+
+interface ConfigurationReceiver {
+    /**
+     * Returns true if the configuration was valid, false otherwise.
+     */
+    suspend fun onConfigurationReceived(json: JSONObject): Boolean
+}
+
+class ConfigManager(
+    context: Context,
+    private val scope: CoroutineScope,
+    private val mapper: ObjectMapper,
+    private val queue: RequestQueue
+) {
+
+    private val prefs = context.getSharedPreferences(SETTINGS_NAME, 
MODE_PRIVATE)
+    private val configurationReceivers = ArrayList<ConfigurationReceiver>()
+
+    var config = Config(
+        configUrl = prefs.getString(SETTINGS_CONFIG_URL, "")!!,
+        username = prefs.getString(SETTINGS_USERNAME, "")!!,
+        password = prefs.getString(SETTINGS_PASSWORD, "")!!
+    )
+    var merchantConfig: MerchantConfig? = null
+
+    private val mConfigUpdateResult = MutableLiveData<ConfigUpdateResult>()
+    val configUpdateResult: LiveData<ConfigUpdateResult> = mConfigUpdateResult
+
+    fun addConfigurationReceiver(receiver: ConfigurationReceiver) {
+        configurationReceivers.add(receiver)
+    }
+
+    @UiThread
+    fun fetchConfig(config: Config, save: Boolean) {
+        mConfigUpdateResult.value = null
+        val configToSave = if (save) config else null
+
+        val stringRequest = object : JsonObjectRequest(GET, config.configUrl, 
null,
+            Listener { onConfigReceived(it, configToSave) },
+            ErrorListener { onNetworkError(it) }
+        ) {
+            // send basic auth header
+            override fun getHeaders(): MutableMap<String, String> {
+                val credentials = "${config.username}:${config.password}"
+                val auth = ("Basic ${encodeToString(credentials.toByteArray(), 
NO_WRAP)}")
+                return mutableMapOf("Authorization" to auth)
+            }
+        }
+        queue.add(stringRequest)
+    }
+
+    private fun onConfigReceived(json: JSONObject, config: Config?) {
+        val merchantConfig: MerchantConfig = try {
+            mapper.readValue(json.getString("config"))
+        } catch (e: Exception) {
+            Log.e(TAG, "Error parsing merchant config", e)
+            mConfigUpdateResult.value = ConfigUpdateResult(null)
+            return
+        }
+        this.merchantConfig = merchantConfig
+
+        val params = mapOf("instance" to merchantConfig.instance)
+        val req = MerchantRequest(GET, merchantConfig, "config", params, null,
+            Listener { onMerchantConfigReceived(config, json, it) },
+            ErrorListener { onNetworkError(it) }
+        )
+        queue.add(req)
+    }
+
+    private fun onMerchantConfigReceived(
+        newConfig: Config?,
+        configJson: JSONObject,
+        json: JSONObject
+    ) = scope.launch(Dispatchers.Main) {
+        val currency = json.getString("currency")
+
+        var configValid = true
+        configurationReceivers.forEach {
+            configValid = configValid or it.onConfigurationReceived(configJson)
+        }
+        if (configValid) {
+            newConfig?.let {
+                config = it
+                saveConfig(it)
+            }
+            Log.e("TEST", "set currency to $currency")
+            merchantConfig = merchantConfig!!.copy(currency = currency)
+            mConfigUpdateResult.value = ConfigUpdateResult(currency)
+        } else {
+            mConfigUpdateResult.value = ConfigUpdateResult(null)
+        }
+    }
+
+    private fun saveConfig(config: Config) {
+        prefs.edit()
+            .putString(SETTINGS_CONFIG_URL, config.configUrl)
+            .putString(SETTINGS_USERNAME, config.username)
+            .putString(SETTINGS_PASSWORD, config.password)
+            .apply()
+    }
+
+    private fun onNetworkError(it: VolleyError) {
+        val authError = it.networkResponse.statusCode == 401
+        mConfigUpdateResult.value = ConfigUpdateResult(null, authError)
+    }
+
+}
+
+class ConfigUpdateResult(val currency: String?, val authError: Boolean = 
false) {
+    val error: Boolean = currency == null
+}
diff --git a/app/src/main/java/net/taler/merchantpos/MerchantConfig.kt 
b/app/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
similarity index 61%
rename from app/src/main/java/net/taler/merchantpos/MerchantConfig.kt
rename to app/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
index 626d60b..63dd487 100644
--- a/app/src/main/java/net/taler/merchantpos/MerchantConfig.kt
+++ b/app/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
@@ -1,12 +1,21 @@
-package net.taler.merchantpos
+package net.taler.merchantpos.config
 
 import android.net.Uri
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class Config(
+    val configUrl: String,
+    val username: String,
+    val password: String
+)
 
 data class MerchantConfig(
+    @JsonProperty("base_url")
     val baseUrl: String,
     val instance: String,
+    @JsonProperty("api_key")
     val apiKey: String,
-    val currency: String
+    val currency: String?
 ) {
     fun urlFor(endpoint: String, params: Map<String, String>?): String {
         val uriBuilder = Uri.parse(baseUrl).buildUpon()
@@ -16,4 +25,4 @@ data class MerchantConfig(
         }
         return uriBuilder.toString()
     }
-}
\ No newline at end of file
+}
diff --git 
a/app/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt 
b/app/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
new file mode 100644
index 0000000..b824d38
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
@@ -0,0 +1,101 @@
+package net.taler.merchantpos.config
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.*
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
+import kotlinx.android.synthetic.main.fragment_merchant_settings.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+
+/**
+ * Fragment that displays merchant settings.
+ */
+class MerchantConfigFragment : Fragment() {
+
+    private val model: MainViewModel by activityViewModels()
+    private val configManager by lazy { model.configManager }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_merchant_settings, 
container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        okButton.setOnClickListener {
+            if (!checkInput()) return@setOnClickListener
+            configUrlView.error = null
+            progressBar.visibility = VISIBLE
+            okButton.visibility = INVISIBLE
+            val config = Config(
+                configUrl = configUrlView.editText!!.text.toString(),
+                username = usernameView.editText!!.text.toString(),
+                password = passwordView.editText!!.text.toString()
+            )
+            configManager.fetchConfig(config, true)
+            configManager.configUpdateResult.observe(viewLifecycleOwner, 
Observer { result ->
+                when {
+                    result == null -> return@Observer
+                    result.error -> onNetworkError(result.authError)
+                    else -> onConfigReceived(result.currency!!)
+                }
+                
configManager.configUpdateResult.removeObservers(viewLifecycleOwner)
+            })
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+        updateView()
+    }
+
+    private fun updateView() {
+        configUrlView.editText!!.setText(configManager.config.configUrl)
+        usernameView.editText!!.setText(configManager.config.username)
+        passwordView.editText!!.setText(configManager.config.password)
+
+        val currency = configManager.merchantConfig?.currency
+        if (currency == null) {
+            currencyView.visibility = GONE
+        } else {
+            currencyView.text = getString(R.string.config_currency, currency)
+            currencyView.visibility = VISIBLE
+        }
+    }
+
+    private fun checkInput(): Boolean {
+        return if (configUrlView.editText!!.text.startsWith("https://";)) {
+            true
+        } else {
+            configUrlView.error = getString(R.string.config_malformed_url)
+            false
+        }
+    }
+
+    private fun onConfigReceived(currency: String) {
+        onResultReceived()
+        updateView()
+        Snackbar.make(view!!, "Changed to new $currency merchant", 
LENGTH_SHORT).show()
+    }
+
+    private fun onNetworkError(authError: Boolean) {
+        onResultReceived()
+        val res = if (authError) R.string.config_auth_error else 
R.string.config_error
+        Snackbar.make(view!!, res, LENGTH_SHORT).show()
+    }
+
+    private fun onResultReceived() {
+        progressBar.visibility = INVISIBLE
+        okButton.visibility = VISIBLE
+    }
+
+}
diff --git a/app/src/main/java/net/taler/merchantpos/MerchantInternalRequest.kt 
b/app/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt
similarity index 92%
rename from app/src/main/java/net/taler/merchantpos/MerchantInternalRequest.kt
rename to app/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt
index b5ab98e..e6b96cd 100644
--- a/app/src/main/java/net/taler/merchantpos/MerchantInternalRequest.kt
+++ b/app/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt
@@ -1,4 +1,4 @@
-package net.taler.merchantpos
+package net.taler.merchantpos.config
 
 
 import android.util.ArrayMap
@@ -6,7 +6,7 @@ import com.android.volley.Response
 import com.android.volley.toolbox.JsonObjectRequest
 import org.json.JSONObject
 
-class MerchantInternalRequest(
+class MerchantRequest(
     method: Int,
     private val merchantConfig: MerchantConfig,
     endpoint: String,
@@ -22,4 +22,4 @@ class MerchantInternalRequest(
         headerMap["Authorization"] = "ApiKey " + merchantConfig.apiKey
         return headerMap
     }
-}
\ No newline at end of file
+}
diff --git 
a/app/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt 
b/app/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
index 9d1ac5e..148699c 100644
--- a/app/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
+++ b/app/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
@@ -13,6 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 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.order.CategoryAdapter.CategoryViewHolder
 
@@ -22,7 +23,8 @@ interface CategorySelectionListener {
 
 class CategoriesFragment : Fragment(), CategorySelectionListener {
 
-    private val viewModel: OrderViewModel by activityViewModels()
+    private val viewModel: MainViewModel by activityViewModels()
+    private val orderManager by lazy { viewModel.orderManager }
     private val adapter = CategoryAdapter(this)
 
     override fun onCreateView(
@@ -39,14 +41,14 @@ class CategoriesFragment : Fragment(), 
CategorySelectionListener {
             layoutManager = LinearLayoutManager(requireContext())
         }
 
-        viewModel.categories.observe(viewLifecycleOwner, Observer { categories 
->
+        orderManager.categories.observe(viewLifecycleOwner, Observer { 
categories ->
             adapter.setItems(categories)
             progressBar.visibility = INVISIBLE
         })
     }
 
     override fun onCategorySelected(category: Category) {
-        viewModel.setCurrentCategory(category)
+        orderManager.setCurrentCategory(category)
     }
 
 }
diff --git a/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt 
b/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
index 3743281..1cb89ba 100644
--- a/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
+++ b/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
@@ -9,12 +9,13 @@ import androidx.fragment.app.activityViewModels
 import androidx.navigation.NavController
 import androidx.navigation.Navigation.findNavController
 import kotlinx.android.synthetic.main.fragment_order.*
+import net.taler.merchantpos.MainViewModel
 import net.taler.merchantpos.R
 
 class OrderFragment : Fragment() {
 
-    private val viewModel: OrderViewModel by activityViewModels()
-
+    private val viewModel: MainViewModel by activityViewModels()
+    private val orderManager by lazy { viewModel.orderManager }
 
     override fun onCreateView(
         inflater: LayoutInflater,
@@ -25,13 +26,16 @@ class OrderFragment : Fragment() {
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        restartButton.setOnClickListener { viewModel.restart() }
+        // TODO build undo-feature that allows to undo a restart and bring 
back old order
+        restartButton.setOnClickListener { orderManager.restart() }
     }
 
     override fun onActivityCreated(savedInstanceState: Bundle?) {
         super.onActivityCreated(savedInstanceState)
         val nav: NavController = findNavController(requireActivity(), 
R.id.nav_host_fragment)
+        reconfigureButton.setOnClickListener { 
nav.navigate(R.id.action_global_merchantSettings) }
         historyButton.setOnClickListener { 
nav.navigate(R.id.action_global_merchantHistory) }
+        logoutButton.setOnClickListener { 
nav.navigate(R.id.action_global_merchantSettings) }
     }
 
 }
diff --git a/app/src/main/java/net/taler/merchantpos/order/OrderViewModel.kt 
b/app/src/main/java/net/taler/merchantpos/order/OrderManager.kt
similarity index 68%
rename from app/src/main/java/net/taler/merchantpos/order/OrderViewModel.kt
rename to app/src/main/java/net/taler/merchantpos/order/OrderManager.kt
index 02ee33f..e7928c7 100644
--- a/app/src/main/java/net/taler/merchantpos/order/OrderViewModel.kt
+++ b/app/src/main/java/net/taler/merchantpos/order/OrderManager.kt
@@ -1,38 +1,21 @@
 package net.taler.merchantpos.order
 
-import android.app.Application
 import android.util.Log
 import androidx.annotation.UiThread
-import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Transformations.map
-import androidx.lifecycle.viewModelScope
-import com.android.volley.Request.Method.GET
-import com.android.volley.Response.ErrorListener
-import com.android.volley.Response.Listener
-import com.android.volley.toolbox.JsonObjectRequest
-import com.android.volley.toolbox.Volley
 import com.fasterxml.jackson.core.type.TypeReference
-import 
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
 import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.KotlinModule
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
+import net.taler.merchantpos.config.ConfigurationReceiver
 import org.json.JSONObject
 
-class OrderViewModel(app: Application) : AndroidViewModel(app) {
+class OrderManager(private val mapper: ObjectMapper) : ConfigurationReceiver {
 
     companion object {
-        val TAG = OrderViewModel::class.java.simpleName
+        val TAG = OrderManager::class.java.simpleName
     }
 
-    private val url = "https://grobox.de/taler/products.json";
-    private val queue = Volley.newRequestQueue(app)
-    private val mapper = ObjectMapper()
-        .registerModule(KotlinModule())
-        .configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
-
     private val productsByCategory = HashMap<Category, ArrayList<Product>>()
 
     private val mOrder = MutableLiveData<HashMap<Product, Int>>()
@@ -45,23 +28,17 @@ class OrderViewModel(app: Application) : 
AndroidViewModel(app) {
     private val mCategories = MutableLiveData<List<Category>>()
     internal val categories: LiveData<List<Category>> = mCategories
 
-    init {
-        val stringRequest = JsonObjectRequest(GET, url, null,
-            Listener { response -> onConfigurationReceived(response) },
-            ErrorListener { onConfigurationError() }
-        )
-        queue.add(stringRequest)
-    }
-
-    override fun onCleared() {
-        queue.cancelAll { !it.isCanceled }
-    }
-
-    private fun onConfigurationReceived(json: JSONObject) = 
viewModelScope.launch(Dispatchers.IO) {
+    override suspend fun onConfigurationReceived(json: JSONObject): Boolean {
         // parse categories
         val categoriesStr = json.getJSONArray("categories").toString()
         val categoriesType = object : TypeReference<List<Category>>() {}
         val categories: List<Category> = mapper.readValue(categoriesStr, 
categoriesType)
+        if (categories.isEmpty()) {
+            Log.e(TAG, "No valid category found.")
+            return false
+        }
+        // pre-select the first category
+        categories[0].selected = true
         mCategories.postValue(categories)
 
         // parse products (live data gets updated in setCurrentCategory())
@@ -77,7 +54,7 @@ class OrderViewModel(app: Application) : 
AndroidViewModel(app) {
                 if (category == null) {
                     Log.e(TAG, "Product $product has unknown category 
$categoryId")
                     onConfigurationError()
-                    return@launch
+                    return false
                 }
                 if (productsByCategory.containsKey(category)) {
                     productsByCategory[category]?.add(product)
@@ -86,9 +63,12 @@ class OrderViewModel(app: Application) : 
AndroidViewModel(app) {
                 }
             }
         }
-        // pre-select the first category
-        if (productsByCategory.size > 0) setCurrentCategory(categories[0])
-        else onConfigurationError()
+        return if (productsByCategory.size > 0) {
+            mProducts.postValue(productsByCategory[categories[0]])
+            true
+        } else {
+            false
+        }
     }
 
     private fun onConfigurationError() {
diff --git 
a/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt 
b/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
index 928b688..b473b5d 100644
--- a/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
+++ b/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
@@ -11,12 +11,14 @@ import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import kotlinx.android.synthetic.main.fragment_order_state.*
+import net.taler.merchantpos.MainViewModel
 import net.taler.merchantpos.R
 import net.taler.merchantpos.order.OrderAdapter.OrderViewHolder
 
 class OrderStateFragment : Fragment() {
 
-    private val viewModel: OrderViewModel by activityViewModels()
+    private val viewModel: MainViewModel by activityViewModels()
+    private val orderManager by lazy { viewModel.orderManager }
     private val adapter = OrderAdapter()
 
     override fun onCreateView(
@@ -33,10 +35,10 @@ class OrderStateFragment : Fragment() {
             layoutManager = LinearLayoutManager(requireContext())
         }
 
-        viewModel.order.observe(viewLifecycleOwner, Observer { order ->
+        orderManager.order.observe(viewLifecycleOwner, Observer { order ->
             adapter.setItems(order)
         })
-        viewModel.orderTotal.observe(viewLifecycleOwner, Observer { orderTotal 
->
+        orderManager.orderTotal.observe(viewLifecycleOwner, Observer { 
orderTotal ->
             if (orderTotal == 0.0) {
                 totalView.text = null
             } else {
diff --git a/app/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt 
b/app/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
index 2a028c0..0fef4bd 100644
--- a/app/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
+++ b/app/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
@@ -13,6 +13,7 @@ import androidx.recyclerview.widget.GridLayoutManager
 import androidx.recyclerview.widget.RecyclerView.Adapter
 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.order.ProductAdapter.ProductViewHolder
 
@@ -22,7 +23,8 @@ interface ProductSelectionListener {
 
 class ProductsFragment : Fragment(), ProductSelectionListener {
 
-    private val viewModel: OrderViewModel by activityViewModels()
+    private val viewModel: MainViewModel by activityViewModels()
+    private val orderManager by lazy { viewModel.orderManager }
     private val adapter = ProductAdapter(this)
 
     override fun onCreateView(
@@ -39,7 +41,7 @@ class ProductsFragment : Fragment(), ProductSelectionListener 
{
             layoutManager = GridLayoutManager(requireContext(), 3)
         }
 
-        viewModel.products.observe(viewLifecycleOwner, Observer { products ->
+        orderManager.products.observe(viewLifecycleOwner, Observer { products 
->
             if (products == null) {
                 adapter.setItems(emptyList())
             } else {
@@ -50,7 +52,7 @@ class ProductsFragment : Fragment(), ProductSelectionListener 
{
     }
 
     override fun onProductSelected(product: Product) {
-        viewModel.addProduct(product)
+        orderManager.addProduct(product)
     }
 
 }
diff --git a/app/src/main/res/layout/fragment_merchant_settings.xml 
b/app/src/main/res/layout/fragment_merchant_settings.xml
index b6e3707..6f1bcc9 100644
--- a/app/src/main/res/layout/fragment_merchant_settings.xml
+++ b/app/src/main/res/layout/fragment_merchant_settings.xml
@@ -1,108 +1,113 @@
 <?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android";
-             xmlns:tools="http://schemas.android.com/tools";
-             android:layout_width="match_parent"
-             android:layout_height="match_parent"
-             android:layout_margin="15dp"
-             tools:context=".MerchantSettings">
-
-
-    <LinearLayout
-            android:orientation="vertical"
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android";
+        xmlns:app="http://schemas.android.com/apk/res-auto";
+        xmlns:tools="http://schemas.android.com/tools";
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:fillViewport="true">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
             android:layout_width="match_parent"
-            android:layout_height="match_parent">
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp"
+            tools:context=".config.MerchantConfigFragment">
 
-        <TextView
-                android:layout_width="match_parent"
+        <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/configUrlView"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
-                android:text="Merchant Backend Base URL" />
-
-        <EditText
-                android:id="@+id/edit_settings_backend_url"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:ems="10"
-                android:inputType="text"
-                android:text="Name" />
-
-        <Space
-                android:layout_width="match_parent"
-                android:layout_height="40dp" />
+                android:layout_margin="16dp"
+                android:hint="@string/config_url"
+                app:boxBackgroundColor="@android:color/transparent"
+                app:boxBackgroundMode="outline"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent">
+
+            <com.google.android.material.textfield.TextInputEditText
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:inputType="textUri" />
 
-        <TextView
-                android:id="@+id/textView4"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:text="Merchant Instance" />
+        </com.google.android.material.textfield.TextInputLayout>
 
-        <EditText
-                android:id="@+id/edit_settings_instance"
-                android:layout_width="match_parent"
+        <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/usernameView"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
-                android:ems="10"
-                android:inputType="text"
-                android:text="Name" />
+                android:layout_margin="16dp"
+                android:hint="@string/config_username"
+                app:boxBackgroundColor="@android:color/transparent"
+                app:boxBackgroundMode="outline"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/configUrlView">
+
+            <com.google.android.material.textfield.TextInputEditText
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:inputType="text" />
 
-        <Space
-                android:layout_width="match_parent"
-                android:layout_height="40dp" />
+        </com.google.android.material.textfield.TextInputLayout>
 
-        <TextView
-                android:layout_width="match_parent"
+        <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/passwordView"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
-                android:text="API Key" />
-
-        <EditText
-                android:id="@+id/edit_settings_apikey"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:ems="10"
-                android:inputType="textPassword" />
+                android:layout_margin="16dp"
+                android:hint="@string/config_password"
+                app:boxBackgroundColor="@android:color/transparent"
+                app:boxBackgroundMode="outline"
+                app:endIconMode="password_toggle"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/usernameView">
+
+            <com.google.android.material.textfield.TextInputEditText
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:inputType="textWebPassword" />
 
-        <Space
-                android:layout_width="match_parent"
-                android:layout_height="40dp" />
+        </com.google.android.material.textfield.TextInputLayout>
 
         <TextView
-                android:layout_width="match_parent"
+                android:id="@+id/currencyView"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
-                android:text="Currency" />
-
-        <TextView
-                android:id="@+id/text_settings_currency"
-                android:layout_width="match_parent"
+                android:layout_margin="16dp"
+                android:textSize="18sp"
+                android:visibility="gone"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/passwordView"
+                tools:text="@string/config_currency"
+                tools:visibility="visible" />
+
+        <Button
+                android:id="@+id/okButton"
+                android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:text="TextView"
-                
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
-
-        <Space
-                android:layout_width="match_parent"
-                android:layout_height="0dp"
-                android:layout_weight="1"/>
-
-        <LinearLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
-
-            <Button
-                    android:id="@+id/button_settings_reset"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="bottom|right"
-                    android:text="Reset" />
-
-            <Space
-                    android:layout_width="match_parent"
-                    android:layout_height="0dp"
-                    android:layout_weight="1"/>
+                android:layout_margin="16dp"
+                android:text="@string/config_ok"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintHorizontal_bias="1.0"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/currencyView"
+                app:layout_constraintVertical_bias="1.0" />
+
+        <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/okButton"
+                app:layout_constraintEnd_toEndOf="@+id/okButton"
+                app:layout_constraintStart_toStartOf="@+id/okButton"
+                app:layout_constraintTop_toTopOf="@+id/okButton"
+                tools:visibility="visible" />
 
-            <Button
-                    android:id="@+id/button_settings_apply"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="bottom|right"
-                    android:text="Apply" />
-        </LinearLayout>
-    </LinearLayout>
+    </androidx.constraintlayout.widget.ConstraintLayout>
 
-</FrameLayout>
\ No newline at end of file
+</ScrollView>
diff --git a/app/src/main/res/layout/fragment_order.xml 
b/app/src/main/res/layout/fragment_order.xml
index 462264d..968544e 100644
--- a/app/src/main/res/layout/fragment_order.xml
+++ b/app/src/main/res/layout/fragment_order.xml
@@ -82,6 +82,16 @@
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintStart_toEndOf="@+id/reconfigureButton" />
 
+    <Button
+            android:id="@+id/logoutButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:backgroundTint="@color/logoutButton"
+            android:text="@string/button_logout"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/historyButton" />
+
     <Button
             android:id="@+id/completeButton"
             android:layout_width="wrap_content"
@@ -92,6 +102,6 @@
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintHorizontal_bias="1.0"
-            app:layout_constraintStart_toEndOf="@+id/historyButton" />
+            app:layout_constraintStart_toEndOf="@+id/logoutButton" />
 
 </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 5951e77..4eaadda 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -31,7 +31,7 @@
     <action android:id="@+id/action_global_merchantHistory" 
app:destination="@id/merchantHistory"/>
     <action android:id="@+id/action_global_createPayment" 
app:destination="@id/createPayment"/>
     <action android:id="@+id/action_global_order" app:destination="@id/order"/>
-    <fragment android:id="@+id/merchantSettings" 
android:name="net.taler.merchantpos.MerchantSettings"
+    <fragment android:id="@+id/merchantSettings" 
android:name="net.taler.merchantpos.config.MerchantConfigFragment"
               android:label="Merchant Settings" 
tools:layout="@layout/fragment_merchant_settings"/>
     <action android:id="@+id/action_global_merchantSettings" 
app:destination="@id/merchantSettings"/>
     <fragment
diff --git a/app/src/main/res/values/colors.xml 
b/app/src/main/res/values/colors.xml
index 950c107..10354c5 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -5,4 +5,5 @@
     <color name="colorAccent">#FFEB3B</color>
 
     <color name="bottomButtons">#9E9D24</color>
+    <color name="logoutButton">#C62828</color>
 </resources>
diff --git a/app/src/main/res/values/strings.xml 
b/app/src/main/res/values/strings.xml
index 09c7342..dbfa175 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -21,5 +21,16 @@
     <string name="order_restart">Restart</string>
     <string name="button_reconfigure">Reconfigure</string>
     <string name="button_history">History</string>
+    <string name="button_logout">Logout</string>
     <string name="button_complete">Complete</string>
+
+    <string name="config_url">Configuration URL</string>
+    <string name="config_username">Username</string>
+    <string name="config_password">Password</string>
+    <string name="config_currency">Currency: %s</string>
+    <string name="config_ok">Fetch Configuration</string>
+    <string name="config_malformed_url">Invalid URL</string>
+    <string name="config_auth_error">Invalid username or password</string>
+    <string name="config_error">Error: Invalid Configuration</string>
+
 </resources>

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



reply via email to

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