gnunet-svn
[Top][All Lists]
Advanced

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

[taler-merchant-terminal-android] branch master updated (f463d1b -> 39af


From: gnunet
Subject: [taler-merchant-terminal-android] branch master updated (f463d1b -> 39af919)
Date: Fri, 21 Feb 2020 18:59:53 +0100

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

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

    from f463d1b  actually save settings
     new 8080e4b  Upgrade libraries to latest stable versions
     new 7d299bf  Add screen to process an order
     new dc02246  Fix crash when loading history
     new 77ee9bc  Fetch merchant config from central configuration JSON
     new 1f5c20e  Allow user to undo restarting the order
     new 98125ea  Create payments directly from the order
     new 5be8ea6  Add ordered products to order's contract terms
     new fcce9dd  Use actual taler icon for the app
     new f5740d3  Factor out NFC code from MainActivity
     new 163f131  Require valid configuration before showing UI
     new 94dd96d  Allow user to decide if they want to save password, add 
FORGET option
     new 5441e55  Fix invalid product configuration
     new 0ea377b  Make NFC and QR code re-useable in another app
     new 2cbfec1  Use product categories for order summary
     new 4734bd6  Introduce different product classes for re-use in other taler 
apps
     new 99b760a  Allow editing order with -1 and +1 buttons
     new 2256d2f  Make order sorting deterministic
     new 4f96a05  Don't talk about NFC if it is not supported
     new 39af919  Check for duplicate product IDs

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


Summary of changes:
 .idea/codeStyles/Project.xml                       |   5 +
 .idea/misc.xml                                     |   2 +-
 app/build.gradle                                   |  41 ++-
 app/src/main/AndroidManifest.xml                   |  25 +-
 .../java/net/taler/merchantpos/CreatePayment.kt    | 130 --------
 .../java/net/taler/merchantpos/MainActivity.kt     | 360 ++++-----------------
 .../java/net/taler/merchantpos/MainViewModel.kt    |  37 +++
 .../java/net/taler/merchantpos/MerchantHistory.kt  | 228 +++++++------
 .../java/net/taler/merchantpos/MerchantSettings.kt | 128 --------
 .../main/java/net/taler/merchantpos/NfcManager.kt  | 215 ++++++++++++
 .../java/net/taler/merchantpos/PaymentSuccess.kt   |  29 --
 .../net/taler/merchantpos/PosTerminalViewModel.kt  |  18 --
 .../java/net/taler/merchantpos/ProcessPayment.kt   | 150 ---------
 .../java/net/taler/merchantpos/QrCodeManager.kt    |  26 ++
 app/src/main/java/net/taler/merchantpos/Utils.kt   |  69 ++++
 .../merchantpos/config/ConfigFetcherFragment.kt    |  46 +++
 .../net/taler/merchantpos/config/ConfigManager.kt  | 157 +++++++++
 .../merchantpos/{ => config}/MerchantConfig.kt     |  18 +-
 .../merchantpos/config/MerchantConfigFragment.kt   | 120 +++++++
 .../MerchantRequest.kt}                            |   6 +-
 .../taler/merchantpos/order/CategoriesFragment.kt  |  90 ++++++
 .../net/taler/merchantpos/order/Definitions.kt     | 115 +++++++
 .../net/taler/merchantpos/order/OrderFragment.kt   |  76 +++++
 .../net/taler/merchantpos/order/OrderManager.kt    | 159 +++++++++
 .../taler/merchantpos/order/OrderStateFragment.kt  | 171 ++++++++++
 .../taler/merchantpos/order/ProductsFragment.kt    |  95 ++++++
 .../java/net/taler/merchantpos/payment/Payment.kt  |  13 +
 .../taler/merchantpos/payment/PaymentManager.kt    | 130 ++++++++
 .../merchantpos/payment/PaymentSuccessFragment.kt  |  27 ++
 .../merchantpos/payment/ProcessPaymentFragment.kt  |  67 ++++
 .../main/res/drawable/selectable_background.xml    |   5 +
 app/src/main/res/layout/activity_main.xml          |   4 +-
 app/src/main/res/layout/content_main.xml           |   4 +-
 app/src/main/res/layout/fragment_categories.xml    |  30 ++
 .../main/res/layout/fragment_config_fetcher.xml    |  29 ++
 .../main/res/layout/fragment_create_payment.xml    |  59 ----
 .../main/res/layout/fragment_merchant_settings.xml | 206 +++++++-----
 app/src/main/res/layout/fragment_order.xml         | 130 ++++++++
 app/src/main/res/layout/fragment_order_state.xml   |  31 ++
 .../main/res/layout/fragment_process_payment.xml   |   4 +-
 app/src/main/res/layout/fragment_products.xml      |  28 ++
 app/src/main/res/layout/list_item_category.xml     |  17 +
 app/src/main/res/layout/list_item_order.xml        |  45 +++
 app/src/main/res/layout/list_item_product.xml      |  40 +++
 app/src/main/res/menu/activity_main_drawer.xml     |  12 +-
 app/src/main/res/menu/main.xml                     |   8 -
 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml |   5 -
 .../res/mipmap-anydpi-v26/ic_launcher_round.xml    |   5 -
 app/src/main/res/mipmap-hdpi/ic_launcher.png       | Bin 2963 -> 0 bytes
 app/src/main/res/mipmap-hdpi/ic_launcher_round.png | Bin 4905 -> 0 bytes
 app/src/main/res/mipmap-mdpi/ic_launcher.png       | Bin 2060 -> 0 bytes
 app/src/main/res/mipmap-mdpi/ic_launcher_round.png | Bin 2783 -> 0 bytes
 app/src/main/res/mipmap-xhdpi/ic_launcher.png      | Bin 4490 -> 0 bytes
 .../main/res/mipmap-xhdpi/ic_launcher_round.png    | Bin 6895 -> 0 bytes
 app/src/main/res/mipmap-xxhdpi/ic_launcher.png     | Bin 6387 -> 0 bytes
 .../main/res/mipmap-xxhdpi/ic_launcher_round.png   | Bin 10413 -> 0 bytes
 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png    | Bin 9128 -> 0 bytes
 .../main/res/mipmap-xxxhdpi/ic_launcher_round.png  | Bin 15132 -> 0 bytes
 app/src/main/res/navigation/nav_graph.xml          |  88 +++--
 app/src/main/res/values-night/colors.xml           |   4 +
 app/src/main/res/values-v21/styles.xml             |   7 -
 app/src/main/res/values/colors.xml                 |   4 +
 app/src/main/res/values/strings.xml                |  25 +-
 app/src/main/res/values/styles.xml                 |  11 +-
 build.gradle                                       |   2 +-
 65 files changed, 2444 insertions(+), 1112 deletions(-)
 delete mode 100644 app/src/main/java/net/taler/merchantpos/CreatePayment.kt
 create mode 100644 app/src/main/java/net/taler/merchantpos/MainViewModel.kt
 delete mode 100644 app/src/main/java/net/taler/merchantpos/MerchantSettings.kt
 create mode 100644 app/src/main/java/net/taler/merchantpos/NfcManager.kt
 delete mode 100644 app/src/main/java/net/taler/merchantpos/PaymentSuccess.kt
 delete mode 100644 
app/src/main/java/net/taler/merchantpos/PosTerminalViewModel.kt
 delete mode 100644 app/src/main/java/net/taler/merchantpos/ProcessPayment.kt
 create mode 100644 app/src/main/java/net/taler/merchantpos/QrCodeManager.kt
 create mode 100644 app/src/main/java/net/taler/merchantpos/Utils.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
 rename app/src/main/java/net/taler/merchantpos/{ => config}/MerchantConfig.kt 
(54%)
 create mode 100644 
app/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
 rename app/src/main/java/net/taler/merchantpos/{MerchantInternalRequest.kt => 
config/MerchantRequest.kt} (92%)
 create mode 100644 
app/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
 create mode 100644 app/src/main/java/net/taler/merchantpos/order/Definitions.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/order/OrderManager.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
 create mode 100644 app/src/main/java/net/taler/merchantpos/payment/Payment.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/payment/PaymentSuccessFragment.kt
 create mode 100644 
app/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
 create mode 100644 app/src/main/res/drawable/selectable_background.xml
 create mode 100644 app/src/main/res/layout/fragment_categories.xml
 create mode 100644 app/src/main/res/layout/fragment_config_fetcher.xml
 delete mode 100644 app/src/main/res/layout/fragment_create_payment.xml
 create mode 100644 app/src/main/res/layout/fragment_order.xml
 create mode 100644 app/src/main/res/layout/fragment_order_state.xml
 create mode 100644 app/src/main/res/layout/fragment_products.xml
 create mode 100644 app/src/main/res/layout/list_item_category.xml
 create mode 100644 app/src/main/res/layout/list_item_order.xml
 create mode 100644 app/src/main/res/layout/list_item_product.xml
 delete mode 100644 app/src/main/res/menu/main.xml
 delete mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
 delete mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
 delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png
 delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png
 delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png
 delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png
 delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png
 delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
 delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png
 delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
 delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
 delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
 create mode 100644 app/src/main/res/values-night/colors.xml
 delete mode 100644 app/src/main/res/values-v21/styles.xml

diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index ce889bd..a705caf 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,6 +1,11 @@
 <component name="ProjectCodeStyleConfiguration">
   <code_scheme name="Project" version="173">
+    <AndroidXmlCodeStyleSettings>
+      <option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
+    </AndroidXmlCodeStyleSettings>
     <JetCodeStyleSettings>
+      <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
+      <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" 
value="2147483647" />
       <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
     </JetCodeStyleSettings>
     <codeStyleSettings language="XML">
diff --git a/.idea/misc.xml b/.idea/misc.xml
index b6ea2b1..7bfef59 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" 
project-jdk-name="JDK" project-jdk-type="JavaSDK">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" 
project-jdk-name="1.8" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>
   <component name="ProjectType">
