gnunet-svn
[Top][All Lists]
Advanced

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

[taler-taler-android] branch master updated (ed3f864 -> e71c580)


From: gnunet
Subject: [taler-taler-android] branch master updated (ed3f864 -> e71c580)
Date: Thu, 27 Aug 2020 22:02:34 +0200

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

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

    from ed3f864  [wallet] include JSON error details in user-facing error 
message
     new 53d99e4  [cashier] don't crash on unexpected network input
     new e71c580  [wallet] fulfillment_url is no longer required in contract 
terms

The 2 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:
 cashier/build.gradle                               |  16 ++-
 .../main/java/net/taler/cashier/BalanceFragment.kt |  11 +-
 .../src/main/java/net/taler/cashier/HttpHelper.kt  |  53 ++++----
 .../main/java/net/taler/cashier/MainActivity.kt    |   7 +-
 .../main/java/net/taler/cashier/MainViewModel.kt   | 132 ++++---------------
 .../src/main/java/net/taler/cashier/Response.kt    |  86 +++++++++++++
 .../main/java/net/taler/cashier/config/Config.kt   |  40 +++---
 .../taler/cashier/{ => config}/ConfigFragment.kt   |  19 +--
 .../java/net/taler/cashier/config/ConfigManager.kt | 141 +++++++++++++++++++++
 .../net/taler/cashier/withdraw/WithdrawManager.kt  |  56 ++++----
 cashier/src/main/res/navigation/nav_graph.xml      |   2 +-
 merchant-lib/build.gradle                          |   2 +-
 .../main/java/net/taler/common/ContractTerms.kt    |   4 +-
 .../taler/wallet/payment/PaymentResponsesTest.kt   |  78 ++++++++++++
 14 files changed, 445 insertions(+), 202 deletions(-)
 create mode 100644 cashier/src/main/java/net/taler/cashier/Response.kt
 copy merchant-lib/src/main/java/net/taler/merchantlib/Refunds.kt => 
cashier/src/main/java/net/taler/cashier/config/Config.kt (59%)
 rename cashier/src/main/java/net/taler/cashier/{ => config}/ConfigFragment.kt 
(89%)
 create mode 100644 
cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt
 create mode 100644 
wallet/src/test/java/net/taler/wallet/payment/PaymentResponsesTest.kt

diff --git a/cashier/build.gradle b/cashier/build.gradle
index 916758b..4defd7a 100644
--- a/cashier/build.gradle
+++ b/cashier/build.gradle
@@ -14,10 +14,13 @@
  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
