[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-taler-android] branch master updated (78096ab -> 3ab6f15)
From: |
gnunet |
Subject: |
[taler-taler-android] branch master updated (78096ab -> 3ab6f15) |
Date: |
Tue, 11 Aug 2020 22:35:44 +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 78096ab [wallet] support Timestamp with "never"
new a20adab [wallet] show error icon for transactions with error
new f0670e2 [pos] Improve coroutine-based merchant library access
new d13be7c [wallet] start to move deserialization into the backend API
new 3ab6f15 [wallet] upgrade wallet-core and adapt payment API
The 4 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:
merchant-lib/build.gradle | 1 +
.../main/java/net/taler/merchantlib/MerchantApi.kt | 76 ++++++++-----
.../main/java/net/taler/merchantlib/Response.kt | 4 +-
.../java/net/taler/merchantlib/MerchantApiTest.kt | 20 ++--
.../net/taler/merchantpos/config/ConfigManager.kt | 6 +-
.../taler/merchantpos/history/HistoryManager.kt | 19 ++--
.../taler/merchantpos/payment/PaymentManager.kt | 72 ++++++------
.../net/taler/merchantpos/refund/RefundManager.kt | 24 ++--
taler-kotlin-android/build.gradle | 7 +-
wallet/build.gradle | 7 +-
.../main/java/net/taler/wallet/MainViewModel.kt | 63 ++++++-----
.../net/taler/wallet/backend/WalletBackendApi.kt | 96 ++++++++++++----
.../net/taler/wallet/backend/WalletResponse.kt | 117 ++++++++++++++++++++
.../net/taler/wallet/balances/BalanceAdapter.kt | 2 +
.../net/taler/wallet/balances/BalanceResponse.kt | 11 +-
.../net/taler/wallet/exchanges/ExchangeAdapter.kt | 2 +
.../net/taler/wallet/payment/PaymentManager.kt | 72 ++++++------
.../net/taler/wallet/payment/PaymentResponses.kt | 17 +++
.../taler/wallet/payment/PromptPaymentFragment.kt | 7 +-
.../wallet/transactions/TransactionAdapter.kt | 7 +-
.../wallet/transactions/TransactionManager.kt | 2 +-
.../net/taler/wallet/transactions/Transactions.kt | 11 +-
.../java/net/taler/wallet/withdraw/TosSection.kt | 7 ++
.../net/taler/wallet/withdraw/WithdrawManager.kt | 122 +++++++++++----------
wallet/src/main/res/drawable/ic_error.xml | 1 +
.../src/main/res/drawable/transaction_refresh.xml | 1 +
.../src/main/res/drawable/transaction_refund.xml | 1 +
.../main/res/drawable/transaction_tip_accepted.xml | 1 +
.../main/res/drawable/transaction_withdrawal.xml | 1 +
.../src/main/res/layout/list_item_transaction.xml | 1 -
.../net/taler/wallet/backend/WalletResponseTest.kt | 90 +++++++++++++++
31 files changed, 595 insertions(+), 273 deletions(-)
create mode 100644
wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt
copy taler-kotlin-common/src/nativeMain/kotlin/net/taler/common/Time.kt =>
wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt (82%)
create mode 100644
wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
diff --git a/merchant-lib/build.gradle b/merchant-lib/build.gradle
index 33e8379..5082253 100644
--- a/merchant-lib/build.gradle
+++ b/merchant-lib/build.gradle
@@ -56,4 +56,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'
}
diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt
b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt
index c92d4d2..a4ca397 100644
--- a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt
+++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt
@@ -27,63 +27,81 @@ import io.ktor.client.request.post
import io.ktor.http.ContentType.Application.Json
import io.ktor.http.HttpHeaders.Authorization
import io.ktor.http.contentType
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import net.taler.merchantlib.Response.Companion.response
-class MerchantApi(private val httpClient: HttpClient) {
+class MerchantApi(
+ private val httpClient: HttpClient = getDefaultHttpClient(),
+ private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
+) {
- suspend fun getConfig(baseUrl: String): Response<ConfigResponse> =
response {
- httpClient.get("$baseUrl/config") as ConfigResponse
+ suspend fun getConfig(baseUrl: String): Response<ConfigResponse> =
withContext(ioDispatcher) {
+ response {
+ httpClient.get("$baseUrl/config") as ConfigResponse
+ }
}
suspend fun postOrder(
merchantConfig: MerchantConfig,
orderRequest: PostOrderRequest
- ): Response<PostOrderResponse> = response {
- httpClient.post(merchantConfig.urlFor("private/orders")) {
- header(Authorization, "ApiKey ${merchantConfig.apiKey}")
- contentType(Json)
- body = orderRequest
- } as PostOrderResponse
+ ): Response<PostOrderResponse> = withContext(ioDispatcher) {
+ response {
+ httpClient.post(merchantConfig.urlFor("private/orders")) {
+ header(Authorization, "ApiKey ${merchantConfig.apiKey}")
+ contentType(Json)
+ body = orderRequest
+ } as PostOrderResponse
+ }
}
suspend fun checkOrder(
merchantConfig: MerchantConfig,
orderId: String
- ): Response<CheckPaymentResponse> = response {
- httpClient.get(merchantConfig.urlFor("private/orders/$orderId")) {
- header(Authorization, "ApiKey ${merchantConfig.apiKey}")
- } as CheckPaymentResponse
+ ): Response<CheckPaymentResponse> = withContext(ioDispatcher) {
+ response {
+ httpClient.get(merchantConfig.urlFor("private/orders/$orderId")) {
+ header(Authorization, "ApiKey ${merchantConfig.apiKey}")
+ } as CheckPaymentResponse
+ }
}
suspend fun deleteOrder(
merchantConfig: MerchantConfig,
orderId: String
- ): Response<Unit> = response {
- httpClient.delete(merchantConfig.urlFor("private/orders/$orderId")) {
- header(Authorization, "ApiKey ${merchantConfig.apiKey}")
- } as Unit
+ ): Response<Unit> = withContext(ioDispatcher) {
+ response {
+
httpClient.delete(merchantConfig.urlFor("private/orders/$orderId")) {
+ header(Authorization, "ApiKey ${merchantConfig.apiKey}")
+ } as Unit
+ }
}
- suspend fun getOrderHistory(merchantConfig: MerchantConfig):
Response<OrderHistory> = response {
- httpClient.get(merchantConfig.urlFor("private/orders")) {
- header(Authorization, "ApiKey ${merchantConfig.apiKey}")
- } as OrderHistory
- }
+ suspend fun getOrderHistory(merchantConfig: MerchantConfig):
Response<OrderHistory> =
+ withContext(ioDispatcher) {
+ response {
+ httpClient.get(merchantConfig.urlFor("private/orders")) {
+ header(Authorization, "ApiKey ${merchantConfig.apiKey}")
+ } as OrderHistory
+ }
+ }
suspend fun giveRefund(
merchantConfig: MerchantConfig,
orderId: String,
request: RefundRequest
- ): Response<RefundResponse> = response {
-
httpClient.post(merchantConfig.urlFor("private/orders/$orderId/refund")) {
- header(Authorization, "ApiKey ${merchantConfig.apiKey}")
- contentType(Json)
- body = request
- } as RefundResponse
+ ): Response<RefundResponse> = withContext(ioDispatcher) {
+ response {
+
httpClient.post(merchantConfig.urlFor("private/orders/$orderId/refund")) {
+ header(Authorization, "ApiKey ${merchantConfig.apiKey}")
+ contentType(Json)
+ body = request
+ } as RefundResponse
+ }
}
-
}
fun getDefaultHttpClient(): HttpClient = HttpClient(OkHttp) {
diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt
b/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt
index 65a12a9..fb48b46 100644
--- a/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt
+++ b/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt
@@ -25,7 +25,6 @@ import kotlinx.serialization.Serializable
class Response<out T> private constructor(
private val value: Any?
) {
-
companion object {
suspend fun <T> response(request: suspend () -> T): Response<T> {
return try {
@@ -45,7 +44,7 @@ class Response<out T> private constructor(
val isFailure: Boolean get() = value is Failure
- suspend fun handle(onFailure: ((String) -> Any)? = null, onSuccess: ((T)
-> Any)? = null) {
+ suspend fun handle(onFailure: ((String) -> Unit)? = null, onSuccess: ((T)
-> Unit)? = null) {
if (value is Failure) onFailure?.let { it(getFailureString(value)) }
else onSuccess?.let {
@Suppress("UNCHECKED_CAST")
@@ -86,5 +85,4 @@ class Response<out T> private constructor(
val code: Int?,
val hint: String?
)
-
}
diff --git
a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt
b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt
index f9f5e87..992af6f 100644
--- a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt
+++ b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt
@@ -17,7 +17,9 @@
package net.taler.merchantlib
import io.ktor.http.HttpStatusCode.Companion.NotFound
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.runBlockingTest
import net.taler.common.Amount
import net.taler.common.ContractProduct
import net.taler.common.ContractTerms
@@ -28,9 +30,10 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
+@ExperimentalCoroutinesApi
class MerchantApiTest {
- private val api = MerchantApi(httpClient)
+ private val api = MerchantApi(httpClient, TestCoroutineDispatcher())
private val merchantConfig = MerchantConfig(
baseUrl = "http://example.net/",
instance = "testInstance",
@@ -39,7 +42,7 @@ class MerchantApiTest {
private val orderId = "orderIdFoo"
@Test
- fun testGetConfig() = runBlocking {
+ fun testGetConfig() = runBlockingTest {
httpClient.giveJsonResponse("https://backend.int.taler.net/config") {
"""
{
@@ -54,7 +57,7 @@ class MerchantApiTest {
}
@Test
- fun testPostOrder() = runBlocking {
+ fun testPostOrder() = runBlockingTest {
val product = ContractProduct(
productId = "foo",
description = "bar",
@@ -111,7 +114,7 @@ class MerchantApiTest {
}
@Test
- fun testCheckOrder() = runBlocking {
+ fun testCheckOrder() = runBlockingTest {
val unpaidResponse = CheckPaymentResponse.Unpaid(false,
"http://taler.net/foo")
httpClient.giveJsonResponse("http://example.net/instances/testInstance/private/orders/$orderId")
{
"""{
@@ -140,7 +143,7 @@ class MerchantApiTest {
}
@Test
- fun testDeleteOrder() = runBlocking {
+ fun testDeleteOrder() = runBlockingTest {
httpClient.giveJsonResponse("http://example.net/instances/testInstance/private/orders/$orderId")
{
"{}"
}
@@ -163,7 +166,7 @@ class MerchantApiTest {
}
@Test
- fun testGetOrderHistory() = runBlocking {
+ fun testGetOrderHistory() = runBlockingTest {
httpClient.giveJsonResponse("http://example.net/instances/testInstance/private/orders")
{
"""{ "orders": [
{
@@ -213,7 +216,7 @@ class MerchantApiTest {
}
@Test
- fun testGiveRefund() = runBlocking {
+ fun testGiveRefund() = runBlockingTest {
httpClient.giveJsonResponse("http://example.net/instances/testInstance/private/orders/$orderId/refund")
{
"""{
"taler_refund_uri": "taler://refund/foo/bar"
@@ -227,5 +230,4 @@ class MerchantApiTest {
assertEquals("taler://refund/foo/bar", it.talerRefundUri)
}
}
-
}
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
index c0b01a2..67e3685 100644
---
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
@@ -30,6 +30,7 @@ import io.ktor.client.features.ClientRequestException
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
@@ -114,7 +115,7 @@ class ConfigManager(
Log.e(TAG, "Error retrieving merchant config", e)
val msg = if (e is ClientRequestException) {
context.getString(
- if (e.response.status.value == 401)
R.string.config_auth_error
+ if (e.response.status == Unauthorized)
R.string.config_auth_error
else R.string.config_error_network
)
} else {
@@ -145,7 +146,7 @@ class ConfigManager(
Log.e(TAG, "Error handling configuration by
${receiver::class.java.simpleName}", e)
context.getString(R.string.config_error_unknown)
}
- if (result != null) { // error
+ if (result != null) { // error
mConfigUpdateResult.postValue(ConfigUpdateResult.Error(result))
return
}
@@ -178,7 +179,6 @@ class ConfigManager(
private fun onNetworkError(msg: String) = scope.launch(Dispatchers.Main) {
mConfigUpdateResult.value = ConfigUpdateResult.Error(msg)
}
-
}
sealed class ConfigUpdateResult {
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
index aabe4cc..d880eaa 100644
---
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
@@ -20,8 +20,8 @@ import androidx.annotation.UiThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import net.taler.common.assertUiThread
import net.taler.merchantlib.MerchantApi
import net.taler.merchantlib.OrderHistoryEntry
import net.taler.merchantpos.config.ConfigManager
@@ -44,20 +44,19 @@ class HistoryManager(
val items: LiveData<HistoryResult> = mItems
@UiThread
- internal fun fetchHistory() {
+ internal fun fetchHistory() = scope.launch {
mIsLoading.value = true
val merchantConfig = configManager.merchantConfig!!
- scope.launch(Dispatchers.IO) {
- api.getOrderHistory(merchantConfig).handle(::onHistoryError) {
- mIsLoading.postValue(false)
- mItems.postValue(HistoryResult.Success(it.orders))
- }
+ api.getOrderHistory(merchantConfig).handle(::onHistoryError) {
+ assertUiThread()
+ mIsLoading.value = false
+ mItems.value = HistoryResult.Success(it.orders)
}
}
private fun onHistoryError(msg: String) {
- mIsLoading.postValue(false)
- mItems.postValue(HistoryResult.Error(msg))
+ assertUiThread()
+ mIsLoading.value = false
+ mItems.value = HistoryResult.Error(msg)
}
-
}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
index 6bab0e6..b39355a 100644
---
a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt
@@ -23,13 +23,14 @@ import androidx.annotation.UiThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import net.taler.common.Duration
+import net.taler.common.assertUiThread
import net.taler.merchantlib.CheckPaymentResponse
import net.taler.merchantlib.MerchantApi
import net.taler.merchantlib.PostOrderRequest
-import net.taler.merchantlib.PostOrderResponse
import net.taler.merchantpos.MainActivity.Companion.TAG
import net.taler.merchantpos.R
import net.taler.merchantpos.config.ConfigManager
@@ -50,12 +51,16 @@ class PaymentManager(
private val mPayment = MutableLiveData<Payment>()
val payment: LiveData<Payment> = mPayment
+ private var checkJob: Job? = null
- private val checkTimer = object : CountDownTimer(TIMEOUT, CHECK_INTERVAL) {
+ private val checkTimer: CountDownTimer = object : CountDownTimer(TIMEOUT,
CHECK_INTERVAL) {
override fun onTick(millisUntilFinished: Long) {
val orderId = payment.value?.orderId
if (orderId == null) cancel()
- else checkPayment(orderId)
+ // only start new job if old one doesn't exist or is complete
+ else if (checkJob == null || checkJob?.isCompleted == true) {
+ checkJob = checkPayment(orderId)
+ }
}
override fun onFinish() {
@@ -64,44 +69,39 @@ class PaymentManager(
}
@UiThread
- fun createPayment(order: Order) {
+ fun createPayment(order: Order) = scope.launch {
val merchantConfig = configManager.merchantConfig!!
mPayment.value = Payment(order, order.summary,
configManager.currency!!)
- scope.launch(Dispatchers.IO) {
- val request = PostOrderRequest(
- contractTerms = order.toContractTerms(),
- refundDelay = Duration(HOURS.toMillis(1))
- )
- val response = api.postOrder(merchantConfig, request)
- response.handle(::onNetworkError, ::onOrderCreated)
+ val request = PostOrderRequest(
+ contractTerms = order.toContractTerms(),
+ refundDelay = Duration(HOURS.toMillis(1))
+ )
+ api.postOrder(merchantConfig, request).handle(::onNetworkError) {
orderResponse ->
+ assertUiThread()
+ mPayment.value = mPayment.value!!.copy(orderId =
orderResponse.orderId)
+ checkTimer.start()
}
}
- private fun onOrderCreated(orderResponse: PostOrderResponse) =
scope.launch(Dispatchers.Main) {
- mPayment.value = mPayment.value!!.copy(orderId = orderResponse.orderId)
- checkTimer.start()
- }
-
- private fun checkPayment(orderId: String) {
+ private fun checkPayment(orderId: String) = scope.launch {
val merchantConfig = configManager.merchantConfig!!
- scope.launch(Dispatchers.IO) {
- val response = api.checkOrder(merchantConfig, orderId)
- response.handle(::onNetworkError, ::onPaymentChecked)
- }
- }
-
- private fun onPaymentChecked(response: CheckPaymentResponse) =
scope.launch(Dispatchers.Main) {
- val currentValue = requireNotNull(mPayment.value)
- if (response.paid) {
- mPayment.value = currentValue.copy(paid = true)
- checkTimer.cancel()
- } else if (currentValue.talerPayUri == null) {
- response as CheckPaymentResponse.Unpaid
- mPayment.value = currentValue.copy(talerPayUri =
response.talerPayUri)
+ api.checkOrder(merchantConfig, orderId).handle(::onNetworkError) {
response ->
+ assertUiThread()
+ if (!isActive) return@handle // don't continue if job was cancelled
+ val currentValue = requireNotNull(mPayment.value)
+ if (response.paid) {
+ mPayment.value = currentValue.copy(paid = true)
+ checkTimer.cancel()
+ } else if (currentValue.talerPayUri == null) {
+ response as CheckPaymentResponse.Unpaid
+ mPayment.value = currentValue.copy(talerPayUri =
response.talerPayUri)
+ }
}
}
- private fun onNetworkError(error: String) = scope.launch(Dispatchers.Main)
{
+ private fun onNetworkError(error: String) {
+ assertUiThread()
+ Log.d(TAG, "Network error: $error")
cancelPayment(error)
}
@@ -112,14 +112,14 @@ class PaymentManager(
mPayment.value?.let { payment ->
if (!payment.paid && payment.error != null) payment.orderId?.let {
orderId ->
Log.d(TAG, "Deleting cancelled and unpaid order $orderId")
- scope.launch(Dispatchers.IO) {
+ scope.launch {
api.deleteOrder(merchantConfig, orderId)
}
}
}
-
mPayment.value = mPayment.value!!.copy(error = error)
checkTimer.cancel()
+ checkJob?.isCancelled
+ checkJob = null
}
-
}
diff --git
a/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundManager.kt
b/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundManager.kt
index ea2d398..25c7c5e 100644
---
a/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundManager.kt
+++
b/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundManager.kt
@@ -20,9 +20,9 @@ import androidx.annotation.UiThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.taler.common.Amount
+import net.taler.common.assertUiThread
import net.taler.merchantlib.MerchantApi
import net.taler.merchantlib.OrderHistoryEntry
import net.taler.merchantlib.RefundRequest
@@ -65,27 +65,25 @@ class RefundManager(
}
@UiThread
- internal fun refund(item: OrderHistoryEntry, amount: Amount, reason:
String) {
+ internal fun refund(item: OrderHistoryEntry, amount: Amount, reason:
String) = scope.launch {
val merchantConfig = configManager.merchantConfig!!
val request = RefundRequest(amount, reason)
- scope.launch(Dispatchers.IO) {
- api.giveRefund(merchantConfig, item.orderId,
request).handle(::onRefundError) {
- val result = RefundResult.Success(
- refundUri = it.talerRefundUri,
- item = item,
- amount = amount,
- reason = reason
- )
- mRefundResult.postValue(result)
- }
+ api.giveRefund(merchantConfig, item.orderId,
request).handle(::onRefundError) {
+ assertUiThread()
+ mRefundResult.value = RefundResult.Success(
+ refundUri = it.talerRefundUri,
+ item = item,
+ amount = amount,
+ reason = reason
+ )
}
}
@UiThread
private fun onRefundError(msg: String) {
+ assertUiThread()
if (msg.contains("2602")) {
mRefundResult.postValue(RefundResult.AlreadyRefunded)
} else mRefundResult.postValue(RefundResult.Error(msg))
}
-
}
diff --git a/taler-kotlin-android/build.gradle
b/taler-kotlin-android/build.gradle
index d6d6003..20590e0 100644
--- a/taler-kotlin-android/build.gradle
+++ b/taler-kotlin-android/build.gradle
@@ -53,15 +53,14 @@ dependencies {
api project(":taler-kotlin-common")
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
- implementation 'androidx.appcompat:appcompat:1.1.0'
- implementation 'androidx.core:core-ktx:1.3.0'
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.core:core-ktx:1.3.1'
// Navigation
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
// ViewModel and LiveData
- def lifecycle_version = "2.2.0"
implementation
"androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// QR codes
@@ -71,7 +70,7 @@ dependencies {
api "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0"
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2"
- lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha'
+ lintPublish 'com.github.thirdegg:lint-rules:0.0.4-alpha'
testImplementation 'junit:junit:4.13'
testImplementation 'org.json:json:20190722'
diff --git a/wallet/build.gradle b/wallet/build.gradle
index ef5ddfa..329e271 100644
--- a/wallet/build.gradle
+++ b/wallet/build.gradle
@@ -20,10 +20,11 @@ plugins {
id "com.android.application"
id "kotlin-android"
id "kotlin-android-extensions"
+ id 'kotlinx-serialization'
id "de.undercouch.download"
}
-def walletCoreVersion = "v0.7.1-dev.18"
+def walletCoreVersion = "v0.7.1-dev.19"
static def versionCodeEpoch() {
return (new Date().getTime() / 1000).toInteger()
@@ -47,7 +48,7 @@ android {
minSdkVersion 24
targetSdkVersion 29
versionCode 6
- versionName "0.7.1.dev.18"
+ versionName "0.7.1.dev.19"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "WALLET_CORE_VERSION",
"\"$walletCoreVersion\""
}
@@ -102,7 +103,7 @@ dependencies {
implementation 'net.taler:akono:0.1'
implementation 'androidx.preference:preference:1.1.1'
- implementation 'com.google.android.material:material:1.1.0'
+ implementation 'com.google.android.material:material:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
// Lists and Selection
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
index 2c5e318..24a8f1e 100644
--- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
@@ -27,7 +27,8 @@ import androidx.lifecycle.viewModelScope
import
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
-import com.fasterxml.jackson.module.kotlin.readValue
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
import net.taler.common.Amount
import net.taler.common.AmountMixin
import net.taler.common.Event
@@ -37,6 +38,7 @@ import net.taler.common.assertUiThread
import net.taler.common.toEvent
import net.taler.wallet.backend.WalletBackendApi
import net.taler.wallet.balances.BalanceItem
+import net.taler.wallet.balances.BalanceResponse
import net.taler.wallet.exchanges.ExchangeManager
import net.taler.wallet.payment.PaymentManager
import net.taler.wallet.pending.PendingOperationsManager
@@ -68,15 +70,19 @@ class MainViewModel(val app: Application) :
AndroidViewModel(app) {
var merchantVersion: String? = null
private set
- private val walletBackendApi = WalletBackendApi(app, {
- // nothing to do when we connect, balance will be requested by
BalanceFragment in onStart()
- }) { payload ->
+ private val mapper = ObjectMapper()
+ .registerModule(KotlinModule())
+ .configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .addMixIn(Amount::class.java, AmountMixin::class.java)
+ .addMixIn(Timestamp::class.java, TimestampMixin::class.java)
+
+ private val api = WalletBackendApi(app) { payload ->
if (payload.optString("operation") == "init") {
val result = payload.getJSONObject("result")
val versions = result.getJSONObject("supported_protocol_versions")
exchangeVersion = versions.getString("exchange")
merchantVersion = versions.getString("merchant")
- } else if (payload.getString("type") != "waiting-for-retry") { //
ignore ping
+ } else if (payload.getString("type") != "waiting-for-retry") { //
ignore ping
Log.i(TAG, "Received notification from wallet-core:
${payload.toString(2)}")
loadBalances()
if (payload.optString("type") in transactionNotifications) {
@@ -92,20 +98,12 @@ class MainViewModel(val app: Application) :
AndroidViewModel(app) {
}
}
- private val mapper = ObjectMapper()
- .registerModule(KotlinModule())
- .configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
- .addMixIn(Amount::class.java, AmountMixin::class.java)
- .addMixIn(Timestamp::class.java, TimestampMixin::class.java)
-
- val withdrawManager = WithdrawManager(walletBackendApi, mapper)
- val paymentManager = PaymentManager(walletBackendApi, mapper)
- val pendingOperationsManager: PendingOperationsManager =
- PendingOperationsManager(walletBackendApi)
- val transactionManager: TransactionManager =
- TransactionManager(walletBackendApi, viewModelScope, mapper)
- val refundManager = RefundManager(walletBackendApi)
- val exchangeManager: ExchangeManager = ExchangeManager(walletBackendApi,
mapper)
+ val withdrawManager = WithdrawManager(api, viewModelScope)
+ val paymentManager = PaymentManager(api, viewModelScope, mapper)
+ val pendingOperationsManager: PendingOperationsManager =
PendingOperationsManager(api)
+ val transactionManager: TransactionManager = TransactionManager(api,
viewModelScope, mapper)
+ val refundManager = RefundManager(api)
+ val exchangeManager: ExchangeManager = ExchangeManager(api, mapper)
private val mTransactionsEvent = MutableLiveData<Event<String>>()
val transactionsEvent: LiveData<Event<String>> = mTransactionsEvent
@@ -118,20 +116,21 @@ class MainViewModel(val app: Application) :
AndroidViewModel(app) {
val lastBackup: LiveData<Long> = mLastBackup
override fun onCleared() {
- walletBackendApi.destroy()
+ api.destroy()
super.onCleared()
}
@UiThread
- fun loadBalances() {
+ fun loadBalances(): Job = viewModelScope.launch {
showProgressBar.value = true
- walletBackendApi.sendRequest("getBalances") { isError, result ->
- if (isError) {
- Log.e(TAG, "Error retrieving balances: ${result.toString(2)}")
- return@sendRequest
- }
- mBalances.value = mapper.readValue(result.getString("balances"))
- showProgressBar.value = false
+ val response = api.request("getBalances", BalanceResponse.serializer())
+ showProgressBar.value = false
+ response.onError {
+ // TODO expose in UI
+ Log.e(TAG, "Error retrieving balances: $it")
+ }
+ response.onSuccess {
+ mBalances.value = it.balances
}
}
@@ -145,22 +144,22 @@ class MainViewModel(val app: Application) :
AndroidViewModel(app) {
@UiThread
fun dangerouslyReset() {
- walletBackendApi.sendRequest("reset")
+ api.sendRequest("reset")
withdrawManager.testWithdrawalInProgress.value = false
mBalances.value = emptyList()
}
fun startTunnel() {
- walletBackendApi.sendRequest("startTunnel")
+ api.sendRequest("startTunnel")
}
fun stopTunnel() {
- walletBackendApi.sendRequest("stopTunnel")
+ api.sendRequest("stopTunnel")
}
fun tunnelResponse(resp: String) {
val respJson = JSONObject(resp)
- walletBackendApi.sendRequest("tunnelResponse", respJson)
+ api.sendRequest("tunnelResponse", respJson)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
index 51b3419..5ca2255 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
@@ -14,7 +14,6 @@
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-
package net.taler.wallet.backend
import android.app.Application
@@ -27,21 +26,37 @@ import android.os.IBinder
import android.os.Message
import android.os.Messenger
import android.util.Log
-import android.util.SparseArray
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonConfiguration
+import net.taler.wallet.backend.WalletBackendService.Companion.MSG_COMMAND
+import net.taler.wallet.backend.WalletBackendService.Companion.MSG_NOTIFY
+import net.taler.wallet.backend.WalletBackendService.Companion.MSG_REPLY
+import
net.taler.wallet.backend.WalletBackendService.Companion.MSG_SUBSCRIBE_NOTIFY
import org.json.JSONObject
import java.lang.ref.WeakReference
import java.util.LinkedList
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
class WalletBackendApi(
private val app: Application,
- private val onConnected: (() -> Unit),
private val notificationHandler: ((payload: JSONObject) -> Unit)
) {
-
+ private val json = Json(
+ JsonConfiguration.Stable.copy(ignoreUnknownKeys = true)
+ )
private var walletBackendMessenger: Messenger? = null
private val queuedMessages = LinkedList<Message>()
- private val handlers = SparseArray<(isError: Boolean, message: JSONObject)
-> Unit>()
- private var nextRequestID = 1
+ private val handlers = ConcurrentHashMap<Int, (isError: Boolean, message:
JSONObject) -> Unit>()
+ private var nextRequestID = AtomicInteger(0)
+ private val incomingMessenger = Messenger(IncomingHandler(this))
private val walletBackendConn = object : ServiceConnection {
override fun onServiceDisconnected(p0: ComponentName?) {
@@ -54,10 +69,15 @@ class WalletBackendApi(
val bm = Messenger(binder)
walletBackendMessenger = bm
pumpQueue(bm)
- val msg = Message.obtain(null,
WalletBackendService.MSG_SUBSCRIBE_NOTIFY)
+ val msg = Message.obtain(null, MSG_SUBSCRIBE_NOTIFY)
msg.replyTo = incomingMessenger
bm.send(msg)
- onConnected.invoke()
+ }
+ }
+
+ init {
+ Intent(app, WalletBackendService::class.java).also { intent ->
+ app.bindService(intent, walletBackendConn,
Context.BIND_AUTO_CREATE)
}
}
@@ -66,11 +86,11 @@ class WalletBackendApi(
override fun handleMessage(msg: Message) {
val api = weakApi.get() ?: return
when (msg.what) {
- WalletBackendService.MSG_REPLY -> {
+ MSG_REPLY -> {
val requestID = msg.data.getInt("requestID", 0)
val operation = msg.data.getString("operation", "??")
Log.i(TAG, "got reply for operation $operation
($requestID)")
- val h = api.handlers.get(requestID)
+ val h = api.handlers.remove(requestID)
if (h == null) {
Log.e(TAG, "request ID not associated with a handler")
return
@@ -84,7 +104,7 @@ class WalletBackendApi(
val json = JSONObject(response)
h(isError, json)
}
- WalletBackendService.MSG_NOTIFY -> {
+ MSG_NOTIFY -> {
val payloadStr = msg.data.getString("payload")
if (payloadStr == null) {
Log.e(TAG, "Notification had no payload: $msg")
@@ -97,14 +117,6 @@ class WalletBackendApi(
}
}
- private val incomingMessenger = Messenger(IncomingHandler(this))
-
- init {
- Intent(app, WalletBackendService::class.java).also { intent ->
- app.bindService(intent, walletBackendConn,
Context.BIND_AUTO_CREATE)
- }
- }
-
private fun pumpQueue(bm: Messenger) {
while (true) {
val msg = queuedMessages.pollFirst() ?: return
@@ -112,16 +124,15 @@ class WalletBackendApi(
}
}
-
fun sendRequest(
operation: String,
args: JSONObject? = null,
onResponse: (isError: Boolean, message: JSONObject) -> Unit = { _, _
-> }
) {
- val requestID = nextRequestID++
+ val requestID = nextRequestID.incrementAndGet()
Log.i(TAG, "sending request for operation $operation ($requestID)")
- val msg = Message.obtain(null, WalletBackendService.MSG_COMMAND)
- handlers.put(requestID, onResponse)
+ val msg = Message.obtain(null, MSG_COMMAND)
+ handlers[requestID] = onResponse
msg.replyTo = incomingMessenger
val data = msg.data
data.putString("operation", operation)
@@ -137,6 +148,45 @@ class WalletBackendApi(
}
}
+ suspend fun <T> request(
+ operation: String,
+ serializer: KSerializer<T>? = null,
+ args: (JSONObject.() -> JSONObject)? = null
+ ): WalletResponse<T> = withContext(Dispatchers.Default) {
+ suspendCoroutine<WalletResponse<T>> { cont ->
+ sendRequest(operation, args?.invoke(JSONObject())) { isError,
message ->
+ val response = if (isError) {
+ val error = json.parse(WalletErrorInfo.serializer(),
message.toString())
+ WalletResponse.Error<T>(error)
+ } else {
+ @Suppress("UNCHECKED_CAST") // if serializer is null, T
must be Unit
+ val t: T = serializer?.let { json.parse(serializer,
message.toString()) } ?: Unit as T
+ WalletResponse.Success(t)
+ }
+ cont.resume(response)
+ }
+ }
+ }
+
+ suspend inline fun <reified T> request(
+ operation: String,
+ mapper: ObjectMapper,
+ noinline args: (JSONObject.() -> JSONObject)? = null
+ ): WalletResponse<T> = withContext(Dispatchers.Default) {
+ suspendCoroutine<WalletResponse<T>> { cont ->
+ sendRequest(operation, args?.invoke(JSONObject())) { isError,
message ->
+ val response = if (isError) {
+ val error: WalletErrorInfo =
mapper.readValue(message.toString())
+ WalletResponse.Error<T>(error)
+ } else {
+ val t: T = mapper.readValue(message.toString())
+ WalletResponse.Success(t)
+ }
+ cont.resume(response)
+ }
+ }
+ }
+
fun destroy() {
// FIXME: implement this!
}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt
b/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt
new file mode 100644
index 0000000..ab3d42e
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.backend
+
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
+import kotlinx.serialization.Decoder
+import kotlinx.serialization.Encoder
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.PrimitiveDescriptor
+import kotlinx.serialization.PrimitiveKind.STRING
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonInput
+import kotlinx.serialization.json.JsonObject
+import org.json.JSONObject
+
+
+@Serializable
+sealed class WalletResponse<T> {
+ @Serializable
+ @SerialName("response")
+ data class Success<T>(
+ val result: T
+ ) : WalletResponse<T>()
+
+ @Serializable
+ @SerialName("error")
+ data class Error<T>(
+ val error: WalletErrorInfo
+ ) : WalletResponse<T>()
+
+ fun onSuccess(block: (result: T) -> Unit): WalletResponse<T> {
+ if (this is Success) block(this.result)
+ return this
+ }
+
+ fun onError(block: (result: WalletErrorInfo) -> Unit): WalletResponse<T> {
+ if (this is Error) block(this.error)
+ return this
+ }
+}
+
+@Serializable
+data class WalletErrorInfo(
+ // Numeric error code defined defined in the
+ // GANA gnu-taler-error-codes registry.
+ val talerErrorCode: Int,
+
+ // English description of the error code.
+ val talerErrorHint: String,
+
+ // English diagnostic message that can give details
+ // for the instance of the error.
+ val message: String,
+
+ // Error details, type depends on talerErrorCode
+ @Serializable(JSONObjectDeserializer::class)
+ @JsonDeserialize(using = JsonObjectDeserializer::class)
+ val details: JSONObject?
+) {
+ val userFacingMsg: String
+ get() {
+ return StringBuilder().apply {
+ append(talerErrorCode)
+ append(" ")
+ append(message)
+ details?.let { details ->
+ details.optJSONObject("errorResponse")?.let {
errorResponse ->
+ append("\n\n")
+ append(errorResponse.optString("code"))
+ append(" ")
+ append(errorResponse.optString("hint"))
+ }
+ }
+ }.toString()
+ }
+}
+
+class JSONObjectDeserializer : KSerializer<JSONObject> {
+
+ override val descriptor = PrimitiveDescriptor("JSONObjectDeserializer",
STRING)
+
+ override fun deserialize(decoder: Decoder): JSONObject {
+ val input = decoder as JsonInput
+ val tree = input.decodeJson() as JsonObject
+ return JSONObject(tree.toString())
+ }
+
+ override fun serialize(encoder: Encoder, value: JSONObject) {
+ error("not supported")
+ }
+}
+
+class JsonObjectDeserializer :
StdDeserializer<JSONObject>(JSONObject::class.java) {
+ override fun deserialize(p: JsonParser, ctxt: DeserializationContext):
JSONObject {
+ val node: JsonNode = p.codec.readTree(p)
+ return JSONObject(node.toString())
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
index c090e75..24ee1a1 100644
--- a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
@@ -24,10 +24,12 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
+import kotlinx.serialization.Serializable
import net.taler.common.Amount
import net.taler.wallet.R
import net.taler.wallet.balances.BalanceAdapter.BalanceViewHolder
+@Serializable
data class BalanceItem(
val available: Amount,
val pendingIncoming: Amount,
diff --git a/taler-kotlin-common/src/nativeMain/kotlin/net/taler/common/Time.kt
b/wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt
similarity index 82%
copy from taler-kotlin-common/src/nativeMain/kotlin/net/taler/common/Time.kt
copy to wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt
index 8a4091a..d1a111f 100644
--- a/taler-kotlin-common/src/nativeMain/kotlin/net/taler/common/Time.kt
+++ b/wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt
@@ -14,10 +14,11 @@
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-package net.taler.common
+package net.taler.wallet.balances
-import kotlin.system.getTimeMillis
+import kotlinx.serialization.Serializable
-actual fun nowMillis(): Long {
- return getTimeMillis()
-}
+@Serializable
+data class BalanceResponse(
+ val balances: List<BalanceItem>
+)
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
index 189f444..17ac50f 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
@@ -24,10 +24,12 @@ import android.widget.TextView
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
+import kotlinx.serialization.Serializable
import net.taler.wallet.R
import net.taler.wallet.cleanExchange
import net.taler.wallet.exchanges.ExchangeAdapter.ExchangeItemViewHolder
+@Serializable
data class ExchangeItem(
val exchangeBaseUrl: String,
val currency: String,
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
index db21da4..041fcd3 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
@@ -22,11 +22,13 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
import net.taler.common.Amount
import net.taler.common.ContractTerms
import net.taler.wallet.TAG
import net.taler.wallet.backend.WalletBackendApi
-import net.taler.wallet.getErrorString
+import net.taler.wallet.backend.WalletErrorInfo
import net.taler.wallet.payment.PayStatus.AlreadyPaid
import net.taler.wallet.payment.PayStatus.InsufficientBalance
import net.taler.wallet.payment.PreparePayResponse.AlreadyConfirmedResponse
@@ -47,14 +49,20 @@ sealed class PayStatus {
val amountEffective: Amount
) : PayStatus()
- data class InsufficientBalance(val contractTerms: ContractTerms) :
PayStatus()
+ data class InsufficientBalance(
+ val contractTerms: ContractTerms,
+ val amountRaw: Amount
+ ) : PayStatus()
+
+ // TODO bring user to fulfilment URI
object AlreadyPaid : PayStatus()
data class Error(val error: String) : PayStatus()
data class Success(val currency: String) : PayStatus()
}
class PaymentManager(
- private val walletBackendApi: WalletBackendApi,
+ private val api: WalletBackendApi,
+ private val scope: CoroutineScope,
private val mapper: ObjectMapper
) {
@@ -65,21 +73,21 @@ class PaymentManager(
internal val detailsShown: LiveData<Boolean> = mDetailsShown
@UiThread
- fun preparePay(url: String) {
+ fun preparePay(url: String) = scope.launch {
mPayStatus.value = PayStatus.Loading
mDetailsShown.value = false
-
- val args = JSONObject(mapOf("talerPayUri" to url))
- walletBackendApi.sendRequest("preparePay", args) { isError, result ->
- if (isError) {
- handleError("preparePay", getErrorString(result))
- return@sendRequest
- }
- val response: PreparePayResponse =
mapper.readValue(result.toString())
- Log.e(TAG, "PreparePayResponse $response")
+ api.request<PreparePayResponse>("preparePay", mapper) {
+ put("talerPayUri", url)
+ }.onError {
+ handleError("preparePay", it)
+ }.onSuccess { response ->
+ Log.e(TAG, "PreparePayResponse $response") // TODO remove
mPayStatus.value = when (response) {
is PaymentPossibleResponse -> response.toPayStatusPrepared()
- is InsufficientBalanceResponse ->
InsufficientBalance(response.contractTerms)
+ is InsufficientBalanceResponse -> InsufficientBalance(
+ response.contractTerms,
+ response.amountRaw
+ )
is AlreadyConfirmedResponse -> AlreadyPaid
}
}
@@ -99,13 +107,12 @@ class PaymentManager(
return terms
}
- fun confirmPay(proposalId: String, currency: String) {
- val args = JSONObject(mapOf("proposalId" to proposalId))
- walletBackendApi.sendRequest("confirmPay", args) { isError, result ->
- if (isError) {
- handleError("preparePay", getErrorString(result))
- return@sendRequest
- }
+ fun confirmPay(proposalId: String, currency: String) = scope.launch {
+ api.request("confirmPay", ConfirmPayResult.serializer()) {
+ put("proposalId", proposalId)
+ }.onError {
+ handleError("confirmPay", it)
+ }.onSuccess {
mPayStatus.postValue(PayStatus.Success(currency))
}
}
@@ -119,17 +126,14 @@ class PaymentManager(
resetPayStatus()
}
- internal fun abortProposal(proposalId: String) {
- val args = JSONObject(mapOf("proposalId" to proposalId))
-
+ internal fun abortProposal(proposalId: String) = scope.launch {
Log.i(TAG, "aborting proposal")
-
- walletBackendApi.sendRequest("abortProposal", args) { isError, result
->
- if (isError) {
- handleError("abortProposal", getErrorString(result))
- Log.e(TAG, "received error response to abortProposal")
- return@sendRequest
- }
+ api.request<String>("abortProposal", mapper) {
+ put("proposalId", proposalId)
+ }.onError {
+ Log.e(TAG, "received error response to abortProposal")
+ handleError("abortProposal", it)
+ }.onSuccess {
mPayStatus.postValue(PayStatus.None)
}
}
@@ -145,9 +149,9 @@ class PaymentManager(
mPayStatus.value = PayStatus.None
}
- private fun handleError(operation: String, msg: String) {
- Log.e(TAG, "got $operation error result $msg")
- mPayStatus.value = PayStatus.Error(msg)
+ private fun handleError(operation: String, error: WalletErrorInfo) {
+ Log.e(TAG, "got $operation error result $error")
+ mPayStatus.value = PayStatus.Error(error.userFacingMsg)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt
b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt
index 1ff8867..120489d 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt
@@ -19,8 +19,11 @@ package net.taler.wallet.payment
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME
import com.fasterxml.jackson.annotation.JsonTypeName
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
import net.taler.common.Amount
import net.taler.common.ContractTerms
+import net.taler.wallet.transactions.TransactionError
@JsonTypeInfo(use = NAME, property = "status")
sealed class PreparePayResponse(open val proposalId: String) {
@@ -42,6 +45,7 @@ sealed class PreparePayResponse(open val proposalId: String) {
@JsonTypeName("insufficient-balance")
data class InsufficientBalanceResponse(
override val proposalId: String,
+ val amountRaw: Amount,
val contractTerms: ContractTerms
) : PreparePayResponse(proposalId)
@@ -52,6 +56,8 @@ sealed class PreparePayResponse(open val proposalId: String) {
* Did the payment succeed?
*/
val paid: Boolean,
+ val amountRaw: Amount,
+ val amountEffective: Amount,
/**
* Redirect URL for the fulfillment page, only given if paid==true.
@@ -59,3 +65,14 @@ sealed class PreparePayResponse(open val proposalId: String)
{
val nextUrl: String?
) : PreparePayResponse(proposalId)
}
+
+@Serializable
+sealed class ConfirmPayResult {
+ @Serializable
+ @SerialName("done")
+ data class Done(val nextUrl: String) : ConfirmPayResult()
+
+ @Serializable
+ @SerialName("pending")
+ data class Pending(val lastError: TransactionError) : ConfirmPayResult()
+}
diff --git
a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
index ce2b6f7..40664e3 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
@@ -96,7 +96,7 @@ class PromptPaymentFragment : Fragment(),
ProductImageClickListener {
is PayStatus.Prepared -> {
showLoading(false)
val fees = payStatus.amountEffective - payStatus.amountRaw
- showOrder(payStatus.contractTerms, fees)
+ showOrder(payStatus.contractTerms, payStatus.amountRaw, fees)
confirmButton.isEnabled = true
confirmButton.setOnClickListener {
model.showProgressBar.value = true
@@ -110,7 +110,7 @@ class PromptPaymentFragment : Fragment(),
ProductImageClickListener {
}
is PayStatus.InsufficientBalance -> {
showLoading(false)
- showOrder(payStatus.contractTerms)
+ showOrder(payStatus.contractTerms, payStatus.amountRaw)
errorView.setText(R.string.payment_balance_insufficient)
errorView.fadeIn()
}
@@ -142,11 +142,10 @@ class PromptPaymentFragment : Fragment(),
ProductImageClickListener {
}
}
- private fun showOrder(contractTerms: ContractTerms, totalFees: Amount? =
null) {
+ private fun showOrder(contractTerms: ContractTerms, amount:Amount,
totalFees: Amount? = null) {
orderView.text = contractTerms.summary
adapter.setItems(contractTerms.products)
if (contractTerms.products.size == 1)
paymentManager.toggleDetailsShown()
- val amount = contractTerms.amount
totalView.text = amount.toString()
if (totalFees != null && !totalFees.isZero()) {
feeView.text = getString(R.string.payment_fee, totalFees)
diff --git
a/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
index d670b74..f494b05 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
@@ -89,8 +89,11 @@ internal class TransactionAdapter(
v.foreground = selectableForeground
v.setOnClickListener { listener.onTransactionClicked(transaction) }
v.isActivated = selected
-
- icon.setImageResource(transaction.icon)
+ if (transaction.error == null) {
+ icon.setImageResource(transaction.icon)
+ } else {
+ icon.setImageResource(R.drawable.ic_error)
+ }
title.text = transaction.getTitle(context)
bindExtraInfo(transaction)
time.text = transaction.timestamp.ms.toRelativeTime(context)
diff --git
a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
index 8ec3914..b9f86b3 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
@@ -87,7 +87,7 @@ class TransactionManager(
@WorkerThread
private fun onTransactionsLoaded(
liveData: MutableLiveData<TransactionsResult>,
- currency: String?, // only non-null if we should update all
transactions cache
+ currency: String?, // only non-null if we should update all
transactions cache
result: JSONObject
) {
val transactionsArray = result.getString("transactions")
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
index 1dc55dc..1ba7e79 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
@@ -27,6 +27,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME
import com.fasterxml.jackson.annotation.JsonTypeName
+import kotlinx.serialization.Serializable
import net.taler.common.Amount
import net.taler.common.ContractMerchant
import net.taler.common.ContractProduct
@@ -36,6 +37,8 @@ import net.taler.wallet.cleanExchange
import net.taler.wallet.transactions.WithdrawalDetails.ManualTransfer
import net.taler.wallet.transactions.WithdrawalDetails.TalerBankIntegrationApi
+data class Transactions(val transactions: List<Transaction>)
+
@JsonTypeInfo(use = NAME, include = PROPERTY, property = "type")
@JsonSubTypes(
Type(value = TransactionWithdrawal::class, name = "withdrawal"),
@@ -72,8 +75,12 @@ sealed class AmountType {
object Neutral : AmountType()
}
-class TransactionError(private val ec: Int, private val hint: String?) {
- val text get() = if (hint == null) "$ec" else "$ec - $hint"
+@Serializable
+data class TransactionError(
+ private val ec: Int,
+ private val hint: String?
+) {
+ val text get() = if (hint == null) "$ec" else "$ec $hint"
}
@JsonTypeName("withdrawal")
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/TosSection.kt
b/wallet/src/main/java/net/taler/wallet/withdraw/TosSection.kt
index b27de42..b198478 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/TosSection.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/TosSection.kt
@@ -17,6 +17,7 @@
package net.taler.wallet.withdraw
import io.noties.markwon.Markwon
+import kotlinx.serialization.Serializable
import org.commonmark.node.Code
import org.commonmark.node.Document
import org.commonmark.node.Heading
@@ -73,3 +74,9 @@ private fun getNodeText(rootNode: Node): String {
}
return text
}
+
+@Serializable
+data class TosResponse(
+ val tos: String,
+ val currentEtag: String
+)
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
index 6fb9390..1066550 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
@@ -19,16 +19,16 @@ package net.taler.wallet.withdraw
import android.util.Log
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.readValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
import net.taler.common.Amount
import net.taler.wallet.TAG
import net.taler.wallet.backend.WalletBackendApi
+import net.taler.wallet.backend.WalletErrorInfo
import net.taler.wallet.exchanges.ExchangeFees
import net.taler.wallet.exchanges.ExchangeItem
-import net.taler.wallet.getErrorString
import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails
-import org.json.JSONObject
sealed class WithdrawStatus {
data class Loading(val talerWithdrawUri: String? = null) : WithdrawStatus()
@@ -53,12 +53,14 @@ sealed class WithdrawStatus {
data class Error(val message: String?) : WithdrawStatus()
}
+@Serializable
data class WithdrawalDetailsForUri(
val amount: Amount,
val defaultExchangeBaseUrl: String?,
val possibleExchanges: List<ExchangeItem>
)
+@Serializable
data class WithdrawalDetails(
val tosAccepted: Boolean,
val amountRaw: Amount,
@@ -66,8 +68,8 @@ data class WithdrawalDetails(
)
class WithdrawManager(
- private val walletBackendApi: WalletBackendApi,
- private val mapper: ObjectMapper
+ private val api: WalletBackendApi,
+ private val scope: CoroutineScope
) {
val withdrawStatus = MutableLiveData<WithdrawStatus>()
@@ -78,22 +80,21 @@ class WithdrawManager(
fun withdrawTestkudos() {
testWithdrawalInProgress.value = true
- walletBackendApi.sendRequest("withdrawTestkudos") { _, _ ->
+ api.sendRequest("withdrawTestkudos") { _, _ ->
testWithdrawalInProgress.postValue(false)
}
}
- fun getWithdrawalDetails(uri: String) {
+ fun getWithdrawalDetails(uri: String) = scope.launch {
withdrawStatus.value = WithdrawStatus.Loading(uri)
- val args = JSONObject().apply {
- put("talerWithdrawUri", uri)
- }
- walletBackendApi.sendRequest("getWithdrawalDetailsForUri", args) {
isError, result ->
- if (isError) {
- handleError("getWithdrawalDetailsForUri", result)
- return@sendRequest
+ val response =
+ api.request("getWithdrawalDetailsForUri",
WithdrawalDetailsForUri.serializer()) {
+ put("talerWithdrawUri", uri)
}
- val details: WithdrawalDetailsForUri =
mapper.readValue(result.toString())
+ response.onError { error ->
+ handleError("getWithdrawalDetailsForUri", error)
+ }
+ response.onSuccess { details ->
if (details.defaultExchangeBaseUrl == null) {
// TODO go to exchange selection screen instead
val chosenExchange =
details.possibleExchanges[0].exchangeBaseUrl
@@ -104,45 +105,51 @@ class WithdrawManager(
}
}
- fun getWithdrawalDetails(exchangeBaseUrl: String, amount: Amount, uri:
String? = null) {
+ fun getWithdrawalDetails(
+ exchangeBaseUrl: String,
+ amount: Amount,
+ uri: String? = null
+ ) = scope.launch {
withdrawStatus.value = WithdrawStatus.Loading(uri)
- val args = JSONObject().apply {
- put("exchangeBaseUrl", exchangeBaseUrl)
- put("amount", amount.toJSONString())
- }
- walletBackendApi.sendRequest("getWithdrawalDetailsForAmount", args) {
isError, result ->
- if (isError) {
- handleError("getWithdrawalDetailsForAmount", result)
- return@sendRequest
+ val response =
+ api.request("getWithdrawalDetailsForAmount",
WithdrawalDetails.serializer()) {
+ put("exchangeBaseUrl", exchangeBaseUrl)
+ put("amount", amount.toJSONString())
}
- val details: WithdrawalDetails =
mapper.readValue(result.toString())
- if (details.tosAccepted)
+ response.onError { error ->
+ handleError("getWithdrawalDetailsForAmount", error)
+ }
+ response.onSuccess { details ->
+ if (details.tosAccepted) {
withdrawStatus.value = ReceivedDetails(
talerWithdrawUri = uri,
exchangeBaseUrl = exchangeBaseUrl,
amountRaw = details.amountRaw,
amountEffective = details.amountEffective
)
- else getExchangeTos(exchangeBaseUrl, details, uri)
+ } else getExchangeTos(exchangeBaseUrl, details, uri)
}
}
- private fun getExchangeTos(exchangeBaseUrl: String, details:
WithdrawalDetails, uri: String?) {
- val args = JSONObject().apply {
+ private fun getExchangeTos(
+ exchangeBaseUrl: String,
+ details: WithdrawalDetails,
+ uri: String?
+ ) = scope.launch {
+ val response = api.request("getExchangeTos", TosResponse.serializer())
{
put("exchangeBaseUrl", exchangeBaseUrl)
}
- walletBackendApi.sendRequest("getExchangeTos", args) { isError, result
->
- if (isError) {
- handleError("getExchangeTos", result)
- return@sendRequest
- }
+ response.onError {
+ handleError("getExchangeTos", it)
+ }
+ response.onSuccess {
withdrawStatus.value = WithdrawStatus.TosReviewRequired(
talerWithdrawUri = uri,
exchangeBaseUrl = exchangeBaseUrl,
amountRaw = details.amountRaw,
amountEffective = details.amountEffective,
- tosText = result.getString("tos"),
- tosEtag = result.getString("currentEtag")
+ tosText = it.tos,
+ tosEtag = it.currentEtag
)
}
}
@@ -150,17 +157,14 @@ class WithdrawManager(
/**
* Accept the currently displayed terms of service.
*/
- fun acceptCurrentTermsOfService() {
+ fun acceptCurrentTermsOfService() = scope.launch {
val s = withdrawStatus.value as WithdrawStatus.TosReviewRequired
- val args = JSONObject().apply {
+ api.request<Unit>("setExchangeTosAccepted") {
put("exchangeBaseUrl", s.exchangeBaseUrl)
put("etag", s.tosEtag)
- }
- walletBackendApi.sendRequest("setExchangeTosAccepted", args) {
isError, result ->
- if (isError) {
- handleError("setExchangeTosAccepted", result)
- return@sendRequest
- }
+ }.onError {
+ handleError("setExchangeTosAccepted", it)
+ }.onSuccess {
withdrawStatus.value = ReceivedDetails(
talerWithdrawUri = s.talerWithdrawUri,
exchangeBaseUrl = s.exchangeBaseUrl,
@@ -171,33 +175,33 @@ class WithdrawManager(
}
@UiThread
- fun acceptWithdrawal() {
+ fun acceptWithdrawal() = scope.launch {
val status = withdrawStatus.value as ReceivedDetails
+ val operation = if (status.talerWithdrawUri == null) {
+ "acceptManualWithdrawal"
+ } else {
+ "acceptBankIntegratedWithdrawal"
+ }
+ withdrawStatus.value = WithdrawStatus.Withdrawing
- val operation = if (status.talerWithdrawUri == null)
- "acceptManualWithdrawal" else "acceptBankIntegratedWithdrawal"
- val args = JSONObject().apply {
+ api.request<Unit>(operation) {
put("exchangeBaseUrl", status.exchangeBaseUrl)
if (status.talerWithdrawUri == null) {
put("amount", status.amountRaw)
} else {
put("talerWithdrawUri", status.talerWithdrawUri)
}
- }
- withdrawStatus.value = WithdrawStatus.Withdrawing
- walletBackendApi.sendRequest(operation, args) { isError, result ->
- if (isError) {
- handleError(operation, result)
- return@sendRequest
- }
+ }.onError {
+ handleError(operation, it)
+ }.onSuccess {
withdrawStatus.value =
WithdrawStatus.Success(status.amountRaw.currency)
}
}
@UiThread
- private fun handleError(operation: String, result: JSONObject) {
- Log.e(TAG, "Error $operation ${result.toString(2)}")
- withdrawStatus.value = WithdrawStatus.Error(getErrorString(result))
+ private fun handleError(operation: String, error: WalletErrorInfo) {
+ Log.e(TAG, "Error $operation $error")
+ withdrawStatus.value = WithdrawStatus.Error(error.userFacingMsg)
}
}
diff --git a/wallet/src/main/res/drawable/ic_error.xml
b/wallet/src/main/res/drawable/ic_error.xml
index abbe33e..4f747f1 100644
--- a/wallet/src/main/res/drawable/ic_error.xml
+++ b/wallet/src/main/res/drawable/ic_error.xml
@@ -17,6 +17,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="@color/red"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
diff --git a/wallet/src/main/res/drawable/transaction_refresh.xml
b/wallet/src/main/res/drawable/transaction_refresh.xml
index 219b891..63889d9 100644
--- a/wallet/src/main/res/drawable/transaction_refresh.xml
+++ b/wallet/src/main/res/drawable/transaction_refresh.xml
@@ -17,6 +17,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/wallet/src/main/res/drawable/transaction_refund.xml
b/wallet/src/main/res/drawable/transaction_refund.xml
index 6c3d0a7..864add9 100644
--- a/wallet/src/main/res/drawable/transaction_refund.xml
+++ b/wallet/src/main/res/drawable/transaction_refund.xml
@@ -17,6 +17,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/wallet/src/main/res/drawable/transaction_tip_accepted.xml
b/wallet/src/main/res/drawable/transaction_tip_accepted.xml
index b945b53..27b1ae4 100644
--- a/wallet/src/main/res/drawable/transaction_tip_accepted.xml
+++ b/wallet/src/main/res/drawable/transaction_tip_accepted.xml
@@ -17,6 +17,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/wallet/src/main/res/drawable/transaction_withdrawal.xml
b/wallet/src/main/res/drawable/transaction_withdrawal.xml
index 4fd64f5..edbd4ea 100644
--- a/wallet/src/main/res/drawable/transaction_withdrawal.xml
+++ b/wallet/src/main/res/drawable/transaction_withdrawal.xml
@@ -17,6 +17,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/wallet/src/main/res/layout/list_item_transaction.xml
b/wallet/src/main/res/layout/list_item_transaction.xml
index 34712a2..239e656 100644
--- a/wallet/src/main/res/layout/list_item_transaction.xml
+++ b/wallet/src/main/res/layout/list_item_transaction.xml
@@ -33,7 +33,6 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
- app:tint="?android:colorControlNormal"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_cash_usd_outline" />
diff --git
a/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
b/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
new file mode 100644
index 0000000..698c90a
--- /dev/null
+++ b/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.backend
+
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import com.fasterxml.jackson.module.kotlin.readValue
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonConfiguration
+import net.taler.common.Amount
+import net.taler.common.AmountMixin
+import net.taler.common.Timestamp
+import net.taler.common.TimestampMixin
+import net.taler.wallet.balances.BalanceResponse
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class WalletResponseTest {
+
+ private val json = Json(
+ JsonConfiguration.Stable.copy(ignoreUnknownKeys = true)
+ )
+
+ private val mapper = ObjectMapper()
+ .registerModule(KotlinModule())
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .addMixIn(Amount::class.java, AmountMixin::class.java)
+ .addMixIn(Timestamp::class.java, TimestampMixin::class.java)
+
+ @Test
+ fun testBalanceResponse() {
+ val serializer =
WalletResponse.Success.serializer(BalanceResponse.serializer())
+ val response = json.parse(
+ serializer, """
+ {
+ "type": "response",
+ "operation": "getBalances",
+ "id": 2,
+ "result": {
+ "balances": [
+ {
+ "available": "TESTKUDOS:15.8",
+ "pendingIncoming": "TESTKUDOS:0",
+ "pendingOutgoing": "TESTKUDOS:0",
+ "hasPendingTransactions": false,
+ "requiresUserInput": false
+ }
+ ]
+ }
+ }
+ """.trimIndent()
+ )
+ assertEquals(1, response.result.balances.size)
+ }
+
+ @Test
+ fun testWalletErrorInfo() {
+ val infoJson = """
+ {
+ "talerErrorCode":7001,
+ "talerErrorHint":"Error: WALLET_UNEXPECTED_EXCEPTION",
+ "details":{
+ "httpStatusCode": 401,
+ "requestUrl":
"https:\/\/backend.demo.taler.net\/-\/FSF\/orders\/2020.224-02XC8W52BHH3G\/claim",
+ "requestMethod": "POST"
+ },
+ "message":"unexpected exception: Error: BUG: invariant
violation (purchase status)"
+ }
+ """.trimIndent()
+ val info = json.parse(WalletErrorInfo.serializer(), infoJson)
+ val infoJackson: WalletErrorInfo = mapper.readValue(infoJson)
+ println(info.userFacingMsg)
+ assertEquals(info.userFacingMsg, infoJackson.userFacingMsg)
+ }
+}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [taler-taler-android] branch master updated (78096ab -> 3ab6f15),
gnunet <=