diff --git a/app/build.gradle b/app/build.gradle
index c1fc89b..ca16c9d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -7,10 +7,10 @@ apply plugin: 'kotlin-android-extensions'
 
 android {
     compileSdkVersion 29
-    buildToolsVersion "29.0.1"
+    buildToolsVersion "29.0.2"
     defaultConfig {
         applicationId "net.taler.merchantpos"
-        minSdkVersion 27
+        minSdkVersion 21
         targetSdkVersion 29
         versionCode 1
         versionName "1.0"
@@ -36,36 +36,35 @@ android {
 dependencies {
     implementation fileTree(dir: 'libs', include: ['*.jar'])
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-    implementation 'androidx.appcompat:appcompat:1.0.2'
-    implementation 'androidx.core:core-ktx:1.0.2'
-    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
-    implementation 'com.google.android.material:material:1.1.0-alpha09'
+    implementation 'androidx.appcompat:appcompat:1.1.0'
+    implementation 'androidx.core:core-ktx:1.2.0'
+    implementation 'com.google.android.material:material:1.1.0'
     implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
-    testImplementation 'junit:junit:4.12'
-    androidTestImplementation 'androidx.test:runner:1.2.0'
-    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
-
-    implementation "androidx.recyclerview:recyclerview:1.1.0-beta02"
-
+    implementation "androidx.recyclerview:recyclerview:1.1.0"
+    implementation "androidx.recyclerview:recyclerview-selection:1.1.0-rc01"
 
-    def nav_version = "2.1.0-rc01"
+    // Navigation
+    def nav_version = "2.2.1"
     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'
 
-    // ViewModel and LiveData
-    def lifecycle_version = "2.0.0"
-    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
-    implementation 
"androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
-    kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
-
     implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
 
-    // JSON literals and parsing
-    implementation 'com.beust:klaxon:5.0.11'
+    // JSON parsing and serialization
+    implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2"
+
+    testImplementation 'junit:junit:4.13'
+    androidTestImplementation 'androidx.test:runner:1.2.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
 }
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0920771..95a071f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,27 +1,36 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android";
-          xmlns:tools="http://schemas.android.com/tools"; 
package="net.taler.merchantpos">
+        xmlns:tools="http://schemas.android.com/tools";
+        package="net.taler.merchantpos">
 
     <uses-permission android:name="android.permission.NFC" />
-    <uses-permission android:name="android.permission.INTERNET"/>
-    <uses-feature android:name="android.hardware.nfc"
+    <uses-permission android:name="android.permission.INTERNET" />
+
+    <uses-feature
+            android:name="android.hardware.nfc"
             android:required="true" />
 
+    <uses-feature
+            android:name="android.hardware.telephony"
+            android:required="false" />
+
     <application
             android:allowBackup="true"
-            android:icon="@mipmap/ic_launcher"
+            android:icon="@mipmap/ic_taler_logo"
             android:label="@string/app_name"
-            android:roundIcon="@mipmap/ic_launcher_round"
+            android:roundIcon="@mipmap/ic_taler_logo_round"
             android:supportsRtl="true"
-            android:theme="@style/AppTheme" 
tools:ignore="GoogleAppIndexingWarning">
+            android:theme="@style/AppTheme"
+            tools:ignore="GoogleAppIndexingWarning">
         <activity
                 android:name=".MainActivity"
                 android:label="@string/app_name"
+                android:screenOrientation="landscape"
                 android:theme="@style/AppTheme.NoActionBar">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
+                <action android:name="android.intent.action.MAIN" />
 
-                <category android:name="android.intent.category.LAUNCHER"/>
+                <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
     </application>
diff --git a/app/src/main/java/net/taler/merchantpos/CreatePayment.kt 
b/app/src/main/java/net/taler/merchantpos/CreatePayment.kt
deleted file mode 100644
index 83dbed8..0000000
--- a/app/src/main/java/net/taler/merchantpos/CreatePayment.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-package net.taler.merchantpos
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-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.fragment.app.Fragment
-import androidx.lifecycle.ViewModelProviders
-import androidx.navigation.fragment.findNavController
-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 allows the merchant to create a payment.
- */
-class CreatePayment : Fragment() {
-    private lateinit var queue: RequestQueue
-    private lateinit var model: PosTerminalViewModel
-
-    private var paused: Boolean = false
-
-
-    override fun onPause() {
-        super.onPause()
-        this.paused = true
-    }
-
-    override fun onResume() {
-        super.onResume()
-        this.paused = false
-
-        val textView = 
view!!.findViewById<TextView>(R.id.text_create_payment_amount_label)
-        @SuppressLint("SetTextI18n")
-        textView.text = "Amount (${model.merchantConfig!!.currency})"
-    }
-
-    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 onRequestPayment() {
-        val amountValStr = 
activity!!.findViewById<EditText>(R.id.edit_payment_amount).text
-        val amount = "${model.merchantConfig!!.currency}:${amountValStr}"
-        model.activeAmount = amount
-        model.activeSubject = 
activity!!.findViewById<EditText>(R.id.edit_payment_subject).text
-
-        val order = JSONObject().also {
-            it.put("amount", amount)
-            it.put("summary", model.activeSubject!!)
-            it.put("fulfillment_url", "https://example.com";)
-            it.put("instance", "default")
-        }
-
-        val reqBody = JSONObject().also { it.put("order", order) }
-
-        val req = MerchantInternalRequest(
-            Request.Method.POST,
-            model.merchantConfig!!,
-            "order",
-            null,
-            reqBody,
-            Response.Listener { onOrderCreated(it) },
-            Response.ErrorListener { onNetworkError(it) })
-
-        queue.add(req)
-    }
-
-    private fun onNetworkError(volleyError: VolleyError?) {
-        val mySnackbar = Snackbar.make(view!!, "Network Error", 
Snackbar.LENGTH_SHORT)
-        mySnackbar.show()
-    }
-
-    private fun onOrderCreated(orderResponse: JSONObject) {
-        val merchantConfig = model.merchantConfig!!
-        val orderId = orderResponse.getString("order_id")
-        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) })
-        queue.add(req)
-    }
-
-    /**
-     * Called when the /check-payment response gave a result.
-     */
-    private fun onCheckPayment(checkPaymentResponse: JSONObject) {
-        if (paused) {
-            return
-        }
-        if (checkPaymentResponse.getBoolean("paid")) {
-            val mySnackbar = Snackbar.make(view!!, "Already paid?!", 
Snackbar.LENGTH_SHORT)
-            mySnackbar.show()
-            return
-        }
-        model.activeTalerPayUri = 
checkPaymentResponse.getString("taler_pay_uri")
-        
findNavController().navigate(R.id.action_createPayment_to_processPayment)
-    }
-
-    override fun onCreateView(
-        inflater: LayoutInflater, container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View? {
-        // Inflate the layout for this fragment
-        val view = inflater.inflate(R.layout.fragment_create_payment, 
container, false)
-        val requestPaymentButton = 
view.findViewById<Button>(R.id.button_request_payment);
-        requestPaymentButton.setOnClickListener {
-            onRequestPayment()
-        }
-
-        return view
-    }
-
-}
diff --git a/app/src/main/java/net/taler/merchantpos/MainActivity.kt 
b/app/src/main/java/net/taler/merchantpos/MainActivity.kt
index 7c8330a..4206b51 100644
--- a/app/src/main/java/net/taler/merchantpos/MainActivity.kt
+++ b/app/src/main/java/net/taler/merchantpos/MainActivity.kt
@@ -1,338 +1,108 @@
 package net.taler.merchantpos
 
-import android.content.Context
-import android.nfc.NfcAdapter
-import android.nfc.Tag
-import android.nfc.tech.IsoDep
+import android.content.Intent
+import android.content.Intent.ACTION_MAIN
+import android.content.Intent.CATEGORY_HOME
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
 import android.os.Bundle
-import android.util.Log
-import androidx.core.view.GravityCompat
 import android.view.MenuItem
-import androidx.drawerlayout.widget.DrawerLayout
-import com.google.android.material.navigation.NavigationView
+import androidx.activity.viewModels
 import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.widget.Toolbar
-import android.view.Menu
-import androidx.lifecycle.ViewModelProviders
+import androidx.core.view.GravityCompat.START
+import androidx.drawerlayout.widget.DrawerLayout
+import androidx.lifecycle.Observer
 import androidx.navigation.NavController
-import androidx.navigation.findNavController
+import androidx.navigation.fragment.NavHostFragment
 import androidx.navigation.ui.AppBarConfiguration
 import androidx.navigation.ui.setupWithNavController
-import net.taler.merchantpos.Utils.Companion.hexStringToByteArray
-import org.json.JSONObject
-import java.io.ByteArrayOutputStream
-import java.net.URL
-import javax.net.ssl.HttpsURLConnection
-
-
-class Utils {
-    companion object {
-        private val HEX_CHARS = "0123456789ABCDEF"
-        fun hexStringToByteArray(data: String): ByteArray {
-
-            val result = ByteArray(data.length / 2)
-
-            for (i in 0 until data.length 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.set(i.shr(1), octet.toByte())
-            }
-
-            return result
-        }
-
-        private val HEX_CHARS_ARRAY = "0123456789ABCDEF".toCharArray()
-        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()
-        }
-    }
-}
-
-val TALER_AID = "A0000002471001"
-
-
-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")
-    }
-}
-
-fun apduSelectFile(): ByteArray {
-    return hexStringToByteArray("00A4040007A0000002471001")
-}
-
-
-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()
-}
-
-fun apduPutTalerData(talerInst: Int, payload: ByteArray): ByteArray {
-    val realPayload = ByteArrayOutputStream()
-    realPayload.write(talerInst)
-    realPayload.write(payload)
-    return apduPutData(realPayload.toByteArray())
-}
-
-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()
-}
-
-
-class MainActivity : AppCompatActivity(), 
NavigationView.OnNavigationItemSelectedListener,
-    NfcAdapter.ReaderCallback {
-
-    companion object {
-        const val TAG = "taler-merchant"
-    }
-
-    private lateinit var model: PosTerminalViewModel
-    private var nfcAdapter: NfcAdapter? = null
-
-    private var currentTag: IsoDep? = null
-
-    override fun onTagDiscovered(tag: Tag?) {
-
-        Log.v(TAG, "tag discovered")
-
-        val isoDep = IsoDep.get(tag)
-        isoDep.connect()
-
-        currentTag = isoDep
-
-        isoDep.transceive(apduSelectFile())
-
-        val contractUri: String? = model.activeTalerPayUri
-
-        if (contractUri != null) {
-            isoDep.transceive(apduPutTalerData(1, contractUri.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")
+import com.google.android.material.navigation.NavigationView
+import 
com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener
 
-                isoDep.transceive(apduPutTalerData(2, 
tunnelResp.toString().toByteArray()))
-            } catch (e: Exception) {
-                Log.v(TAG, "exception during NFC loop: ${e}")
-                break
-            }
-        }
 
-        isoDep.close()
-    }
+class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener {
 
-    public override fun onResume() {
-        super.onResume()
-        nfcAdapter?.enableReaderMode(
-            this, this,
-            NfcAdapter.FLAG_READER_NFC_A or
-                    NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
-            null
-        )
-    }
+    private val model: MainViewModel by viewModels()
+    private val nfcManager = NfcManager()
 
-    public override fun onPause() {
-        super.onPause()
-        nfcAdapter?.disableReaderMode(this)
-    }
+    private lateinit var nav: NavController
+    private lateinit var drawerLayout: DrawerLayout
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
 
-        nfcAdapter = NfcAdapter.getDefaultAdapter(this)
+        model.paymentManager.payment.observe(this, Observer { payment ->
+            payment?.talerPayUri?.let {
+                nfcManager.setTagString(it)
+            }
+        })
 
-        setContentView(R.layout.activity_main)
         val toolbar: Toolbar = findViewById(R.id.toolbar)
         setSupportActionBar(toolbar)
 
-        val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
+        drawerLayout = findViewById(R.id.drawer_layout)
         val navView: NavigationView = findViewById(R.id.nav_view)
-
         navView.setNavigationItemSelectedListener(this)
+        val appBarConfiguration = AppBarConfiguration(
+            setOf(
+                R.id.order,
+                R.id.merchantSettings,
+                R.id.merchantHistory
+            ), drawerLayout
+        )
 
-        val navController = findNavController(R.id.nav_host_fragment)
-        val appBarConfiguration =
-            AppBarConfiguration(
-                setOf(
-                    R.id.createPayment,
-                    R.id.merchantSettings,
-                    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 = ViewModelProviders.of(this)[PosTerminalViewModel::class.java]
-        model.merchantConfig =
-            MerchantConfig(baseUrl!!, instance!!, apiKey!!, currency!!)
-
+        val navHostFragment =
+            supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as 
NavHostFragment
+        nav = navHostFragment.navController
+        toolbar.setupWithNavController(nav, appBarConfiguration)
     }
 
-    override fun onBackPressed() {
-        val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
-        if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
-            drawerLayout.closeDrawer(GravityCompat.START)
-        } else {
-            super.onBackPressed()
+    override fun onStart() {
+        super.onStart()
+        if (model.configManager.needsConfig()) {
+            nav.navigate(R.id.action_global_merchantSettings)
+        } else if (model.configManager.merchantConfig == null) {
+            nav.navigate(R.id.action_global_configFetcher)
         }
     }
 
-    override fun onCreateOptionsMenu(menu: Menu): Boolean {
-        // Inflate the menu; this adds items to the action bar if it is 
present.
-        menuInflater.inflate(R.menu.main, menu)
-        return true
+    public override fun onResume() {
+        super.onResume()
+        // TODO should we only read tags when a payment is to be made?
+        NfcManager.start(this, nfcManager)
     }
 
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        // Handle action bar item clicks here. The action bar will
-        // automatically handle clicks on the Home/Up button, so long
-        // as you specify a parent activity in AndroidManifest.xml.
-        return when (item.itemId) {
-            R.id.action_settings -> true
-            else -> super.onOptionsItemSelected(item)
-        }
+    public override fun onPause() {
+        super.onPause()
+        NfcManager.stop(this)
     }
 
     override fun onNavigationItemSelected(item: MenuItem): Boolean {
         // Handle navigation view item clicks here.
-        val nav: NavController = findNavController(R.id.nav_host_fragment)
         when (item.itemId) {
-            R.id.nav_home -> {
-                nav.navigate(R.id.action_global_createPayment)
-            }
-            R.id.nav_history -> {
-                nav.navigate(R.id.action_global_merchantHistory)
-            }
-            R.id.nav_settings -> {
-                nav.navigate(R.id.action_global_merchantSettings)
-            }
+            R.id.nav_order -> nav.navigate(R.id.action_global_order)
+            R.id.nav_history -> 
nav.navigate(R.id.action_global_merchantHistory)
+            R.id.nav_settings -> 
nav.navigate(R.id.action_global_merchantSettings)
         }
         val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
-        drawerLayout.closeDrawer(GravityCompat.START)
+        drawerLayout.closeDrawer(START)
         return true
     }
 
+    override fun onBackPressed() {
+        if (drawerLayout.isDrawerOpen(START)) {
+            drawerLayout.closeDrawer(START)
+        } else if (nav.currentDestination?.id == R.id.merchantSettings && 
model.configManager.needsConfig()) {
+            // we are in the configuration screen and need a config to continue
+            val intent = Intent(ACTION_MAIN).apply {
+                addCategory(CATEGORY_HOME)
+                flags = FLAG_ACTIVITY_NEW_TASK
+            }
+            startActivity(intent)
+        } else {
+            super.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..f4f792b
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/MainViewModel.kt
@@ -0,0 +1,37 @@
+package net.taler.merchantpos
+
+import android.app.Application
+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
+import net.taler.merchantpos.payment.PaymentManager
+
+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 paymentManager = PaymentManager(configManager, queue, mapper)
+
+    init {
+        if (configManager.merchantConfig == null && 
configManager.config.isValid()) {
+            configManager.fetchConfig(configManager.config, false)
+        }
+    }
+
+    override fun onCleared() {
+        queue.cancelAll { !it.isCanceled }
+    }
+
+}
diff --git a/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt 
b/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt
index b79f836..eb8288c 100644
--- a/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt
+++ b/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt
@@ -1,179 +1,173 @@
 package net.taler.merchantpos
 
-
 import android.annotation.SuppressLint
 import android.os.Bundle
 import android.util.Log
-import androidx.fragment.app.Fragment
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
 import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModelProviders
+import androidx.navigation.fragment.findNavController
 import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
 import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
-import com.android.volley.Request
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import com.android.volley.Request.Method.GET
 import com.android.volley.RequestQueue
-import com.android.volley.Response
-import com.android.volley.VolleyError
+import com.android.volley.Response.ErrorListener
+import com.android.volley.Response.Listener
 import com.android.volley.toolbox.Volley
 import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
+import kotlinx.android.synthetic.main.fragment_merchant_history.*
+import net.taler.merchantpos.HistoryItemAdapter.HistoryItemViewHolder
+import net.taler.merchantpos.config.MerchantRequest
 import org.json.JSONObject
 import java.time.Instant
 import java.time.ZoneId
 import java.time.format.DateTimeFormatter
-import java.time.format.FormatStyle
+import java.time.format.FormatStyle.SHORT
 import java.util.*
 
-
-
-data class HistoryItem(
-    val orderId: String,
-    val amount: Amount,
-    val summary: String,
-    val timestamp: Instant
-)
-
-class MyAdapter(private var myDataset: List<HistoryItem>) :
-    RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
MyViewHolder {
-        val view =
-            LayoutInflater.from(parent.context).inflate(R.layout.history_row, 
parent, false)
-        return MyViewHolder(view)
-    }
-
-    override fun getItemCount(): Int {
-        return myDataset.size
-    }
-
-    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
-        val item = myDataset[position]
-        val summaryTextView = 
holder.rowView.findViewById<TextView>(R.id.text_history_summary)
-        summaryTextView.text = myDataset[position].summary
-
-        val amount = myDataset[position].amount
-        val amountTextView = 
holder.rowView.findViewById<TextView>(R.id.text_history_amount)
-        @SuppressLint("SetTextI18n")
-        amountTextView.text = "${amount.amount} ${amount.currency}"
-
-        val formatter = 
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
-            .withLocale(Locale.UK)
-            .withZone(ZoneId.systemDefault())
-        val timestampTextView = 
holder.rowView.findViewById<TextView>(R.id.text_history_time)
-        timestampTextView.text = formatter.format(item.timestamp)
-
-        val orderIdTextView =  
holder.rowView.findViewById<TextView>(R.id.text_history_order_id)
-        orderIdTextView.text = item.orderId
-    }
-
-    fun setData(dataset: List<HistoryItem>) {
-        this.myDataset = dataset
-        this.notifyDataSetChanged()
-    }
-
-    class MyViewHolder(val rowView: View) : RecyclerView.ViewHolder(rowView)
-}
-
-fun parseTalerTimestamp(s: String): Instant {
-    return 
Instant.ofEpochSecond(s.substringAfterLast('(').substringBeforeLast(')').toLong())
-}
-
 /**
  * Fragment to display the merchant's payment history,
  * received from the backend.
  */
 class MerchantHistory : Fragment() {
+
+    companion object {
+        const val TAG = "taler-merchant"
+    }
+
     private lateinit var queue: RequestQueue
-    private lateinit var model: PosTerminalViewModel
-    private val historyListAdapter = MyAdapter(listOf())
+    private val model: MainViewModel by activityViewModels()
+    private val historyListAdapter = HistoryItemAdapter(listOf())
 
     private val isLoading = MutableLiveData<Boolean>().apply { value = false }
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
-        model = activity?.run {
-            ViewModelProviders.of(this)[PosTerminalViewModel::class.java]
-        } ?: throw Exception("Invalid Activity")
-
         queue = Volley.newRequestQueue(context)
     }
 
-    private fun onNetworkError(volleyError: VolleyError?) {
-        this.isLoading.value = false
-        val mySnackbar = Snackbar.make(view!!, "Network Error", 
Snackbar.LENGTH_SHORT)
-        mySnackbar.show()
+    override fun onCreateView(
+        inflater: LayoutInflater, container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_merchant_history, container, 
false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        list_history.apply {
+            layoutManager = LinearLayoutManager(requireContext())
+            addItemDecoration(DividerItemDecoration(context, VERTICAL))
+            adapter = historyListAdapter
+        }
+
+        swiperefresh.isRefreshing = false
+        swiperefresh.setOnRefreshListener {
+            Log.v(TAG, "refreshing!")
+            fetchHistory()
+        }
+
+        this.isLoading.observe(viewLifecycleOwner, androidx.lifecycle.Observer 
{ loading ->
+            Log.v(TAG, "setting refreshing to $loading")
+            swiperefresh.isRefreshing = loading
+        })
+    }
+
+    override fun onStart() {
+        super.onStart()
+        if (model.configManager.merchantConfig?.instance == null) {
+            findNavController().navigate(R.id.action_global_merchantSettings)
+        } else {
+            fetchHistory()
+        }
+    }
+
+    private fun fetchHistory() {
+        isLoading.value = true
+        val merchantConfig = model.configManager.merchantConfig!!
+        val params = mapOf("instance" to merchantConfig.instance)
+        val req = MerchantRequest(GET, merchantConfig, "history", params, null,
+            Listener { onHistoryResponse(it) },
+            ErrorListener { onNetworkError() })
+        queue.add(req)
     }
 
     private fun onHistoryResponse(body: JSONObject) {
         this.isLoading.value = false
-        Log.v(TAG, "got history response ${body}")
+        Log.v(TAG, "got history response $body")
+        // TODO use jackson instead of manual parsing
         val data = arrayListOf<HistoryItem>()
         val historyJson = body.getJSONArray("history")
         for (i in 0 until historyJson.length()) {
             val item = historyJson.getJSONObject(i)
             val orderId = item.getString("order_id")
             val summary = item.getString("summary")
-            val timestampStr = item.getString("timestamp")
-            val timestamp = parseTalerTimestamp(timestampStr)
+            val timestampObj = item.getJSONObject("timestamp")
+            val timestamp = Instant.ofEpochSecond(timestampObj.getLong("t_ms"))
             val amount = Amount.fromString(item.getString("amount"))
             data.add(HistoryItem(orderId, amount, summary, timestamp))
         }
         historyListAdapter.setData(data)
     }
 
-    private fun fetchHistory() {
-        isLoading.value = true
-        val instance = model.merchantConfig!!.instance
-        val req = MerchantInternalRequest(
-            Request.Method.GET,
-            model.merchantConfig!!,
-            "history",
-            mapOf("instance" to instance),
-            null,
-            Response.Listener { onHistoryResponse(it) },
-            Response.ErrorListener { onNetworkError(it) })
-        queue.add(req)
+    private fun onNetworkError() {
+        this.isLoading.value = false
+        Snackbar.make(view!!, "Network Error", LENGTH_SHORT).show()
     }
 
-    override fun onResume() {
-        super.onResume()
-        fetchHistory()
-    }
+}
 
-    override fun onCreateView(
-        inflater: LayoutInflater, container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View? {
-        val myLayoutManager = LinearLayoutManager(this@MerchantHistory.context)
-        val myItemDecoration = DividerItemDecoration(context, 
myLayoutManager.orientation)
-        // Inflate the layout for this fragment
-        val view = inflater.inflate(R.layout.fragment_merchant_history, 
container, false)
-        view.findViewById<RecyclerView>(R.id.list_history).apply {
-            layoutManager = myLayoutManager
-            adapter = historyListAdapter
-            addItemDecoration(myItemDecoration)
-        }
+data class HistoryItem(
+    val orderId: String,
+    val amount: Amount,
+    val summary: String,
+    val timestamp: Instant
+)
 
-        val refreshLayout = 
view.findViewById<SwipeRefreshLayout>(R.id.swiperefresh)
-        refreshLayout.isRefreshing = false
-        refreshLayout.setOnRefreshListener {
-            Log.v(TAG, "refreshing!")
-            fetchHistory()
-        }
+class HistoryItemAdapter(private var items: List<HistoryItem>) : 
Adapter<HistoryItemViewHolder>() {
 
-        this.isLoading.observe(this, androidx.lifecycle.Observer { loading ->
-            Log.v(TAG, "setting refreshing to ${loading}")
-            refreshLayout.isRefreshing = loading
-        })
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
HistoryItemViewHolder {
+        val v = 
LayoutInflater.from(parent.context).inflate(R.layout.history_row, parent, false)
+        return HistoryItemViewHolder(v)
+    }
+
+    override fun getItemCount() = items.size
 
-        return view
+    override fun onBindViewHolder(holder: HistoryItemViewHolder, position: 
Int) {
+        holder.bind(items[position])
     }
 
-    companion object {
-        val TAG = "taler-merchant"
+    fun setData(items: List<HistoryItem>) {
+        this.items = items
+        this.notifyDataSetChanged()
     }
+
+    class HistoryItemViewHolder(v: View) : ViewHolder(v) {
+
+        private val summaryTextView: TextView = 
v.findViewById(R.id.text_history_summary)
+        private val amountTextView: TextView = 
v.findViewById(R.id.text_history_amount)
+        private val timestampTextView: TextView = 
v.findViewById(R.id.text_history_time)
+        private val orderIdTextView: TextView = 
v.findViewById(R.id.text_history_order_id)
+        private val formatter: DateTimeFormatter = 
DateTimeFormatter.ofLocalizedDateTime(SHORT)
+            .withLocale(Locale.getDefault())
+            .withZone(ZoneId.systemDefault())
+
+        fun bind(item: HistoryItem) {
+            summaryTextView.text = item.summary
+            val amount = item.amount
+            @SuppressLint("SetTextI18n")
+            amountTextView.text = "${amount.amount} ${amount.currency}"
+            timestampTextView.text = formatter.format(item.timestamp)
+            orderIdTextView.text = item.orderId
+        }
+    }
+
 }
diff --git a/app/src/main/java/net/taler/merchantpos/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/NfcManager.kt 
b/app/src/main/java/net/taler/merchantpos/NfcManager.kt
new file mode 100644
index 0000000..1d21795
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/NfcManager.kt
@@ -0,0 +1,215 @@
+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
+
+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 TALER_AID = "A0000002471001"
+    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/app/src/main/java/net/taler/merchantpos/PaymentSuccess.kt 
b/app/src/main/java/net/taler/merchantpos/PaymentSuccess.kt
deleted file mode 100644
index 20b6ed1..0000000
--- a/app/src/main/java/net/taler/merchantpos/PaymentSuccess.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package net.taler.merchantpos
-
-
-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 androidx.navigation.findNavController
-
-/**
- * A simple [Fragment] subclass.
- */
-class PaymentSuccess : Fragment() {
-
-    override fun onCreateView(
-        inflater: LayoutInflater, container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View? {
-        val view = inflater.inflate(R.layout.fragment_payment_success, 
container, false)
-        view.findViewById<Button>(R.id.button_success_back).setOnClickListener 
{
-            activity!!.findNavController(R.id.nav_host_fragment).navigateUp()
-        }
-        return view
-    }
-
-
-}
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
deleted file mode 100644
index fbd60c6..0000000
--- a/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-package net.taler.merchantpos
-
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.Color
-import android.net.Uri
-import android.os.Bundle
-import androidx.fragment.app.Fragment
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import com.google.zxing.BarcodeFormat
-import com.google.zxing.common.BitMatrix
-import com.google.zxing.qrcode.QRCodeWriter
-import android.opengl.ETC1.getWidth
-import android.opengl.ETC1.getHeight
-import android.os.Handler
-import android.util.Log
-import android.widget.Button
-import android.widget.ImageView
-import android.widget.TextView
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.addCallback
-import androidx.lifecycle.ViewModelProviders
-import androidx.navigation.findNavController
-import androidx.navigation.fragment.findNavController
-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
-import java.net.URLEncoder
-
-
-// TODO: Rename parameter arguments, choose names that match
-// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
-private const val ARG_PARAM1 = "param1"
-private const val ARG_PARAM2 = "param2"
-
-/**
- * A simple [Fragment] subclass.
- *
- */
-class ProcessPayment : Fragment() {
-
-    private var paused: Boolean = true
-    private lateinit var queue: RequestQueue
-    private lateinit var model: PosTerminalViewModel
-
-    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 onCheckPayment(checkPaymentResponse: JSONObject) {
-        if (paused) {
-            return
-        }
-        //Log.v("taler-merchant", "got check payment result 
${checkPaymentResponse}")
-        if (checkPaymentResponse.getBoolean("paid")) {
-            queue.cancelAll { true }
-            
findNavController().navigate(R.id.action_processPayment_to_paymentSuccess)
-            return
-        }
-    }
-
-    private fun onNetworkError(volleyError: VolleyError?) {
-        val mySnackbar = Snackbar.make(view!!, "Network Error", 
Snackbar.LENGTH_SHORT)
-        mySnackbar.show()
-    }
-
-    private fun checkPaid() {
-        if (paused) {
-            return
-        }
-        //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) })
-        queue.add(req)
-        val handler = Handler()
-        handler.postDelayed({
-            checkPaid()
-        }, 500)
-
-    }
-
-    override fun onResume() {
-        this.paused = false
-        checkPaid()
-        super.onResume()
-    }
-
-    override fun onPause() {
-        this.paused = true
-        super.onPause()
-        queue.cancelAll { true }
-    }
-
-    override fun onCreateView(
-        inflater: LayoutInflater, container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View? {
-        // Inflate the layout for this fragment
-        val view = inflater.inflate(R.layout.fragment_process_payment, 
container, false)
-        val img = view.findViewById<ImageView>(R.id.qrcode)
-        val talerPayUrl = model.activeTalerPayUri!!;
-        val myBitmap = makeQrCode(talerPayUrl)
-        img.setImageBitmap(myBitmap)
-        val cancelPaymentButton = 
view.findViewById<Button>(R.id.button_cancel_payment)
-        cancelPaymentButton.setOnClickListener {
-            onPaymentCancel()
-        }
-        val textViewAmount = view.findViewById<TextView>(R.id.text_view_amount)
-        textViewAmount.text = model.activeAmountPretty()
-        val textViewOrderId = 
view.findViewById<TextView>(R.id.text_view_order_reference)
-        textViewOrderId.text = "Order Reference: " + model.activeOrderId
-        return view
-    }
-
-    private fun onPaymentCancel() {
-        val navController = findNavController()
-        navController.popBackStack()
-
-        val mySnackbar = Snackbar.make(view!!, "Payment Canceled", 
Snackbar.LENGTH_SHORT)
-        mySnackbar.show()
-    }
-
-    fun makeQrCode(text: String): Bitmap {
-        val qrCodeWriter: QRCodeWriter = QRCodeWriter()
-        val bitMatrix: BitMatrix = qrCodeWriter.encode(text, 
BarcodeFormat.QR_CODE, 256, 256)
-        val height = bitMatrix.height
-        val width = bitMatrix.width
-        val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
-        for (x in 0 until width) {
-            for (y in 0 until height) {
-                bmp.setPixel(x, y, if (bitMatrix.get(x, y)) Color.BLACK else 
Color.WHITE)
-            }
-        }
-        return bmp
-    }
-}
diff --git a/app/src/main/java/net/taler/merchantpos/QrCodeManager.kt 
b/app/src/main/java/net/taler/merchantpos/QrCodeManager.kt
new file mode 100644
index 0000000..6ec9893
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/QrCodeManager.kt
@@ -0,0 +1,26 @@
+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/app/src/main/java/net/taler/merchantpos/Utils.kt 
b/app/src/main/java/net/taler/merchantpos/Utils.kt
new file mode 100644
index 0000000..a318d23
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/Utils.kt
@@ -0,0 +1,69 @@
+package net.taler.merchantpos
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.Observer
+
+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()
+
+    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()
+    }
+
+}
+
+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/app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt 
b/app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
new file mode 100644
index 0000000..4520f8f
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
@@ -0,0 +1,46 @@
+package net.taler.merchantpos.config
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.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.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+
+class ConfigFetcherFragment : 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_config_fetcher, container, 
false)
+    }
+
+    override fun onActivityCreated(savedInstanceState: Bundle?) {
+        super.onActivityCreated(savedInstanceState)
+
+        configManager.configUpdateResult.observe(viewLifecycleOwner, Observer 
{ result ->
+            when {
+                result == null -> return@Observer
+                result.error -> onNetworkError(result.authError)
+                else -> 
findNavController().navigate(R.id.action_configFetcher_to_order)
+            }
+        })
+    }
+
+    private fun onNetworkError(authError: Boolean) {
+        val res = if (authError) R.string.config_auth_error else 
R.string.config_error
+        Snackbar.make(view!!, res, LENGTH_SHORT).show()
+        
findNavController().navigate(R.id.action_configFetcher_to_merchantSettings)
+    }
+
+}
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..6c230d1
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
@@ -0,0 +1,157 @@
+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, currency: String): 
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)
+    }
+
+    /**
+     * Returns true if the user needs to provide more configuration
+     * and false if the configuration is sufficient to continue.
+     */
+    fun needsConfig(): Boolean {
+        return !config.isValid() || (!config.hasPassword() && merchantConfig 
== null)
+    }
+
+    @UiThread
+    fun fetchConfig(config: Config, save: Boolean, savePassword: Boolean = 
false) {
+        mConfigUpdateResult.value = null
+        val configToSave = if (save) {
+            if (savePassword) config else config.copy(password = "")
+        } 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
+        }
+
+        val params = mapOf("instance" to merchantConfig.instance)
+        val req = MerchantRequest(GET, merchantConfig, "config", params, null,
+            Listener { onMerchantConfigReceived(config, json, merchantConfig, 
it) },
+            ErrorListener { onNetworkError(it) }
+        )
+        queue.add(req)
+    }
+
+    private fun onMerchantConfigReceived(
+        newConfig: Config?,
+        configJson: JSONObject,
+        merchantConfig: MerchantConfig,
+        json: JSONObject
+    ) = scope.launch(Dispatchers.Main) {
+        val currency = json.getString("currency")
+
+        var configValid = true
+        configurationReceivers.forEach {
+            val result = it.onConfigurationReceived(configJson, currency)
+            configValid = result && configValid
+        }
+        if (configValid) {
+            newConfig?.let {
+                config = it
+                saveConfig(it)
+            }
+            this@ConfigManager.merchantConfig = merchantConfig.copy(currency = 
currency)
+            mConfigUpdateResult.value = ConfigUpdateResult(currency)
+        } else {
+            mConfigUpdateResult.value = ConfigUpdateResult(null)
+        }
+    }
+
+    fun forgetPassword() {
+        config = config.copy(password = "")
+        saveConfig(config)
+        merchantConfig = 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 54%
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..634cc3e 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,24 @@
-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
+) {
+    fun isValid() = !configUrl.isBlank()
+    fun hasPassword() = !password.isBlank()
+}
 
 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 +28,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..abee7e3
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
@@ -0,0 +1,120 @@
+package net.taler.merchantpos.config
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+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 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, 
savePasswordCheckBox.isChecked)
+            configManager.configUpdateResult.observe(viewLifecycleOwner, 
Observer { result ->
+                when {
+                    result == null -> return@Observer
+                    result.error -> onNetworkError(result.authError)
+                    else -> onConfigReceived(result.currency!!)
+                }
+                
configManager.configUpdateResult.removeObservers(viewLifecycleOwner)
+            })
+        }
+        forgetPasswordButton.setOnClickListener {
+            configManager.forgetPassword()
+            passwordView.editText!!.text = null
+            forgetPasswordButton.visibility = GONE
+            currencyView.visibility = GONE
+        }
+        updateView()
+    }
+
+    override fun onStart() {
+        super.onStart()
+        // focus password if this is the only empty field
+        if (passwordView.editText!!.text.isBlank()
+            && !configUrlView.editText!!.text.isBlank()
+            && !usernameView.editText!!.text.isBlank()
+        ) {
+            passwordView.requestFocus()
+        }
+    }
+
+    private fun updateView() {
+        configUrlView.editText!!.setText(configManager.config.configUrl)
+        usernameView.editText!!.setText(configManager.config.username)
+        passwordView.editText!!.setText(configManager.config.password)
+
+        forgetPasswordButton.visibility = if 
(configManager.config.hasPassword()) VISIBLE else GONE
+
+        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()
+        findNavController().navigate(R.id.order)
+    }
+
+    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
new file mode 100644
index 0000000..148699c
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt
@@ -0,0 +1,90 @@
+package net.taler.merchantpos.order
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.ViewGroup
+import android.widget.Button
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+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
+
+interface CategorySelectionListener {
+    fun onCategorySelected(category: Category)
+}
+
+class CategoriesFragment : Fragment(), CategorySelectionListener {
+
+    private val viewModel: MainViewModel by activityViewModels()
+    private val orderManager by lazy { viewModel.orderManager }
+    private val adapter = CategoryAdapter(this)
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_categories, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        categoriesList.apply {
+            adapter = this@CategoriesFragment.adapter
+            layoutManager = LinearLayoutManager(requireContext())
+        }
+
+        orderManager.categories.observe(viewLifecycleOwner, Observer { 
categories ->
+            adapter.setItems(categories)
+            progressBar.visibility = INVISIBLE
+        })
+    }
+
+    override fun onCategorySelected(category: Category) {
+        orderManager.setCurrentCategory(category)
+    }
+
+}
+
+private class CategoryAdapter(
+    private val listener: CategorySelectionListener
+) : Adapter<CategoryViewHolder>() {
+
+    private val categories = ArrayList<Category>()
+
+    override fun getItemCount() = categories.size
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
CategoryViewHolder {
+        val view =
+            
LayoutInflater.from(parent.context).inflate(R.layout.list_item_category, 
parent, false)
+        return CategoryViewHolder(view)
+    }
+
+    override fun onBindViewHolder(holder: CategoryViewHolder, position: Int) {
+        holder.bind(categories[position])
+    }
+
+    fun setItems(items: List<Category>) {
+        categories.clear()
+        categories.addAll(items)
+        notifyDataSetChanged()
+    }
+
+    private inner class CategoryViewHolder(v: View) : 
RecyclerView.ViewHolder(v) {
+        private val button: Button = v.findViewById(R.id.button)
+
+        fun bind(category: Category) {
+            button.text = category.name
+            button.isPressed = category.selected
+            button.setOnClickListener { listener.onCategorySelected(category) }
+        }
+    }
+
+}
diff --git a/app/src/main/java/net/taler/merchantpos/order/Definitions.kt 
b/app/src/main/java/net/taler/merchantpos/order/Definitions.kt
new file mode 100644
index 0000000..872f2f5
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/order/Definitions.kt
@@ -0,0 +1,115 @@
+package net.taler.merchantpos.order
+
+import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonProperty
+import net.taler.merchantpos.Amount
+
+data class Category(
+    val id: Int,
+    val name: String
+) {
+    var selected: Boolean = false
+}
+
+interface Product {
+    val id: String
+    val description: String
+    val price: String
+    val location: String?
+}
+
+data class ConfigProduct(
+    @JsonProperty("product_id")
+    override val id: String,
+    override val description: String,
+    override val price: String,
+    @JsonProperty("delivery_location")
+    override val location: 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?): Boolean {
+        return other is ConfigProduct && id == other.id
+    }
+
+    override fun hashCode(): Int {
+        var result = id.hashCode()
+        result = 31 * result + description.hashCode()
+        result = 31 * result + price.hashCode()
+        result = 31 * result + (location?.hashCode() ?: 0)
+        result = 31 * result + categories.hashCode()
+        return result
+    }
+}
+
+data class ContractProduct(
+    @JsonProperty("product_id")
+    override val id: String,
+    override val description: String,
+    override val price: String,
+    @JsonProperty("delivery_location")
+    override val location: String?,
+    val quantity: Int
+) : Product {
+    constructor(product: ConfigProduct) : this(
+        product.id,
+        product.description,
+        product.price,
+        product.location,
+        product.quantity
+    )
+}
+
+data class Order(val availableCategories: Map<Int, Category>) {
+    val products = ArrayList<ConfigProduct>()
+    val summary: String
+        get() {
+            val categories = HashMap<Category, Int>()
+            products.forEach { product ->
+                val categoryId = product.categories[0]
+                val category = availableCategories.getValue(categoryId)
+                val oldQuantity = categories[category] ?: 0
+                categories[category] = oldQuantity + product.quantity
+            }
+            return categories.map { (category, quantity) ->
+                "$quantity x ${category.name}"
+            }.joinToString()
+        }
+    val total: Double
+        get() {
+            var total = 0.0
+            products.forEach { product ->
+                val price = product.priceAsDouble
+                total += price * product.quantity
+            }
+            return total
+        }
+    val totalAsString: String
+        get() = String.format("%.2f", total)
+
+    operator fun plus(product: ConfigProduct): Order {
+        val i = products.indexOf(product)
+        if (i == -1) {
+            products.add(product.copy(quantity = 1))
+        } else {
+            val quantity = products[i].quantity
+            products[i] = products[i].copy(quantity = quantity + 1)
+        }
+        return this
+    }
+
+    operator fun minus(product: ConfigProduct): Order {
+        val i = products.indexOf(product)
+        if (i == -1) return this
+        val quantity = products[i].quantity
+        if (quantity <= 1) {
+            products.remove(product)
+        } else {
+            products[i] = products[i].copy(quantity = quantity - 1)
+        }
+        return this
+    }
+}
diff --git a/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt 
b/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
new file mode 100644
index 0000000..6696afe
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
@@ -0,0 +1,76 @@
+package net.taler.merchantpos.order
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.NavController
+import androidx.navigation.Navigation.findNavController
+import androidx.navigation.fragment.findNavController
+import androidx.transition.TransitionManager.beginDelayedTransition
+import kotlinx.android.synthetic.main.fragment_order.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.order.RestartState.ENABLED
+import net.taler.merchantpos.order.RestartState.UNDO
+
+class OrderFragment : Fragment() {
+
+    private val viewModel: MainViewModel by activityViewModels()
+    private val orderManager by lazy { viewModel.orderManager }
+    private val paymentManager by lazy { viewModel.paymentManager }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_order, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        restartButton.setOnClickListener { orderManager.restartOrUndo() }
+        orderManager.restartState.observe(viewLifecycleOwner, Observer { state 
->
+            beginDelayedTransition(view as ViewGroup)
+            if (state == UNDO) {
+                restartButton.setText(R.string.order_undo)
+                restartButton.isEnabled = true
+                completeButton.isEnabled = false
+            } else {
+                restartButton.setText(R.string.order_restart)
+                restartButton.isEnabled = state == ENABLED
+                completeButton.isEnabled = state == ENABLED
+            }
+        })
+        minusButton.setOnClickListener { 
orderManager.decreaseSelectedOrderLine() }
+        plusButton.setOnClickListener { 
orderManager.increaseSelectedOrderLine() }
+        orderManager.modifyOrderAllowed.observe(viewLifecycleOwner, Observer { 
allowed ->
+            minusButton.isEnabled = allowed
+            plusButton.isEnabled = allowed
+        })
+    }
+
+    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_order_to_merchantSettings) }
+        historyButton.setOnClickListener { 
nav.navigate(R.id.action_order_to_merchantHistory) }
+        logoutButton.setOnClickListener { 
nav.navigate(R.id.action_order_to_merchantSettings) }
+        completeButton.setOnClickListener {
+            val order = orderManager.order.value ?: return@setOnClickListener
+            paymentManager.createPayment(order)
+            nav.navigate(R.id.action_order_to_processPayment)
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+        if (viewModel.configManager.needsConfig() || 
viewModel.configManager.merchantConfig?.currency == null) {
+            findNavController().navigate(R.id.action_global_merchantSettings)
+        }
+    }
+
+}
diff --git a/app/src/main/java/net/taler/merchantpos/order/OrderManager.kt 
b/app/src/main/java/net/taler/merchantpos/order/OrderManager.kt
new file mode 100644
index 0000000..d7db048
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/order/OrderManager.kt
@@ -0,0 +1,159 @@
+package net.taler.merchantpos.order
+
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+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.merchantpos.CombinedLiveData
+import net.taler.merchantpos.config.ConfigurationReceiver
+import net.taler.merchantpos.order.RestartState.DISABLED
+import net.taler.merchantpos.order.RestartState.ENABLED
+import net.taler.merchantpos.order.RestartState.UNDO
+import org.json.JSONObject
+
+enum class RestartState { ENABLED, DISABLED, UNDO }
+
+class OrderManager(private val mapper: ObjectMapper) : ConfigurationReceiver {
+
+    companion object {
+        val TAG = OrderManager::class.java.simpleName
+    }
+
+    private val productsByCategory = HashMap<Category, 
ArrayList<ConfigProduct>>()
+
+    private val mOrder = MutableLiveData<Order>()
+    private val newOrder  // an empty order containing only available 
categories
+        get() = Order(productsByCategory.keys.map { it.id to it }.toMap())
+    internal val order: LiveData<Order> = mOrder
+    internal val orderTotal: LiveData<Double> = map(mOrder) { it.total }
+
+    private val mProducts = MutableLiveData<List<ConfigProduct>>()
+    internal val products: LiveData<List<ConfigProduct>> = mProducts
+
+    private val mCategories = MutableLiveData<List<Category>>()
+    internal val categories: LiveData<List<Category>> = mCategories
+
+    private var undoOrder: Order? = null
+    private val mRestartState = MutableLiveData<RestartState>().apply { value 
= DISABLED }
+    internal val restartState: LiveData<RestartState> = mRestartState
+
+    private val mSelectedOrderLine = MutableLiveData<ConfigProduct>()
+
+    internal val modifyOrderAllowed =
+        CombinedLiveData(restartState, mSelectedOrderLine) { restartState, 
selectedOrderLine ->
+            restartState != DISABLED && selectedOrderLine != null
+        }
+
+    @Suppress("BlockingMethodInNonBlockingContext") // run on Dispatchers.Main
+    override suspend fun onConfigurationReceived(json: JSONObject, currency: 
String): 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
+
+        // parse products (live data gets updated in setCurrentCategory())
+        val productsStr = json.getJSONArray("products").toString()
+        val productsType = object : TypeReference<List<ConfigProduct>>() {}
+        val products: List<ConfigProduct> = mapper.readValue(productsStr, 
productsType)
+
+        // group products by categories
+        productsByCategory.clear()
+        val seenIds = ArrayList<String>()
+        products.forEach { product ->
+            val productCurrency = fromString(product.price).currency
+            if (productCurrency != currency) {
+                Log.e(TAG, "Product $product has currency $productCurrency, 
$currency expected")
+                return false
+            }
+            if (seenIds.contains(product.id)) {
+                Log.e(TAG, "Product $product has duplicate product_id 
${product.id}")
+                return false
+            }
+            seenIds.add(product.id)
+            product.categories.forEach { categoryId ->
+                val category = categories.find { it.id == categoryId }
+                if (category == null) {
+                    Log.e(TAG, "Product $product has unknown category 
$categoryId")
+                    return false
+                }
+                if (productsByCategory.containsKey(category)) {
+                    productsByCategory[category]?.add(product)
+                } else {
+                    productsByCategory[category] = 
ArrayList<ConfigProduct>().apply { add(product) }
+                }
+            }
+        }
+        return if (productsByCategory.size > 0) {
+            mCategories.postValue(categories)
+            mProducts.postValue(productsByCategory[categories[0]])
+            true
+        } else {
+            false
+        }
+    }
+
+    internal fun setCurrentCategory(category: Category) {
+        val newCategories = categories.value?.apply {
+            forEach { if (it.selected) it.selected = false }
+            category.selected = true
+        }
+        mCategories.postValue(newCategories)
+        mProducts.postValue(productsByCategory[category])
+    }
+
+    @UiThread
+    internal fun addProduct(product: ConfigProduct) {
+        val order = mOrder.value ?: newOrder
+        mOrder.value = order + product
+        mRestartState.value = ENABLED
+    }
+
+    @UiThread
+    internal fun removeProduct(product: ConfigProduct) {
+        val order = mOrder.value ?: throw IllegalStateException()
+        val modifiedOrder = order - product
+        mOrder.value = modifiedOrder
+        mRestartState.value = if (modifiedOrder.products.isEmpty()) DISABLED 
else ENABLED
+    }
+
+    @UiThread
+    internal fun restartOrUndo() {
+        if (restartState.value == UNDO) {
+            mOrder.value = undoOrder
+            mRestartState.value = ENABLED
+            undoOrder = null
+        } else {
+            undoOrder = mOrder.value
+            mOrder.value = newOrder
+            mRestartState.value = UNDO
+        }
+    }
+
+    @UiThread
+    fun selectOrderLine(product: ConfigProduct?) {
+        mSelectedOrderLine.value = product
+    }
+
+    @UiThread
+    fun increaseSelectedOrderLine() {
+        val orderLine = mSelectedOrderLine.value ?: throw 
IllegalStateException()
+        addProduct(orderLine)
+    }
+
+    @UiThread
+    fun decreaseSelectedOrderLine() {
+        val orderLine = mSelectedOrderLine.value ?: throw 
IllegalStateException()
+        removeProduct(orderLine)
+    }
+
+}
diff --git 
a/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt 
b/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
new file mode 100644
index 0000000..3988f17
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt
@@ -0,0 +1,171 @@
+package net.taler.merchantpos.order
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.selection.ItemDetailsLookup
+import androidx.recyclerview.selection.ItemKeyProvider
+import androidx.recyclerview.selection.SelectionPredicates
+import androidx.recyclerview.selection.SelectionTracker
+import androidx.recyclerview.selection.StorageStrategy
+import androidx.recyclerview.widget.AsyncListDiffer
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+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.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.order.OrderAdapter.OrderLineLookup
+import net.taler.merchantpos.order.OrderAdapter.OrderViewHolder
+
+class OrderStateFragment : Fragment() {
+
+    private val viewModel: MainViewModel by activityViewModels()
+    private val orderManager by lazy { viewModel.orderManager }
+    private val adapter = OrderAdapter()
+    private var tracker: SelectionTracker<String>? = null
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_order_state, container, 
false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        orderList.apply {
+            adapter = this@OrderStateFragment.adapter
+            layoutManager = LinearLayoutManager(requireContext())
+        }
+        val detailsLookup = OrderLineLookup(orderList)
+        val tracker = SelectionTracker.Builder(
+            "order-selection-id",
+            orderList,
+            adapter.keyProvider,
+            detailsLookup,
+            StorageStrategy.createStringStorage()
+        ).withSelectionPredicate(
+            SelectionPredicates.createSelectSingleAnything()
+        ).build()
+        savedInstanceState?.let { tracker.onRestoreInstanceState(it) }
+        adapter.tracker = tracker
+        tracker.addObserver(object : 
SelectionTracker.SelectionObserver<String>() {
+            override fun onItemStateChanged(key: String, selected: Boolean) {
+                super.onItemStateChanged(key, selected)
+                val item = if (selected) adapter.getItemByKey(key) else null
+                orderManager.selectOrderLine(item)
+            }
+        })
+        this.tracker = tracker
+
+        orderManager.order.observe(viewLifecycleOwner, Observer { order ->
+            adapter.setItems(order.products) {
+                // workaround for bug: SelectionObserver doesn't update when 
removing selected item
+                if (tracker.hasSelection()) {
+                    val key = tracker.selection.first()
+                    val product = order.products.find { it.id == key }
+                    if (product == null) tracker.clearSelection()
+                }
+            }
+        })
+        orderManager.orderTotal.observe(viewLifecycleOwner, Observer { 
orderTotal ->
+            if (orderTotal == 0.0) {
+                totalView.text = null
+            } else {
+                totalView.text = getString(R.string.order_total, orderTotal)
+            }
+        })
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        tracker?.onSaveInstanceState(outState)
+    }
+
+}
+
+private class OrderAdapter : Adapter<OrderViewHolder>() {
+
+    lateinit var tracker: SelectionTracker<String>
+    val keyProvider = OrderKeyProvider()
+    private val itemCallback = object : DiffUtil.ItemCallback<ConfigProduct>() 
{
+        override fun areItemsTheSame(oldItem: ConfigProduct, newItem: 
ConfigProduct): Boolean {
+            return oldItem == newItem
+        }
+
+        override fun areContentsTheSame(oldItem: ConfigProduct, newItem: 
ConfigProduct): Boolean {
+            return oldItem.quantity == newItem.quantity
+        }
+    }
+    private val differ = AsyncListDiffer<ConfigProduct>(this, itemCallback)
+
+    override fun getItemCount() = differ.currentList.size
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
OrderViewHolder {
+        val view =
+            
LayoutInflater.from(parent.context).inflate(R.layout.list_item_order, parent, 
false)
+        return OrderViewHolder(view)
+    }
+
+    override fun onBindViewHolder(holder: OrderViewHolder, position: Int) {
+        val item = getItem(position)!!
+        holder.bind(item, tracker.isSelected(item.id))
+    }
+
+    fun setItems(items: List<ConfigProduct>, commitCallback: () -> Unit) {
+        // toMutableList() is needed for some reason, otherwise doesn't update 
adapter
+        differ.submitList(items.toMutableList(), commitCallback)
+    }
+
+    fun getItem(position: Int): ConfigProduct? = differ.currentList[position]
+
+    fun getItemByKey(key: String): ConfigProduct? {
+        return differ.currentList.find { it.id == key }
+    }
+
+    private inner class OrderViewHolder(private val v: View) : ViewHolder(v) {
+        private val quantity: TextView = v.findViewById(R.id.quantity)
+        private val name: TextView = v.findViewById(R.id.name)
+        private val price: TextView = v.findViewById(R.id.price)
+
+        fun bind(product: ConfigProduct, selected: Boolean) {
+            v.isActivated = selected
+            quantity.text = product.quantity.toString()
+            name.text = product.description
+            price.text = String.format("%.2f", product.priceAsDouble * 
product.quantity)
+        }
+    }
+
+    private inner class OrderKeyProvider : 
ItemKeyProvider<String>(SCOPE_MAPPED) {
+        override fun getKey(position: Int) = getItem(position)!!.id
+        override fun getPosition(key: String): Int {
+            return differ.currentList.indexOfFirst { it.id == key }
+        }
+    }
+
+    internal class OrderLineLookup(private val list: RecyclerView) : 
ItemDetailsLookup<String>() {
+        override fun getItemDetails(e: MotionEvent): ItemDetails<String>? {
+            list.findChildViewUnder(e.x, e.y)?.let { view ->
+                val holder = list.getChildViewHolder(view)
+                val adapter = list.adapter as OrderAdapter
+                val position = holder.adapterPosition
+                return object : ItemDetails<String>() {
+                    override fun getPosition(): Int = position
+                    override fun getSelectionKey(): String = 
adapter.keyProvider.getKey(position)
+                    override fun inSelectionHotspot(e: MotionEvent) = true
+                }
+            }
+            return null
+        }
+    }
+
+}
diff --git a/app/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt 
b/app/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
new file mode 100644
index 0000000..d680fff
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt
@@ -0,0 +1,95 @@
+package net.taler.merchantpos.order
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+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
+
+interface ProductSelectionListener {
+    fun onProductSelected(product: ConfigProduct)
+}
+
+class ProductsFragment : Fragment(), ProductSelectionListener {
+
+    private val viewModel: MainViewModel by activityViewModels()
+    private val orderManager by lazy { viewModel.orderManager }
+    private val adapter = ProductAdapter(this)
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_products, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        productsList.apply {
+            adapter = this@ProductsFragment.adapter
+            layoutManager = GridLayoutManager(requireContext(), 3)
+        }
+
+        orderManager.products.observe(viewLifecycleOwner, Observer { products 
->
+            if (products == null) {
+                adapter.setItems(emptyList())
+            } else {
+                adapter.setItems(products)
+            }
+            progressBar.visibility = INVISIBLE
+        })
+    }
+
+    override fun onProductSelected(product: ConfigProduct) {
+        orderManager.addProduct(product)
+    }
+
+}
+
+private class ProductAdapter(
+    private val listener: ProductSelectionListener
+) : Adapter<ProductViewHolder>() {
+
+    private val products = ArrayList<ConfigProduct>()
+
+    override fun getItemCount() = products.size
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
ProductViewHolder {
+        val view =
+            
LayoutInflater.from(parent.context).inflate(R.layout.list_item_product, parent, 
false)
+        return ProductViewHolder(view)
+    }
+
+    override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
+        holder.bind(products[position])
+    }
+
+    fun setItems(items: List<ConfigProduct>) {
+        products.clear()
+        products.addAll(items)
+        notifyDataSetChanged()
+    }
+
+    private inner class ProductViewHolder(private val v: View) : ViewHolder(v) 
{
+        private val name: TextView = v.findViewById(R.id.name)
+        private val price: TextView = v.findViewById(R.id.price)
+
+        fun bind(product: ConfigProduct) {
+            name.text = product.description
+            price.text = product.priceAsDouble.toString()
+            v.setOnClickListener { listener.onProductSelected(product) }
+        }
+    }
+
+}
diff --git a/app/src/main/java/net/taler/merchantpos/payment/Payment.kt 
b/app/src/main/java/net/taler/merchantpos/payment/Payment.kt
new file mode 100644
index 0000000..b82c8c0
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/payment/Payment.kt
@@ -0,0 +1,13 @@
+package net.taler.merchantpos.payment
+
+import net.taler.merchantpos.order.Order
+
+data class Payment(
+    val order: Order,
+    val summary: String,
+    val currency: String,
+    val orderId: String? = null,
+    val talerPayUri: String? = null,
+    val paid: Boolean = false,
+    val error: Boolean = false
+)
diff --git a/app/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt 
b/app/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
new file mode 100644
index 0000000..8167f86
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
@@ -0,0 +1,130 @@
+package net.taler.merchantpos.payment
+
+import android.os.CountDownTimer
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.android.volley.Request.Method.GET
+import com.android.volley.Request.Method.POST
+import com.android.volley.RequestQueue
+import com.android.volley.Response.ErrorListener
+import com.android.volley.Response.Listener
+import com.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
+import java.net.URLEncoder
+import java.util.concurrent.TimeUnit.MINUTES
+import java.util.concurrent.TimeUnit.SECONDS
+
+private val TIMEOUT = MINUTES.toMillis(2)
+private val CHECK_INTERVAL = SECONDS.toMillis(1)
+private const val FULFILLMENT_PREFIX = "taler://fulfillment-success/"
+
+class PaymentManager(
+    private val configManager: ConfigManager,
+    private val queue: RequestQueue,
+    private val mapper: ObjectMapper
+) {
+
+    private val mPayment = MutableLiveData<Payment>()
+    val payment: LiveData<Payment> = mPayment
+
+    private val checkTimer = object : CountDownTimer(TIMEOUT, CHECK_INTERVAL) {
+        override fun onTick(millisUntilFinished: Long) {
+            val orderId = payment.value?.orderId
+            if (orderId == null) cancel()
+            else checkPayment(orderId)
+        }
+
+        override fun onFinish() {
+            payment.value?.copy(error = true)?.let { mPayment.value = it }
+        }
+    }
+
+    @UiThread
+    fun createPayment(order: Order) {
+        val merchantConfig = configManager.merchantConfig!!
+
+        val currency = merchantConfig.currency!!
+        val amount = "$currency:${order.totalAsString}"
+        val summary = order.summary
+
+        mPayment.value = Payment(order, summary, currency)
+
+        val fulfillmentId = "${System.currentTimeMillis()}-${order.hashCode()}"
+        val fulfillmentUrl =
+            "${FULFILLMENT_PREFIX}${URLEncoder.encode(summary, 
"UTF-8")}#$fulfillmentId"
+        val body = JSONObject().apply {
+            put("order", JSONObject().apply {
+                put("amount", amount)
+                put("summary", summary)
+                // fulfillment_url needs to be unique per order
+                put("fulfillment_url", fulfillmentUrl)
+                put("instance", "default")
+                put("products", order.getProductsJson())
+            })
+        }
+
+        val req = MerchantRequest(POST, merchantConfig, "order", null, body,
+            Listener { onOrderCreated(it) },
+            ErrorListener { onNetworkError(it) }
+        )
+        queue.add(req)
+    }
+
+    private fun Order.getProductsJson(): JSONArray {
+        val contractProducts = products.map { ContractProduct(it) }
+        val productsStr = mapper.writeValueAsString(contractProducts)
+        return JSONArray(productsStr)
+    }
+
+    private fun onOrderCreated(orderResponse: JSONObject) {
+        val orderId = orderResponse.getString("order_id")
+        mPayment.value = mPayment.value!!.copy(orderId = orderId)
+        checkTimer.start()
+    }
+
+    private fun checkPayment(orderId: String) {
+        val merchantConfig = configManager.merchantConfig!!
+        val params = mapOf(
+            "order_id" to orderId,
+            "instance" to merchantConfig.instance
+        )
+
+        val req = MerchantRequest(GET, merchantConfig, "check-payment", 
params, null,
+            Listener { onPaymentChecked(it) },
+            ErrorListener { onNetworkError(it) })
+        queue.add(req)
+    }
+
+    /**
+     * Called when the /check-payment response gave a result.
+     */
+    private fun onPaymentChecked(checkPaymentResponse: JSONObject) {
+        val currentValue = requireNotNull(mPayment.value)
+        if (checkPaymentResponse.getBoolean("paid")) {
+            mPayment.value = currentValue.copy(paid = true)
+            checkTimer.cancel()
+        } else if (currentValue.talerPayUri == null) {
+            val talerPayUri = checkPaymentResponse.getString("taler_pay_uri")
+            mPayment.value = currentValue.copy(talerPayUri = talerPayUri)
+        }
+    }
+
+    private fun onNetworkError(volleyError: VolleyError) {
+        Log.e(PaymentManager::class.java.simpleName, volleyError.toString())
+        cancelPayment()
+    }
+
+    fun cancelPayment() {
+        mPayment.value = mPayment.value!!.copy(error = true)
+        checkTimer.cancel()
+    }
+
+}
diff --git 
a/app/src/main/java/net/taler/merchantpos/payment/PaymentSuccessFragment.kt 
b/app/src/main/java/net/taler/merchantpos/payment/PaymentSuccessFragment.kt
new file mode 100644
index 0000000..176b6fc
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/payment/PaymentSuccessFragment.kt
@@ -0,0 +1,27 @@
+package net.taler.merchantpos.payment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.navigation.findNavController
+import kotlinx.android.synthetic.main.fragment_payment_success.*
+import net.taler.merchantpos.R
+
+class PaymentSuccessFragment : Fragment() {
+
+    override fun onCreateView(
+        inflater: LayoutInflater, container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_payment_success, container, 
false)
+    }
+
+    override fun onActivityCreated(savedInstanceState: Bundle?) {
+        super.onActivityCreated(savedInstanceState)
+        button_success_back.setOnClickListener {
+            
requireActivity().findNavController(R.id.nav_host_fragment).navigateUp()
+        }
+    }
+}
diff --git 
a/app/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt 
b/app/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
new file mode 100644
index 0000000..89198b8
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt
@@ -0,0 +1,67 @@
+package net.taler.merchantpos.payment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
+import kotlinx.android.synthetic.main.fragment_process_payment.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.NfcManager.Companion.hasNfc
+import net.taler.merchantpos.QrCodeManager.makeQrCode
+import net.taler.merchantpos.R
+
+class ProcessPaymentFragment : Fragment() {
+
+    private val model: MainViewModel by activityViewModels()
+    private val paymentManager by lazy { model.paymentManager }
+
+    override fun onCreateView(
+        inflater: LayoutInflater, container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_process_payment, container, 
false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        val introRes =
+            if (hasNfc(requireContext())) R.string.payment_intro_nfc else 
R.string.payment_intro
+        textView2.setText(introRes)
+        paymentManager.payment.observe(viewLifecycleOwner, Observer { payment 
->
+            onPaymentStateChanged(payment)
+        })
+        button_cancel_payment.setOnClickListener {
+            onPaymentCancel()
+        }
+    }
+
+    private fun onPaymentStateChanged(payment: Payment) {
+        if (payment.error) {
+            Snackbar.make(view!!, "Network Error", LENGTH_SHORT).show()
+            return
+        }
+        if (payment.paid) {
+            
findNavController().navigate(R.id.action_processPayment_to_paymentSuccess)
+            model.orderManager.restartOrUndo()
+            return
+        }
+        text_view_amount.text = "${payment.order.totalAsString} 
${payment.currency}"
+        text_view_order_reference.text = "Order Reference: ${payment.orderId}"
+        payment.talerPayUri?.let {
+            val qrcodeBitmap = makeQrCode(it)
+            qrcode.setImageBitmap(qrcodeBitmap)
+        }
+    }
+
+    private fun onPaymentCancel() {
+        paymentManager.cancelPayment()
+        findNavController().popBackStack()
+        Snackbar.make(view!!, "Payment Canceled", LENGTH_SHORT).show()
+    }
+
+}
diff --git a/app/src/main/res/drawable/selectable_background.xml 
b/app/src/main/res/drawable/selectable_background.xml
new file mode 100644
index 0000000..b82de92
--- /dev/null
+++ b/app/src/main/res/drawable/selectable_background.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android";>
+    <item android:drawable="@color/selectedBackground" 
android:state_activated="true" />
+    <item android:drawable="@android:color/transparent" />
+</selector>
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml 
b/app/src/main/res/layout/activity_main.xml
index ac9a123..5f21a42 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -20,7 +20,7 @@
             android:layout_height="match_parent"
             android:layout_gravity="start"
             android:fitsSystemWindows="true"
