diff options
author | Torsten Grote <t@grobox.de> | 2020-03-18 14:24:41 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-03-18 14:24:41 -0300 |
commit | a4796ec47d89a851b260b6fc195494547208a025 (patch) | |
tree | d2941b68ff2ce22c523e7aa634965033b1100560 /merchant-terminal/src/main/java/net/taler/merchantpos/config | |
download | taler-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')
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 + } +} |