-apply plugin: 'androidx.navigation.safeargs.kotlin'
+plugins {
+    id "com.android.application"
+    id "kotlin-android"
+    id "kotlin-android-extensions"
+    id "kotlinx-serialization"
+    id "androidx.navigation.safeargs.kotlin"
+}
 
 android {
     compileSdkVersion 29
@@ -66,8 +69,9 @@ dependencies {
     implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
     implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
 
-    // https://github.com/square/okhttp/releases
-    implementation "com.squareup.okhttp3:okhttp:3.12.12"
+    implementation "io.ktor:ktor-client:$ktor_version"
+    implementation "io.ktor:ktor-client-okhttp:$ktor_version"
+    implementation "io.ktor:ktor-client-serialization-jvm:$ktor_version"
 
     testImplementation 'junit:junit:4.13'
 
diff --git a/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt 
b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt
index d899e7d..cdfa142 100644
--- a/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt
+++ b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt
@@ -47,6 +47,7 @@ sealed class BalanceResult {
 class BalanceFragment : Fragment() {
 
     private val viewModel: MainViewModel by activityViewModels()
+    private val configManager by lazy { viewModel.configManager}
     private val withdrawManager by lazy { viewModel.withdrawManager }
 
     override fun onCreateView(
@@ -78,7 +79,7 @@ class BalanceFragment : Fragment() {
                 true
             } else false
         }
-        viewModel.currency.observe(viewLifecycleOwner, Observer { currency ->
+        configManager.currency.observe(viewLifecycleOwner, Observer { currency 
->
             currencyView.text = currency
         })
         confirmWithdrawalButton.setOnClickListener { 
onAmountConfirmed(getAmountFromView()) }
@@ -87,7 +88,7 @@ class BalanceFragment : Fragment() {
     override fun onStart() {
         super.onStart()
         // update balance if there's a config
-        if (viewModel.hasConfig()) {
+        if (configManager.hasConfig()) {
             viewModel.getBalance()
         }
     }
@@ -107,12 +108,12 @@ class BalanceFragment : Fragment() {
 
     override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
         R.id.action_reconfigure -> {
-            findNavController().navigate(viewModel.configDestination)
+            findNavController().navigate(configManager.configDestination)
             true
         }
         R.id.action_lock -> {
             viewModel.lock()
-            findNavController().navigate(viewModel.configDestination)
+            findNavController().navigate(configManager.configDestination)
             true
         }
         else -> super.onOptionsItemSelected(item)
@@ -148,7 +149,7 @@ class BalanceFragment : Fragment() {
 
     private fun getAmountFromView(): Amount {
         val str = amountView.editText!!.text.toString()
-        val currency = viewModel.currency.value!!
+        val currency = configManager.currency.value!!
         if (str.isBlank()) return Amount.zero(currency)
         return Amount.fromString(currency, str)
     }
diff --git a/cashier/src/main/java/net/taler/cashier/HttpHelper.kt 
b/cashier/src/main/java/net/taler/cashier/HttpHelper.kt
index 003c2f6..fd48b2d 100644
--- a/cashier/src/main/java/net/taler/cashier/HttpHelper.kt
+++ b/cashier/src/main/java/net/taler/cashier/HttpHelper.kt
@@ -18,12 +18,15 @@ package net.taler.cashier
 
 import android.util.Log
 import androidx.annotation.WorkerThread
+import net.taler.cashier.config.Config
+import okhttp3.Authenticator
 import okhttp3.Credentials
-import okhttp3.MediaType
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
 import okhttp3.OkHttpClient
 import okhttp3.Request
-import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
 import okhttp3.Response
+import okhttp3.Route
 import org.json.JSONException
 import org.json.JSONObject
 
@@ -47,23 +50,23 @@ object HttpHelper {
             Log.e(TAG, "Error retrieving $url", e)
             return HttpJsonResult.Error(0)
         }
-        return if (response.code() == 200 && response.body() != null) {
-            val jsonObject = JSONObject(response.body()!!.string())
+        return if (response.code == 200 && response.body != null) {
+            val jsonObject = JSONObject(response.body!!.string())
             HttpJsonResult.Success(jsonObject)
         } else {
-            Log.e(TAG, "Received status ${response.code()} from $url expected 
200")
-            HttpJsonResult.Error(response.code(), getErrorBody(response))
+            Log.e(TAG, "Received status ${response.code} from $url expected 
200")
+            HttpJsonResult.Error(response.code, getErrorBody(response))
         }
     }
 
-    private val MEDIA_TYPE_JSON = MediaType.parse("$MIME_TYPE_JSON; 
charset=utf-8")
+    private val MEDIA_TYPE_JSON = "$MIME_TYPE_JSON; 
charset=utf-8".toMediaTypeOrNull()
 
     @WorkerThread
     fun makeJsonPostRequest(url: String, body: JSONObject, config: Config): 
HttpJsonResult {
         val request = Request.Builder()
             .addHeader("Accept", MIME_TYPE_JSON)
             .url(url)
-            .post(RequestBody.create(MEDIA_TYPE_JSON, body.toString()))
+            .post(body.toString().toRequestBody(MEDIA_TYPE_JSON))
             .build()
         val response = try {
             getHttpClient(config.username, config.password)
@@ -73,31 +76,33 @@ object HttpHelper {
             Log.e(TAG, "Error retrieving $url", e)
             return HttpJsonResult.Error(0)
         }
-        return if (response.code() == 200 && response.body() != null) {
-            val jsonObject = JSONObject(response.body()!!.string())
+        return if (response.code == 200 && response.body != null) {
+            val jsonObject = JSONObject(response.body!!.string())
             HttpJsonResult.Success(jsonObject)
         } else {
-            Log.e(TAG, "Received status ${response.code()} from $url expected 
200")
-            HttpJsonResult.Error(response.code(), getErrorBody(response))
+            Log.e(TAG, "Received status ${response.code} from $url expected 
200")
+            HttpJsonResult.Error(response.code, getErrorBody(response))
         }
     }
 
     private fun getHttpClient(username: String, password: String) =
-        OkHttpClient.Builder().authenticator { _, response ->
-            val credential = Credentials.basic(username, password)
-            if (credential == response.request().header("Authorization")) {
-                // If we already failed with these credentials, don't retry
-                return@authenticator null
+        OkHttpClient.Builder().authenticator(object : Authenticator {
+            override fun authenticate(route: Route?, response: Response): 
Request? {
+                val credential = Credentials.basic(username, password)
+                if (credential == response.request.header("Authorization")) {
+                    // If we already failed with these credentials, don't retry
+                    return null
+                }
+                return response
+                    .request
+                    .newBuilder()
+                    .header("Authorization", credential)
+                    .build()
             }
-            response
-                .request()
-                .newBuilder()
-                .header("Authorization", credential)
-                .build()
-        }.build()
+        }).build()
 
     private fun getErrorBody(response: Response): String? {
-        val body = response.body()?.string() ?: return null
+        val body = response.body?.string() ?: return null
         Log.e(TAG, "Response body: $body")
         return try {
             val json = JSONObject(body)
diff --git a/cashier/src/main/java/net/taler/cashier/MainActivity.kt 
b/cashier/src/main/java/net/taler/cashier/MainActivity.kt
index 0559b38..ae31be5 100644
--- a/cashier/src/main/java/net/taler/cashier/MainActivity.kt
+++ b/cashier/src/main/java/net/taler/cashier/MainActivity.kt
@@ -30,6 +30,7 @@ import kotlinx.android.synthetic.main.activity_main.*
 class MainActivity : AppCompatActivity() {
 
     private val viewModel: MainViewModel by viewModels()
+    private val configManager by lazy { viewModel.configManager}
     private lateinit var nav: NavController
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -43,13 +44,13 @@ class MainActivity : AppCompatActivity() {
 
     override fun onStart() {
         super.onStart()
-        if (!viewModel.hasConfig()) {
-            nav.navigate(viewModel.configDestination)
+        if (!configManager.hasConfig()) {
+            nav.navigate(configManager.configDestination)
         }
     }
 
     override fun onBackPressed() {
-        if (!viewModel.hasConfig() && nav.currentDestination?.id == 
R.id.configFragment) {
+        if (!configManager.hasConfig() && nav.currentDestination?.id == 
R.id.configFragment) {
             // we are in the configuration screen and need a config to continue
             val intent = Intent(ACTION_MAIN).apply {
                 addCategory(CATEGORY_HOME)
diff --git a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt 
b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
index a25467b..95d94d7 100644
--- a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
+++ b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
@@ -16,126 +16,54 @@
 
 package net.taler.cashier
 
-import android.annotation.SuppressLint
 import android.app.Application
 import android.util.Log
-import androidx.annotation.UiThread
-import androidx.annotation.WorkerThread
 import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.viewModelScope
-import androidx.security.crypto.EncryptedSharedPreferences
-import 
androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV
-import 
androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
-import androidx.security.crypto.MasterKeys
-import androidx.security.crypto.MasterKeys.AES256_GCM_SPEC
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.okhttp.OkHttp
+import io.ktor.client.features.json.JsonFeature
+import io.ktor.client.features.json.serializer.KotlinxSerializer
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
+import kotlinx.serialization.json.Json
 import net.taler.cashier.HttpHelper.makeJsonGetRequest
+import net.taler.cashier.config.ConfigManager
 import net.taler.cashier.withdraw.WithdrawManager
-import net.taler.common.getIncompatibleStringOrNull
 import net.taler.common.isOnline
 import net.taler.lib.common.Amount
 import net.taler.lib.common.AmountParserException
-import net.taler.lib.common.Version
 
 private val TAG = MainViewModel::class.java.simpleName
 
-private val VERSION_BANK = Version(0, 0, 0)
-private const val PREF_NAME = "net.taler.cashier.prefs"
-private const val PREF_KEY_BANK_URL = "bankUrl"
-private const val PREF_KEY_USERNAME = "username"
-private const val PREF_KEY_PASSWORD = "password"
-private const val PREF_KEY_CURRENCY = "currency"
-
 class MainViewModel(private val app: Application) : AndroidViewModel(app) {
 
-    val configDestination = 
ConfigFragmentDirections.actionGlobalConfigFragment()
-
-    private val masterKeyAlias = MasterKeys.getOrCreate(AES256_GCM_SPEC)
-    private val prefs = EncryptedSharedPreferences.create(
-        PREF_NAME, masterKeyAlias, app, AES256_SIV, AES256_GCM
-    )
-
-    internal var config = Config(
-        bankUrl = prefs.getString(PREF_KEY_BANK_URL, "")!!,
-        username = prefs.getString(PREF_KEY_USERNAME, "")!!,
-        password = prefs.getString(PREF_KEY_PASSWORD, "")!!
-    )
-
-    private val mCurrency = MutableLiveData<String>(
-        prefs.getString(PREF_KEY_CURRENCY, null)
-    )
-    internal val currency: LiveData<String> = mCurrency
-
-    private val mConfigResult = MutableLiveData<ConfigResult>()
-    val configResult: LiveData<ConfigResult> = mConfigResult
+    private val httpClient = HttpClient(OkHttp) {
+        engine {
+            config {
+                retryOnConnectionFailure(true)
+            }
+        }
+        install(JsonFeature) {
+            serializer = KotlinxSerializer(
+                Json {
+                    ignoreUnknownKeys = true
+                }
+            )
+        }
+    }
+    val configManager = ConfigManager(app, viewModelScope, httpClient)
 
     private val mBalance = MutableLiveData<BalanceResult>()
     val balance: LiveData<BalanceResult> = mBalance
 
     internal val withdrawManager = WithdrawManager(app, this)
 
-    fun hasConfig() = config.bankUrl.isNotEmpty()
-            && config.username.isNotEmpty()
-            && config.password.isNotEmpty()
-
-    /**
-     * Start observing [configResult] after calling this to get the result 
async.
-     * Warning: Ignore null results that are used to reset old results.
-     */
-    @UiThread
-    fun checkAndSaveConfig(config: Config) {
-        mConfigResult.value = null
-        viewModelScope.launch(Dispatchers.IO) {
-            val url = "${config.bankUrl}/config"
-            Log.d(TAG, "Checking config: $url")
-            val result = when (val response = makeJsonGetRequest(url, config)) 
{
-                is HttpJsonResult.Success -> {
-                    // check if bank's version is compatible with app
-                    val version = response.json.getString("version")
-                    val versionIncompatible = 
VERSION_BANK.getIncompatibleStringOrNull(app, version)
-                    if (versionIncompatible != null) {
-                        ConfigResult.Error(false, versionIncompatible)
-                    } else {
-                        val currency = response.json.getString("currency")
-                        try {
-                            mCurrency.postValue(currency)
-                            prefs.edit().putString(PREF_KEY_CURRENCY, 
currency).apply()
-                            // save config
-                            saveConfig(config)
-                            ConfigResult.Success
-                        } catch (e: Exception) {
-                            ConfigResult.Error(false, "Invalid Config: 
${response.json}")
-                        }
-                    }
-                }
-                is HttpJsonResult.Error -> {
-                    if (response.statusCode > 0 && app.isOnline()) {
-                        ConfigResult.Error(response.statusCode == 401, 
response.msg)
-                    } else {
-                        ConfigResult.Offline
-                    }
-                }
-            }
-            mConfigResult.postValue(result)
-        }
-    }
-
-    @WorkerThread
-    @SuppressLint("ApplySharedPref")
-    private fun saveConfig(config: Config) {
-        this.config = config
-        prefs.edit()
-            .putString(PREF_KEY_BANK_URL, config.bankUrl)
-            .putString(PREF_KEY_USERNAME, config.username)
-            .putString(PREF_KEY_PASSWORD, config.password)
-            .commit()
-    }
-
     fun getBalance() = viewModelScope.launch(Dispatchers.IO) {
-        check(hasConfig()) { "No config to get balance" }
+        check(configManager.hasConfig()) { "No config to get balance" }
+        val config = configManager.config
         val url = "${config.bankUrl}/accounts/${config.username}/balance"
         Log.d(TAG, "Checking balance at $url")
         val result = when (val response = makeJsonGetRequest(url, config)) {
@@ -163,19 +91,7 @@ class MainViewModel(private val app: Application) : 
AndroidViewModel(app) {
     }
 
     fun lock() {
-        saveConfig(config.copy(password = ""))
+        configManager.lock()
     }
 
 }
-
-data class Config(
-    val bankUrl: String,
-    val username: String,
-    val password: String
-)
-
-sealed class ConfigResult {
-    class Error(val authError: Boolean, val msg: String) : ConfigResult()
-    object Offline : ConfigResult()
-    object Success : ConfigResult()
-}
diff --git a/cashier/src/main/java/net/taler/cashier/Response.kt 
b/cashier/src/main/java/net/taler/cashier/Response.kt
new file mode 100644
index 0000000..0ad39d0
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/Response.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.cashier
+
+import android.content.Context
+import android.util.Log
+import io.ktor.client.call.receive
+import io.ktor.client.features.ResponseException
+import io.ktor.http.HttpStatusCode
+import kotlinx.serialization.Serializable
+import net.taler.common.isOnline
+import java.net.UnknownHostException
+
+class Response<out T> private constructor(
+    private val value: Any?
+) {
+    companion object {
+        suspend fun <T> response(request: suspend () -> T): Response<T> {
+            return try {
+                Response(request())
+            } catch (e: Throwable) {
+                Log.e("HttpClient", "Error getting request", e)
+                Response(getFailure(e))
+            }
+        }
+
+        private suspend fun getFailure(e: Throwable): Failure = when (e) {
+            is ResponseException -> Failure(e, getExceptionString(e), 
e.response?.status)
+            else -> Failure(e, e.toString())
+        }
+
+        private suspend fun getExceptionString(e: ResponseException): String {
+            val response = e.response ?: return e.toString()
+            return try {
+                Log.e("TEST", "TRY RECEIVE $response")
+                val error: Error = response.receive()
+                "Error ${error.code}: ${error.hint}"
+            } catch (ex: Exception) {
+                "Status code: ${response.status.value}"
+            }
+        }
+    }
+
+    private val isFailure: Boolean get() = value is Failure
+
+    suspend fun onSuccess(block: suspend (result: T) -> Unit): Response<T> {
+        @Suppress("UNCHECKED_CAST")
+        if (!isFailure) block(value as T)
+        return this
+    }
+
+    suspend fun onError(block: suspend (failure: Failure) -> Unit): 
Response<T> {
+        if (value is Failure) block(value)
+        return this
+    }
+
+    data class Failure(
+        val exception: Throwable,
+        val msg: String,
+        val statusCode: HttpStatusCode? = null
+    ) {
+        fun isOffline(context: Context): Boolean {
+            return exception is UnknownHostException && !context.isOnline()
+        }
+    }
+
+    @Serializable
+    private class Error(
+        val code: Int?,
+        val hint: String?
+    )
+}
diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Refunds.kt 
b/cashier/src/main/java/net/taler/cashier/config/Config.kt
similarity index 59%
copy from merchant-lib/src/main/java/net/taler/merchantlib/Refunds.kt
copy to cashier/src/main/java/net/taler/cashier/config/Config.kt
index b78b571..b50cf92 100644
--- a/merchant-lib/src/main/java/net/taler/merchantlib/Refunds.kt
+++ b/cashier/src/main/java/net/taler/cashier/config/Config.kt
@@ -14,30 +14,28 @@
  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-package net.taler.merchantlib
+package net.taler.cashier.config
 
-import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
-import net.taler.lib.common.Amount
+import net.taler.lib.common.Version
+import okhttp3.Credentials
 
-@Serializable
-data class RefundRequest(
-    /**
-     * Amount to be refunded
-     */
-    val refund: Amount,
-
-    /**
-     * Human-readable refund justification
-     */
-    val reason: String
-)
+data class Config(
+    val bankUrl: String,
+    val username: String,
+    val password: String
+) {
+    val basicAuth: String get() = Credentials.basic(username, password)
+}
 
 @Serializable
-data class RefundResponse(
-    /**
-     * URL (handled by the backend) that the wallet should access to trigger 
refund processing.
-     */
-    @SerialName("taler_refund_uri")
-    val talerRefundUri: String
+data class ConfigResponse(
+    val version: String,
+    val currency: String
 )
+
+sealed class ConfigResult {
+    class Error(val authError: Boolean, val msg: String) : ConfigResult()
+    object Offline : ConfigResult()
+    object Success : ConfigResult()
+}
diff --git a/cashier/src/main/java/net/taler/cashier/ConfigFragment.kt 
b/cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt
similarity index 89%
rename from cashier/src/main/java/net/taler/cashier/ConfigFragment.kt
rename to cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt
index 71495f3..a7aaf2f 100644
--- a/cashier/src/main/java/net/taler/cashier/ConfigFragment.kt
+++ b/cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt
@@ -14,7 +14,7 @@
  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-package net.taler.cashier
+package net.taler.cashier.config
 
 import android.os.Bundle
 import android.text.method.LinkMovementMethod
@@ -34,6 +34,8 @@ 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_config.*
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
 import net.taler.common.exhaustive
 
 private const val URL_BANK_TEST = "https://bank.test.taler.net";
@@ -42,6 +44,7 @@ private const val URL_BANK_TEST_REGISTER = 
"$URL_BANK_TEST/accounts/register"
 class ConfigFragment : Fragment() {
 
     private val viewModel: MainViewModel by activityViewModels()
+    private val configManager by lazy { viewModel.configManager}
 
     override fun onCreateView(
         inflater: LayoutInflater,
@@ -53,13 +56,13 @@ class ConfigFragment : Fragment() {
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         if (savedInstanceState == null) {
-            if (viewModel.config.bankUrl.isBlank()) {
+            if (configManager.config.bankUrl.isBlank()) {
                 urlView.editText!!.setText(URL_BANK_TEST)
             } else {
-                urlView.editText!!.setText(viewModel.config.bankUrl)
+                urlView.editText!!.setText(configManager.config.bankUrl)
             }
-            usernameView.editText!!.setText(viewModel.config.username)
-            passwordView.editText!!.setText(viewModel.config.password)
+            usernameView.editText!!.setText(configManager.config.username)
+            passwordView.editText!!.setText(configManager.config.password)
         } else {
             
urlView.editText!!.setText(savedInstanceState.getCharSequence("urlView"))
             
usernameView.editText!!.setText(savedInstanceState.getCharSequence("usernameView"))
@@ -76,8 +79,8 @@ class ConfigFragment : Fragment() {
                 saveButton.visibility = INVISIBLE
                 progressBar.visibility = VISIBLE
                 // kick off check and observe result
-                viewModel.checkAndSaveConfig(config)
-                viewModel.configResult.observe(viewLifecycleOwner, 
onConfigResult)
+                configManager.checkAndSaveConfig(config)
+                configManager.configResult.observe(viewLifecycleOwner, 
onConfigResult)
                 // hide keyboard
                 val inputMethodManager =
                     getSystemService(requireContext(), 
InputMethodManager::class.java)!!
@@ -145,7 +148,7 @@ class ConfigFragment : Fragment() {
         }.exhaustive
         saveButton.visibility = VISIBLE
         progressBar.visibility = INVISIBLE
-        viewModel.configResult.removeObservers(viewLifecycleOwner)
+        configManager.configResult.removeObservers(viewLifecycleOwner)
     }
 
 }
diff --git a/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt 
b/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt
new file mode 100644
index 0000000..a18073d
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.cashier.config
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.annotation.WorkerThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKeys
+import io.ktor.client.HttpClient
+import io.ktor.client.request.get
+import io.ktor.client.request.header
+import io.ktor.http.HttpHeaders.Authorization
+import io.ktor.http.HttpStatusCode.Companion.Unauthorized
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import net.taler.cashier.Response
+import net.taler.cashier.Response.Companion.response
+import net.taler.common.getIncompatibleStringOrNull
+import net.taler.lib.common.Version
+
+private val VERSION_BANK = Version(0, 0, 0)
+private const val PREF_NAME = "net.taler.cashier.prefs"
+private const val PREF_KEY_BANK_URL = "bankUrl"
+private const val PREF_KEY_USERNAME = "username"
+private const val PREF_KEY_PASSWORD = "password"
+private const val PREF_KEY_CURRENCY = "currency"
+
+private val TAG = ConfigManager::class.java.simpleName
+
+class ConfigManager(
+    private val app: Application,
+    private val scope: CoroutineScope,
+    private val httpClient: HttpClient
+) {
+
+    val configDestination = 
ConfigFragmentDirections.actionGlobalConfigFragment()
+
+    private val masterKeyAlias = 
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
+    private val prefs = EncryptedSharedPreferences.create(
+        PREF_NAME, masterKeyAlias, app,
+        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+    )
+
+    internal var config = Config(
+        bankUrl = prefs.getString(PREF_KEY_BANK_URL, "")!!,
+        username = prefs.getString(PREF_KEY_USERNAME, "")!!,
+        password = prefs.getString(PREF_KEY_PASSWORD, "")!!
+    )
+
+    private val mCurrency = MutableLiveData<String>(
+        prefs.getString(PREF_KEY_CURRENCY, null)
+    )
+    internal val currency: LiveData<String> = mCurrency
+
+    private val mConfigResult = MutableLiveData<ConfigResult>()
+    val configResult: LiveData<ConfigResult> = mConfigResult
+
+    fun hasConfig() = config.bankUrl.isNotEmpty()
+            && config.username.isNotEmpty()
+            && config.password.isNotEmpty()
+
+    /**
+     * Start observing [configResult] after calling this to get the result 
async.
+     * Warning: Ignore null results that are used to reset old results.
+     */
+    @UiThread
+    fun checkAndSaveConfig(config: Config) = scope.launch {
+        mConfigResult.value = null
+        checkConfig(config).onError { failure ->
+            val result = if (failure.isOffline(app)) {
+                ConfigResult.Offline
+            } else {
+                ConfigResult.Error(failure.statusCode == Unauthorized, 
failure.msg)
+            }
+            mConfigResult.postValue(result)
+        }.onSuccess { response ->
+            val versionIncompatible =
+                VERSION_BANK.getIncompatibleStringOrNull(app, response.version)
+            val result = if (versionIncompatible != null) {
+                ConfigResult.Error(false, versionIncompatible)
+            } else {
+                mCurrency.postValue(response.currency)
+                prefs.edit().putString(PREF_KEY_CURRENCY, 
response.currency).apply()
+                // save config
+                saveConfig(config)
+                ConfigResult.Success
+            }
+            mConfigResult.postValue(result)
+        }
+    }
+
+    private suspend fun checkConfig(config: Config): Response<ConfigResponse> =
+        withContext(Dispatchers.IO) {
+            val url = "${config.bankUrl}/config"
+            Log.d(TAG, "Checking config: $url")
+            response {
+                httpClient.get(url) {
+                    // TODO why does that not fail already?
+                    header(Authorization, config.basicAuth)
+                } as ConfigResponse
+            }
+        }
+
+    @WorkerThread
+    @SuppressLint("ApplySharedPref")
+    internal fun saveConfig(config: Config) {
+        this.config = config
+        prefs.edit()
+            .putString(PREF_KEY_BANK_URL, config.bankUrl)
+            .putString(PREF_KEY_USERNAME, config.username)
+            .putString(PREF_KEY_PASSWORD, config.password)
+            .commit()
+    }
+
+    fun lock() {
+        saveConfig(config.copy(password = ""))
+    }
+
+}
diff --git 
a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt 
b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
index 9f3cf54..30ff3d8 100644
--- a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
@@ -54,10 +54,10 @@ class WithdrawManager(
         get() = viewModel.viewModelScope
 
     private val config
-        get() = viewModel.config
+        get() = viewModel.configManager.config
 
     private val currency: String?
-        get() = viewModel.currency.value
+        get() = viewModel.configManager.currency.value
 
     private var withdrawStatusCheck: Job? = null
 
@@ -93,13 +93,17 @@ class WithdrawManager(
             val body = JSONObject(map)
             val result = when (val response = makeJsonPostRequest(url, body, 
config)) {
                 is Success -> {
-                    val talerUri = 
response.json.getString("taler_withdraw_uri")
-                    val withdrawResult = WithdrawResult.Success(
-                        id = response.json.getString("withdrawal_id"),
-                        talerUri = talerUri,
-                        qrCode = makeQrCode(talerUri)
-                    )
-                    withdrawResult
+                    try {
+                        val talerUri = 
response.json.getString("taler_withdraw_uri")
+                        val withdrawResult = WithdrawResult.Success(
+                            id = response.json.getString("withdrawal_id"),
+                            talerUri = talerUri,
+                            qrCode = makeQrCode(talerUri)
+                        )
+                        withdrawResult
+                    } catch (e: Exception) {
+                        WithdrawResult.Error(e.toString())
+                    }
                 }
                 is Error -> {
                     if (response.statusCode > 0 && app.isOnline()) {
@@ -147,25 +151,29 @@ class WithdrawManager(
         val response = makeJsonGetRequest(url, config)
         if (response !is Success) return@launch  // ignore errors and continue 
trying
         val oldStatus = mWithdrawStatus.value
-        when {
-            response.json.getBoolean("aborted") -> {
-                cancelWithdrawStatusCheck()
-                mWithdrawStatus.postValue(WithdrawStatus.Aborted)
-            }
-            response.json.getBoolean("confirmation_done") -> {
-                if (oldStatus !is WithdrawStatus.Success) {
+        try {
+            when {
+                response.json.getBoolean("aborted") -> {
                     cancelWithdrawStatusCheck()
-                    mWithdrawStatus.postValue(WithdrawStatus.Success)
-                    viewModel.getBalance()
+                    mWithdrawStatus.postValue(WithdrawStatus.Aborted)
                 }
-            }
-            response.json.getBoolean("selection_done") -> {
-                // only update status, if there's none, yet
-                // so we don't re-notify or overwrite newer status info
-                if (oldStatus == null) {
-                    
mWithdrawStatus.postValue(WithdrawStatus.SelectionDone(withdrawalId))
+                response.json.getBoolean("confirmation_done") -> {
+                    if (oldStatus !is WithdrawStatus.Success) {
+                        cancelWithdrawStatusCheck()
+                        mWithdrawStatus.postValue(WithdrawStatus.Success)
+                        viewModel.getBalance()
+                    }
+                }
+                response.json.getBoolean("selection_done") -> {
+                    // only update status, if there's none, yet
+                    // so we don't re-notify or overwrite newer status info
+                    if (oldStatus == null) {
+                        
mWithdrawStatus.postValue(WithdrawStatus.SelectionDone(withdrawalId))
+                    }
                 }
             }
+        } catch (e: Exception) {
+            mWithdrawStatus.postValue(WithdrawStatus.Error(e.toString()))
         }
     }
 
diff --git a/cashier/src/main/res/navigation/nav_graph.xml 
b/cashier/src/main/res/navigation/nav_graph.xml
index 49f8881..9cce316 100644
--- a/cashier/src/main/res/navigation/nav_graph.xml
+++ b/cashier/src/main/res/navigation/nav_graph.xml
@@ -23,7 +23,7 @@
 
     <fragment
         android:id="@+id/configFragment"
-        android:name="net.taler.cashier.ConfigFragment"
+        android:name="net.taler.cashier.config.ConfigFragment"
         android:label="ConfigFragment"
         tools:layout="@layout/fragment_config">
         <action
diff --git a/merchant-lib/build.gradle b/merchant-lib/build.gradle
index 9b349ea..d76f867 100644
--- a/merchant-lib/build.gradle
+++ b/merchant-lib/build.gradle
@@ -54,5 +54,5 @@ dependencies {
     testImplementation 'junit:junit:4.13'
     testImplementation "io.ktor:ktor-client-mock-jvm:$ktor_version"
     testImplementation "io.ktor:ktor-client-logging-jvm:$ktor_version"
-    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.8'
+    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9'
 }
diff --git 
a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt 
b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt
index 2c50fa9..d22eaa0 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt
@@ -30,7 +30,9 @@ data class ContractTerms(
     val summaryI18n: Map<String, String>? = null,
     val amount: Amount,
     @SerialName("fulfillment_url")
-    val fulfillmentUrl: String,
+    val fulfillmentUrl: String? = null,
+    @SerialName("fulfillment_message")
+    val fulfillmentMessage: String? = null,
     val products: List<ContractProduct>,
     @SerialName("wire_transfer_deadline")
     val wireTransferDeadline: Timestamp? = null,
diff --git 
a/wallet/src/test/java/net/taler/wallet/payment/PaymentResponsesTest.kt 
b/wallet/src/test/java/net/taler/wallet/payment/PaymentResponsesTest.kt
new file mode 100644
index 0000000..15702c6
--- /dev/null
+++ b/wallet/src/test/java/net/taler/wallet/payment/PaymentResponsesTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.wallet.payment
+
+import kotlinx.serialization.json.Json
+import org.junit.Test
+
+class PaymentResponsesTest {
+
+    private val json = Json {
+        ignoreUnknownKeys = true
+        classDiscriminator = PreparePayResponse.discriminator
+    }
+
+    @Test
+    fun testInsufficientBalanceResponse() {
+        val jsonStr = """
+        {
+          "status": "insufficient-balance",
+          "contractTerms": {
+            "summary": "Gummy bears (BFH)",
+            "amount": "CHF:0.3",
+            "fulfillment_message": "\/Enjoy+your+",
+            "auto_refund": {
+              "d_ms": 300000
+            },
+            "products": [],
+            "h_wire": 
"TAHX3QPREEV64GN5SJRNRJD1EF0ZK50X8Y4BZAGEJSFQ7YVYAW1V3DVTFWVG2RXETPX05ZB9CQSHHXGFX10KRS76JK0XHC60F0YS268",
+            "wire_method": "x-taler-bank",
+            "order_id": "2020.240-01MD5F476HMXW",
+            "timestamp": {
+              "t_ms": 1598538535000
+            },
+            "refund_deadline": {
+              "t_ms": 1598538835000
+            },
+            "pay_deadline": {
+              "t_ms": 1598538835000
+            },
+            "wire_transfer_deadline": {
+              "t_ms": 1598542135000
+            },
+            "max_wire_fee": "CHF:0.1",
+            "max_fee": "CHF:0.1",
+            "wire_fee_amortization": 10,
+            "merchant_base_url": 
"https:\/\/backend.chf.taler.net\/instances\/department\/",
+            "merchant": {
+              "name": "BFH Department Technik und Informatik",
+              "instance": "department"
+            },
+            "exchanges": [],
+            "auditors": [],
+            "merchant_pub": 
"ZMVDPGGAESGYNMZTE4VHDE5QA5BMT7C9A6GR688KGBPMPATF4MKG",
+            "nonce": "W4WNY6D82H3Y8AV57FBTW4M9YR633N1ARRMBJ6R22MWPYB51JS00"
+          },
+          "proposalId": "BYWTGTHW2TM1FJSM923KD5ZGGFACRYB8EFA461R8AHVK7T9S9ZZG",
+          "amountRaw": "CHF:0.3"
+        }
+    """.trimIndent()
+        val response = json.decodeFromString(PreparePayResponse.serializer(), 
jsonStr)
+        println(response)
+    }
+
+}

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

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