-            app:headerLayout="@layout/nav_header_main"
-            app:menu="@menu/activity_main_drawer"/>
+            app:menu="@menu/activity_main_drawer"
+            app:headerLayout="@layout/nav_header_main" />
 
 </androidx.drawerlayout.widget.DrawerLayout>
diff --git a/app/src/main/res/layout/content_main.xml 
b/app/src/main/res/layout/content_main.xml
index 112382e..a87cf40 100644
--- a/app/src/main/res/layout/content_main.xml
+++ b/app/src/main/res/layout/content_main.xml
@@ -9,7 +9,7 @@
         tools:showIn="@layout/app_bar_main"
         tools:context=".MainActivity">
 
-    <fragment
+    <androidx.fragment.app.FragmentContainerView
             android:id="@+id/nav_host_fragment"
             android:name="androidx.navigation.fragment.NavHostFragment"
             android:layout_width="0dp"
@@ -21,4 +21,4 @@
             app:defaultNavHost="true"
             app:navGraph="@navigation/nav_graph" />
 
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/fragment_categories.xml 
b/app/src/main/res/layout/fragment_categories.xml
new file mode 100644
index 0000000..431edd7
--- /dev/null
+++ b/app/src/main/res/layout/fragment_categories.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android";
+        xmlns:app="http://schemas.android.com/apk/res-auto";
+        android:layout_width="match_parent"
+        xmlns:tools="http://schemas.android.com/tools";
+        android:layout_marginStart="8dp"
+        android:layout_marginEnd="8dp"
+        android:layout_height="match_parent">
+
+    <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/categoriesList"
+            android:layout_width="0dp"
+            tools:listitem="@layout/list_item_category"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+    <ProgressBar
+            android:id="@+id/progressBar"
+            style="?android:attr/progressBarStyleLarge"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/fragment_config_fetcher.xml 
b/app/src/main/res/layout/fragment_config_fetcher.xml
new file mode 100644
index 0000000..e00a307
--- /dev/null
+++ b/app/src/main/res/layout/fragment_config_fetcher.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android";
+        xmlns:app="http://schemas.android.com/apk/res-auto";
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_margin="16dp">
+
+    <TextView
+            android:id="@+id/titleView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp"
+            android:text="@string/config_fetching"
+            android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+    <ProgressBar
+            android:id="@+id/progressBar"
+            style="?android:attr/progressBarStyleLarge"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/titleView" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/fragment_create_payment.xml 
b/app/src/main/res/layout/fragment_create_payment.xml
deleted file mode 100644
index c3f2e5f..0000000
--- a/app/src/main/res/layout/fragment_create_payment.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android";
-             xmlns:app="http://schemas.android.com/apk/res-auto"; 
xmlns:tools="http://schemas.android.com/tools";
-             android:layout_width="match_parent"
-             android:layout_height="match_parent"
-             tools:context=".CreatePayment">
-
-    <LinearLayout
-            android:orientation="vertical"
-            android:paddingLeft="16dp"
-            android:paddingRight="16dp"
-            android:paddingBottom="16dp"
-            android:paddingTop="16dp"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent">
-
-        <Space
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_weight="0.5"/>
-
-        <TextView
-                android:text="Payment Subject"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content" 
android:id="@+id/textView5"/>
-
-        <EditText
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:inputType="textPersonName"
-                android:text="Demo Payment"
-                android:ems="10"
-                android:layout_gravity="top"
-                android:id="@+id/edit_payment_subject"/>
-        <TextView
-                android:text="Amount (TESTKUDOS)"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:id="@+id/text_create_payment_amount_label"/>
-        <EditText
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:inputType="numberDecimal"
-                android:ems="10"
-                android:id="@+id/edit_payment_amount" android:layout_weight="0"
-                android:text="1"/>
-        <Space
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_weight="1"/>
-
-        <Button
-                android:text="Request Payment"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:id="@+id/button_request_payment"
-                android:layout_gravity="right"/>
-    </LinearLayout>
-</FrameLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_merchant_settings.xml 
b/app/src/main/res/layout/fragment_merchant_settings.xml
index b6e3707..93874b8 100644
--- a/app/src/main/res/layout/fragment_merchant_settings.xml
+++ b/app/src/main/res/layout/fragment_merchant_settings.xml
@@ -1,108 +1,136 @@
 <?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" />
