[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.
- [taler-taler-android] branch master updated (ed3f864 -> e71c580),
gnunet <=