summaryrefslogtreecommitdiff
path: root/merchant-terminal/src/main/java/net/taler/merchantpos/config
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2020-03-18 14:24:41 -0300
committerTorsten Grote <t@grobox.de>2020-03-18 14:24:41 -0300
commita4796ec47d89a851b260b6fc195494547208a025 (patch)
treed2941b68ff2ce22c523e7aa634965033b1100560 /merchant-terminal/src/main/java/net/taler/merchantpos/config
downloadtaler-android-a4796ec47d89a851b260b6fc195494547208a025.tar.gz
taler-android-a4796ec47d89a851b260b6fc195494547208a025.tar.bz2
taler-android-a4796ec47d89a851b260b6fc195494547208a025.zip
Merge all three apps into one repository
Diffstat (limited to 'merchant-terminal/src/main/java/net/taler/merchantpos/config')
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt66
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt181
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt47
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt165
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt41
5 files changed, 500 insertions, 0 deletions
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
new file mode 100644
index 0000000..c370e33
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt
@@ -0,0 +1,66 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.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
+import net.taler.merchantpos.config.ConfigFetcherFragmentDirections.Companion.actionConfigFetcherToMerchantSettings
+import net.taler.merchantpos.config.ConfigFetcherFragmentDirections.Companion.actionConfigFetcherToOrder
+import net.taler.merchantpos.navigate
+
+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.fetchConfig(configManager.config, false)
+ configManager.configUpdateResult.observe(viewLifecycleOwner, Observer { result ->
+ when (result) {
+ null -> return@Observer
+ is ConfigUpdateResult.Error -> onNetworkError(result.msg)
+ is ConfigUpdateResult.Success -> {
+ actionConfigFetcherToOrder().navigate(findNavController())
+ }
+ }
+ })
+ }
+
+ private fun onNetworkError(msg: String) {
+ Snackbar.make(view!!, msg, LENGTH_SHORT).show()
+ actionConfigFetcherToMerchantSettings().navigate(findNavController())
+ }
+
+}
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
new file mode 100644
index 0000000..edb8059
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
@@ -0,0 +1,181 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.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 net.taler.merchantpos.R
+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"
+
+internal const val CONFIG_URL_DEMO = "https://docs.taler.net/_static/sample-pos-config.json"
+internal const val CONFIG_USERNAME_DEMO = ""
+internal const val CONFIG_PASSWORD_DEMO = ""
+
+private val TAG = ConfigManager::class.java.simpleName
+
+interface ConfigurationReceiver {
+ /**
+ * Returns null if the configuration was valid, or a error string for user display otherwise.
+ */
+ suspend fun onConfigurationReceived(json: JSONObject, currency: String): String?
+}
+
+class ConfigManager(
+ private val 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, CONFIG_URL_DEMO)!!,
+ username = prefs.getString(SETTINGS_USERNAME, CONFIG_USERNAME_DEMO)!!,
+ password = prefs.getString(SETTINGS_PASSWORD, CONFIG_PASSWORD_DEMO)!!
+ )
+ var merchantConfig: MerchantConfig? = null
+ private set
+
+ private val mConfigUpdateResult = MutableLiveData<ConfigUpdateResult>()
+ val configUpdateResult: LiveData<ConfigUpdateResult> = mConfigUpdateResult
+
+ fun addConfigurationReceiver(receiver: ConfigurationReceiver) {
+ configurationReceivers.add(receiver)
+ }
+
+ @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)
+ }
+
+ @UiThread
+ 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)
+ val msg = context.getString(R.string.config_error_malformed)
+ mConfigUpdateResult.value = ConfigUpdateResult.Error(msg)
+ 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.Default) {
+ val currency = json.getString("currency")
+
+ for (receiver in configurationReceivers) {
+ val result = try {
+ receiver.onConfigurationReceived(configJson, currency)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error handling configuration by ${receiver::class.java.simpleName}", e)
+ context.getString(R.string.config_error_unknown)
+ }
+ if (result != null) { // error
+ mConfigUpdateResult.postValue(ConfigUpdateResult.Error(result))
+ return@launch
+ }
+ }
+ newConfig?.let {
+ config = it
+ saveConfig(it)
+ }
+ this@ConfigManager.merchantConfig = merchantConfig.copy(currency = currency)
+ mConfigUpdateResult.postValue(ConfigUpdateResult.Success(currency))
+ }
+
+ 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()
+ }
+
+ @UiThread
+ private fun onNetworkError(it: VolleyError?) {
+ val msg = context.getString(
+ if (it?.networkResponse?.statusCode == 401) R.string.config_auth_error
+ else R.string.config_error_network
+ )
+ mConfigUpdateResult.value = ConfigUpdateResult.Error(msg)
+ }
+
+}
+
+sealed class ConfigUpdateResult {
+ data class Error(val msg: String) : ConfigUpdateResult()
+ data class Success(val currency: String) : ConfigUpdateResult()
+}
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
new file mode 100644
index 0000000..2050e28
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
@@ -0,0 +1,47 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.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?
+) {
+ fun urlFor(endpoint: String, params: Map<String, String>?): String {
+ val uriBuilder = Uri.parse(baseUrl).buildUpon()
+ uriBuilder.appendPath(endpoint)
+ params?.forEach {
+ uriBuilder.appendQueryParameter(it.key, it.value)
+ }
+ return uriBuilder.toString()
+ }
+}
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
new file mode 100644
index 0000000..aad1c93
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt
@@ -0,0 +1,165 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.config
+
+import android.net.Uri
+import android.os.Bundle
+import android.text.method.LinkMovementMethod
+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.BaseTransientBottomBar.LENGTH_LONG
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.android.synthetic.main.fragment_merchant_config.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.config.MerchantConfigFragmentDirections.Companion.actionSettingsToOrder
+import net.taler.merchantpos.navigate
+import net.taler.merchantpos.topSnackbar
+
+/**
+ * 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_config, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ configUrlView.editText!!.setOnFocusChangeListener { _, hasFocus ->
+ if (!hasFocus) checkForUrlCredentials()
+ }
+ okButton.setOnClickListener {
+ checkForUrlCredentials()
+ val inputUrl = configUrlView.editText!!.text
+ val url = if (inputUrl.startsWith("http")) {
+ inputUrl.toString()
+ } else {
+ "https://$inputUrl".also { configUrlView.editText!!.setText(it) }
+ }
+ progressBar.visibility = VISIBLE
+ okButton.visibility = INVISIBLE
+ val config = Config(
+ configUrl = url,
+ username = usernameView.editText!!.text.toString(),
+ password = passwordView.editText!!.text.toString()
+ )
+ configManager.fetchConfig(config, true, savePasswordCheckBox.isChecked)
+ configManager.configUpdateResult.observe(viewLifecycleOwner, Observer { result ->
+ if (onConfigUpdate(result)) {
+ configManager.configUpdateResult.removeObservers(viewLifecycleOwner)
+ }
+ })
+ }
+ forgetPasswordButton.setOnClickListener {
+ configManager.forgetPassword()
+ passwordView.editText!!.text = null
+ forgetPasswordButton.visibility = GONE
+ }
+ configDocsView.movementMethod = LinkMovementMethod.getInstance()
+ updateView(savedInstanceState == null)
+ }
+
+ 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(isInitialization: Boolean = false) {
+ val config = configManager.config
+ configUrlView.editText!!.setText(
+ if (isInitialization && config.configUrl.isBlank()) CONFIG_URL_DEMO
+ else config.configUrl
+ )
+ usernameView.editText!!.setText(
+ if (isInitialization && config.username.isBlank()) CONFIG_USERNAME_DEMO
+ else config.username
+ )
+ passwordView.editText!!.setText(
+ if (isInitialization && config.password.isBlank()) CONFIG_PASSWORD_DEMO
+ else config.password
+ )
+ forgetPasswordButton.visibility = if (config.hasPassword()) VISIBLE else GONE
+ }
+
+ private fun checkForUrlCredentials() {
+ val text = configUrlView.editText!!.text.toString()
+ Uri.parse(text)?.userInfo?.let { userInfo ->
+ if (userInfo.contains(':')) {
+ val (user, pass) = userInfo.split(':')
+ val strippedUrl = text.replace("${userInfo}@", "")
+ configUrlView.editText!!.setText(strippedUrl)
+ usernameView.editText!!.setText(user)
+ passwordView.editText!!.setText(pass)
+ }
+ }
+ }
+
+ /**
+ * Processes updated config and returns true, if observer can be removed.
+ */
+ private fun onConfigUpdate(result: ConfigUpdateResult?) = when (result) {
+ null -> false
+ is ConfigUpdateResult.Error -> {
+ onError(result.msg)
+ true
+ }
+ is ConfigUpdateResult.Success -> {
+ onConfigReceived(result.currency)
+ true
+ }
+ }
+
+ private fun onConfigReceived(currency: String) {
+ onResultReceived()
+ updateView()
+ topSnackbar(view!!, getString(R.string.config_changed, currency), LENGTH_LONG)
+ actionSettingsToOrder().navigate(findNavController())
+ }
+
+ private fun onError(msg: String) {
+ onResultReceived()
+ Snackbar.make(view!!, msg, LENGTH_LONG).show()
+ }
+
+ private fun onResultReceived() {
+ progressBar.visibility = INVISIBLE
+ okButton.visibility = VISIBLE
+ }
+
+}
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt
new file mode 100644
index 0000000..8d95378
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt
@@ -0,0 +1,41 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.config
+
+
+import android.util.ArrayMap
+import com.android.volley.Response
+import com.android.volley.toolbox.JsonObjectRequest
+import org.json.JSONObject
+
+class MerchantRequest(
+ method: Int,
+ private val merchantConfig: MerchantConfig,
+ endpoint: String,
+ params: Map<String, String>?,
+ jsonRequest: JSONObject?,
+ listener: Response.Listener<JSONObject>,
+ errorListener: Response.ErrorListener
+) :
+ JsonObjectRequest(method, merchantConfig.urlFor(endpoint, params), jsonRequest, listener, errorListener) {
+
+ override fun getHeaders(): MutableMap<String, String> {
+ val headerMap = ArrayMap<String, String>()
+ headerMap["Authorization"] = "ApiKey " + merchantConfig.apiKey
+ return headerMap
+ }
+}