+                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" />
 
-        <Space
-                android:layout_width="match_parent"
-                android:layout_height="40dp" />
+        </com.google.android.material.textfield.TextInputLayout>
 
-        <TextView
-                android:id="@+id/textView4"
-                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:text="Merchant Instance" />
+                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" />
 
-        <EditText
-                android:id="@+id/edit_settings_instance"
-                android:layout_width="match_parent"
+        </com.google.android.material.textfield.TextInputLayout>
+
+        <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/passwordView"
+                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_password"
+                app:boxBackgroundColor="@android:color/transparent"
+                app:boxBackgroundMode="outline"
+                app:endIconMode="password_toggle"
+                app:layout_constraintEnd_toStartOf="@+id/forgetPasswordButton"
+                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"
+        <Button
+                android:id="@+id/forgetPasswordButton"
+                android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:text="API Key" />
-
-        <EditText
-                android:id="@+id/edit_settings_apikey"
-                android:layout_width="match_parent"
+                android:layout_margin="16dp"
+                android:text="@string/config_forget_password"
+                android:visibility="gone"
+                app:layout_constraintBottom_toBottomOf="@+id/passwordView"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toTopOf="@+id/passwordView"
+                tools:visibility="visible" />
+
+        <CheckBox
+                android:id="@+id/savePasswordCheckBox"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
-                android:ems="10"
-                android:inputType="textPassword" />
-
-        <Space
-                android:layout_width="match_parent"
-                android:layout_height="40dp" />
+                android:layout_margin="16dp"
+                android:checked="true"
+                android:text="@string/config_save_password"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/passwordView" />
 
         <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/savePasswordCheckBox"
+                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
new file mode 100644
index 0000000..136d1e7
--- /dev/null
+++ b/app/src/main/res/layout/fragment_order.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android";
+        xmlns:app="http://schemas.android.com/apk/res-auto";
+        xmlns:tools="http://schemas.android.com/tools";
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+    <androidx.fragment.app.FragmentContainerView
+            android:id="@+id/fragment1"
+            android:name="net.taler.merchantpos.order.OrderStateFragment"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toTopOf="@+id/restartButton"
+            app:layout_constraintEnd_toStartOf="@+id/guideline1"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:layout="@layout/fragment_order_state" />
+
+    <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/guideline1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            app:layout_constraintGuide_percent="0.25" />
+
+    <androidx.fragment.app.FragmentContainerView
+            android:id="@+id/fragment2"
+            android:name="net.taler.merchantpos.order.ProductsFragment"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toTopOf="@+id/restartButton"
+            app:layout_constraintEnd_toStartOf="@+id/guideline2"
+            app:layout_constraintStart_toStartOf="@+id/guideline1"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:layout="@layout/fragment_products" />
+
+    <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/guideline2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            app:layout_constraintGuide_percent="0.75" />
+
+    <androidx.fragment.app.FragmentContainerView
+            android:id="@+id/fragment3"
+            android:name="net.taler.merchantpos.order.CategoriesFragment"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toTopOf="@+id/restartButton"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="@+id/guideline2"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:layout="@layout/fragment_categories" />
+
+    <Button
+            android:id="@+id/restartButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:backgroundTint="@color/bottomButtons"
+            android:text="@string/order_restart"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent" />
+
+    <Button
+            android:id="@+id/plusButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:minWidth="48dp"
+            android:text="+1"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/minusButton"
+            tools:ignore="HardcodedText" />
+
+    <Button
+            android:id="@+id/minusButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:minWidth="48dp"
+            android:text="-1"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/restartButton"
+            tools:ignore="HardcodedText" />
+
+    <Button
+            android:id="@+id/reconfigureButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:backgroundTint="@color/bottomButtons"
+            android:text="@string/button_reconfigure"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/plusButton" />
+
+    <Button
+            android:id="@+id/historyButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:backgroundTint="@color/bottomButtons"
+            android:text="@string/button_history"
+            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"
+            android:visibility="gone"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/historyButton" />
+
+    <Button
+            android:id="@+id/completeButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="8dp"
+            android:backgroundTint="@color/bottomButtons"
+            android:text="@string/button_complete"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintHorizontal_bias="1.0"
+            app:layout_constraintStart_toEndOf="@+id/logoutButton" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/fragment_order_state.xml 
b/app/src/main/res/layout/fragment_order_state.xml
new file mode 100644
index 0000000..0cd9c75
--- /dev/null
+++ b/app/src/main/res/layout/fragment_order_state.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android";
+        xmlns:app="http://schemas.android.com/apk/res-auto";
+        xmlns:tools="http://schemas.android.com/tools";
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+    <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/orderList"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toTopOf="@+id/totalView"
+            tools:listitem="@layout/list_item_order"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+            android:id="@+id/totalView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="8dp"
+            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/orderList"
+            app:layout_constraintVertical_bias="1.0"
+            tools:text="Total: 23.75" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/fragment_process_payment.xml 
b/app/src/main/res/layout/fragment_process_payment.xml
index 3f1764e..89e88d8 100644
--- a/app/src/main/res/layout/fragment_process_payment.xml
+++ b/app/src/main/res/layout/fragment_process_payment.xml
@@ -4,7 +4,7 @@
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:layout_margin="15dp"
-             tools:context=".ProcessPayment" android:id="@+id/frameLayout2">
+             tools:context=".payment.ProcessPaymentFragment" 
android:id="@+id/frameLayout2">
 
     <androidx.constraintlayout.widget.ConstraintLayout
             android:layout_width="match_parent"
@@ -25,7 +25,7 @@
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintTop_toTopOf="parent" 
android:layout_marginTop="32dp" />
         <TextView
-                android:text="Please scan QR Code or use NFC to pay"
+                android:text="@string/payment_intro_nfc"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:id="@+id/textView2"
diff --git a/app/src/main/res/layout/fragment_products.xml 
b/app/src/main/res/layout/fragment_products.xml
new file mode 100644
index 0000000..909fece
--- /dev/null
+++ b/app/src/main/res/layout/fragment_products.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android";
+        xmlns:app="http://schemas.android.com/apk/res-auto";
+        android:layout_width="match_parent"
+        xmlns:tools="http://schemas.android.com/tools";
+        android:layout_height="match_parent">
+
+    <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/productsList"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            tools:listitem="@layout/list_item_product" />
+
+    <ProgressBar
+            android:id="@+id/progressBar"
+            style="?android:attr/progressBarStyleLarge"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/list_item_category.xml 
b/app/src/main/res/layout/list_item_category.xml
new file mode 100644
index 0000000..496b96b
--- /dev/null
+++ b/app/src/main/res/layout/list_item_category.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android";
+        xmlns:app="http://schemas.android.com/apk/res-auto";
+        xmlns:tools="http://schemas.android.com/tools";
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+    <Button
+            android:id="@+id/button"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="Snacks" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/list_item_order.xml 
b/app/src/main/res/layout/list_item_order.xml
new file mode 100644
index 0000000..03b15a8
--- /dev/null
+++ b/app/src/main/res/layout/list_item_order.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android";
+        xmlns:app="http://schemas.android.com/apk/res-auto";
+        xmlns:tools="http://schemas.android.com/tools";
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@drawable/selectable_background"
+        android:minHeight="48dp"
+        android:padding="8dp">
+
+    <TextView
+            android:id="@+id/quantity"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:gravity="end"
+            android:minWidth="24dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="@+id/name"
+            app:layout_constraintVertical_bias="0.0"
+            tools:text="31" />
+
+    <TextView
+            android:id="@+id/name"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:layout_marginEnd="8dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/price"
+            app:layout_constraintStart_toEndOf="@+id/quantity"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="An order product item that in some cases could have a 
very long name" />
+
+    <TextView
+            android:id="@+id/price"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="@+id/name"
+            app:layout_constraintVertical_bias="0.0"
+            tools:text="23.42" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/list_item_product.xml 
b/app/src/main/res/layout/list_item_product.xml
new file mode 100644
index 0000000..51178a5
--- /dev/null
+++ b/app/src/main/res/layout/list_item_product.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView 
xmlns:android="http://schemas.android.com/apk/res/android";
+        xmlns:app="http://schemas.android.com/apk/res-auto";
+        xmlns:tools="http://schemas.android.com/tools";
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="4dp"
+        android:clickable="true"
+        android:focusable="true"
+        app:cardUseCompatPadding="true">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:padding="8dp">
+
+        <TextView
+                android:id="@+id/name"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:textColor="?android:textColorPrimary"
+                android:textStyle="bold"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                tools:text="Steak and two Eggs" />
+
+        <TextView
+                android:id="@+id/price"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp"
+                android:textColor="?android:textColorSecondary"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/name"
+                tools:text="7.95" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</com.google.android.material.card.MaterialCardView>
\ No newline at end of file
diff --git a/app/src/main/res/menu/activity_main_drawer.xml 
b/app/src/main/res/menu/activity_main_drawer.xml
index 9ce4a06..06f9e5c 100644
--- a/app/src/main/res/menu/activity_main_drawer.xml
+++ b/app/src/main/res/menu/activity_main_drawer.xml
@@ -1,20 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android";
-      xmlns:tools="http://schemas.android.com/tools";
-      tools:showIn="navigation_view">
+        xmlns:tools="http://schemas.android.com/tools";
+        tools:showIn="navigation_view">
 
     <group android:checkableBehavior="single">
         <item
-                android:id="@+id/nav_home"
+                android:id="@+id/nav_order"
                 android:icon="@drawable/ic_move_money_24dp"
-                android:title="@string/menu_home"/>
+                android:title="Order" />
         <item
                 android:id="@+id/nav_history"
                 android:icon="@drawable/ic_history_black_24dp"
-                android:title="History"/>
+                android:title="History" />
         <item
                 android:id="@+id/nav_settings"
                 android:icon="@drawable/ic_menu_manage"
-                android:title="Settings"/>
+                android:title="Settings" />
     </group>
 </menu>
diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml
deleted file mode 100644
index d579f6f..0000000
--- a/app/src/main/res/menu/main.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android";
-      xmlns:app="http://schemas.android.com/apk/res-auto";>
-    <item android:id="@+id/action_settings"
-          android:title="@string/action_settings"
-          android:orderInCategory="100"
-          app:showAsAction="never"/>
-</menu>
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml 
b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index bbd3e02..0000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android";>
-    <background android:drawable="@drawable/ic_launcher_background"/>
-    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
-</adaptive-icon>
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml 
b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index bbd3e02..0000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android";>
-    <background android:drawable="@drawable/ic_launcher_background"/>
-    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
-</adaptive-icon>
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png 
b/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 898f3ed..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png 
b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644
index dffca36..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null 
differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png 
b/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 64ba76f..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png 
b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644
index dae5e08..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null 
differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png 
b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index e5ed465..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null 
differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png 
b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644
index 14ed0af..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and 
/dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png 
b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index b0907ca..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null 
differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png 
b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644
index d8ae031..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and 
/dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png 
b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 2c18de9..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null 
differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png 
b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644
index beed3cd..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and 
/dev/null differ
diff --git a/app/src/main/res/navigation/nav_graph.xml 
b/app/src/main/res/navigation/nav_graph.xml
index 3a5b470..dc4015f 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -1,30 +1,82 @@
 <?xml version="1.0" encoding="utf-8"?>
 <navigation 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:id="@+id/nav_graph"
-            app:startDestination="@id/createPayment">
+        xmlns:app="http://schemas.android.com/apk/res-auto";
+        xmlns:tools="http://schemas.android.com/tools";
+        android:id="@+id/nav_graph"
+        app:startDestination="@id/order">
 
-    <fragment android:id="@+id/createPayment" 
android:name="net.taler.merchantpos.CreatePayment"
-              android:label="Request Payment" 
tools:layout="@layout/fragment_create_payment">
-        <action android:id="@+id/action_createPayment_to_processPayment" 
app:destination="@id/processPayment"/>
+    <fragment
+            android:id="@+id/order"
+            android:name="net.taler.merchantpos.order.OrderFragment"
+            android:label="Order"
+            tools:layout="@layout/fragment_order">
+        <action
+                android:id="@+id/action_order_to_processPayment"
+                app:destination="@id/processPayment" />
+        <action
+                android:id="@+id/action_order_to_merchantHistory"
+                app:destination="@id/merchantHistory" />
+        <action
+                android:id="@+id/action_order_to_merchantSettings"
+                app:destination="@id/merchantSettings" />
     </fragment>
-    <fragment android:id="@+id/processPayment" 
android:name="net.taler.merchantpos.ProcessPayment"
-              android:label="Payment Prompt" 
tools:layout="@layout/fragment_process_payment">
+
+    <fragment
+            android:id="@+id/processPayment"
+            android:name="net.taler.merchantpos.payment.ProcessPaymentFragment"
+            android:label="Payment Prompt"
+            tools:layout="@layout/fragment_process_payment">
         <action
                 android:id="@+id/action_processPayment_to_paymentSuccess"
                 app:destination="@id/paymentSuccess"
-                app:popUpTo="@id/createPayment"/>
+                app:popUpTo="@id/order" />
+    </fragment>
+
+    <fragment
+            android:id="@+id/merchantHistory"
+            android:name="net.taler.merchantpos.MerchantHistory"
+            android:label="Payment History"
+            tools:layout="@layout/fragment_merchant_history" />
+
+    <fragment
+            android:id="@+id/merchantSettings"
+            android:name="net.taler.merchantpos.config.MerchantConfigFragment"
+            android:label="Merchant Settings"
+            tools:layout="@layout/fragment_merchant_settings" />
+
+    <fragment
+            android:id="@+id/configFetcher"
+            android:name="net.taler.merchantpos.config.ConfigFetcherFragment"
+            android:label="Fetching Configuration"
+            tools:layout="@layout/fragment_config_fetcher">
+        <action
+                android:id="@+id/action_configFetcher_to_merchantSettings"
+                app:destination="@id/merchantSettings"
+                app:popUpToInclusive="true" />
+        <action
+                android:id="@+id/action_configFetcher_to_order"
+                app:destination="@id/order"
+                app:launchSingleTop="true"
+                app:popUpTo="@+id/order" />
     </fragment>
-    <fragment android:id="@+id/merchantHistory" 
android:name="net.taler.merchantpos.MerchantHistory"
-              android:label="Payment History" 
tools:layout="@layout/fragment_merchant_history"/>
-    <action android:id="@+id/action_global_merchantHistory" 
app:destination="@id/merchantHistory"/>
-    <action android:id="@+id/action_global_createPayment" 
app:destination="@id/createPayment"/>
-    <fragment android:id="@+id/merchantSettings" 
android:name="net.taler.merchantpos.MerchantSettings"
-              android:label="Merchant Settings" 
tools:layout="@layout/fragment_merchant_settings"/>
-    <action android:id="@+id/action_global_merchantSettings" 
app:destination="@id/merchantSettings"/>
+
     <fragment
             android:id="@+id/paymentSuccess"
-            android:name="net.taler.merchantpos.PaymentSuccess"
+            android:name="net.taler.merchantpos.payment.PaymentSuccessFragment"
             android:label="Payment Received"
             tools:layout="@layout/fragment_payment_success" />
-</navigation>
\ No newline at end of file
+
+    <action
+            android:id="@+id/action_global_order"
+            app:destination="@id/order" />
+    <action
+            android:id="@+id/action_global_merchantHistory"
+            app:destination="@id/merchantHistory" />
+    <action
+            android:id="@+id/action_global_merchantSettings"
+            app:destination="@id/merchantSettings" />
+    <action
+            android:id="@+id/action_global_configFetcher"
+            app:destination="@id/configFetcher" />
+
+</navigation>
diff --git a/app/src/main/res/values-night/colors.xml 
b/app/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000..47721b4
--- /dev/null
+++ b/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="selectedBackground">#363636</color>
+</resources>
diff --git a/app/src/main/res/values-v21/styles.xml 
b/app/src/main/res/values-v21/styles.xml
deleted file mode 100644
index e546804..0000000
--- a/app/src/main/res/values-v21/styles.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<resources>
-    <style name="AppTheme.NoActionBar">
-        <item name="windowActionBar">false</item>
-        <item name="windowNoTitle">true</item>
-        <item name="android:statusBarColor">@android:color/transparent</item>
-    </style>
-</resources>
diff --git a/app/src/main/res/values/colors.xml 
b/app/src/main/res/values/colors.xml
index cbe99a2..3ed8874 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -3,4 +3,8 @@
     <color name="colorPrimary">#795548</color>
     <color name="colorPrimaryDark">#5D4037</color>
     <color name="colorAccent">#FFEB3B</color>
+
+    <color name="selectedBackground">#DADADA</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 f3103af..1a1c4ca 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -14,6 +14,27 @@
     <string name="menu_share">Share</string>
     <string name="menu_send">Send</string>
 
-    <!-- TODO: Remove or change this placeholder text -->
-    <string name="hello_blank_fragment">Hello blank fragment</string>
+    <string name="order_total">Total: %1$.2f</string>
+    <string name="order_restart">Restart</string>
+    <string name="order_undo">Undo</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>
+    <string name="config_fetching">Fetching Configuration…</string>
+    <string name="config_save_password">Remember Password</string>
+    <string name="config_forget_password">Forget</string>
+
+    <string name="payment_intro_nfc">Please scan QR Code or use NFC to 
pay</string>
+    <string name="payment_intro">Please scan QR Code to pay</string>
+
 </resources>
diff --git a/app/src/main/res/values/styles.xml 
b/app/src/main/res/values/styles.xml
index 1eb4629..4445a01 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,16 +1,21 @@
 <resources>
     <!-- Base application theme. -->
-    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+    <style name="AppTheme" 
parent="Theme.MaterialComponents.DayNight.DarkActionBar">
         <!-- Customize your theme here. -->
         <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorOnPrimary">@android:color/white</item>
         <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
         <item name="colorAccent">@color/colorAccent</item>
     </style>
+
     <style name="AppTheme.NoActionBar">
         <item name="windowActionBar">false</item>
         <item name="windowNoTitle">true</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
     </style>
-    <style name="AppTheme.AppBarOverlay" 
parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
-    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/>
+
+    <style name="AppTheme.AppBarOverlay" 
parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
+
+    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" 
/>
 
 </resources>
diff --git a/build.gradle b/build.gradle
index 4420c59..dfab280 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,7 @@ buildscript {
         
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.5.0'
+        classpath 'com.android.tools.build:gradle:3.5.3'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files

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



reply via email to

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