aboutsummaryrefslogtreecommitdiff
path: root/wallet
diff options
context:
space:
mode:
Diffstat (limited to 'wallet')
-rw-r--r--wallet/build.gradle6
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainViewModel.kt2
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt21
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt45
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt72
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt17
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt7
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt2
-rw-r--r--wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt42
9 files changed, 163 insertions, 51 deletions
diff --git a/wallet/build.gradle b/wallet/build.gradle
index d0fd97d..329e271 100644
--- a/wallet/build.gradle
+++ b/wallet/build.gradle
@@ -24,7 +24,7 @@ plugins {
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()
@@ -48,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\""
}
@@ -103,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 330704e..24a8f1e 100644
--- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
@@ -99,7 +99,7 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) {
}
val withdrawManager = WithdrawManager(api, viewModelScope)
- val paymentManager = PaymentManager(api, mapper)
+ val paymentManager = PaymentManager(api, viewModelScope, mapper)
val pendingOperationsManager: PendingOperationsManager = PendingOperationsManager(api)
val transactionManager: TransactionManager = TransactionManager(api, viewModelScope, mapper)
val refundManager = RefundManager(api)
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 ea8f26f..5ca2255 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
@@ -26,6 +26,8 @@ import android.os.IBinder
import android.os.Message
import android.os.Messenger
import android.util.Log
+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
@@ -166,6 +168,25 @@ class WalletBackendApi(
}
}
+ 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
index 05a53f3..ab3d42e 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt
@@ -16,10 +16,23 @@
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
@@ -58,9 +71,10 @@ data class WalletErrorInfo(
// for the instance of the error.
val message: String,
- // Error details, type depends
- // on talerErrorCode
- val details: String?
+ // Error details, type depends on talerErrorCode
+ @Serializable(JSONObjectDeserializer::class)
+ @JsonDeserialize(using = JsonObjectDeserializer::class)
+ val details: JSONObject?
) {
val userFacingMsg: String
get() {
@@ -68,8 +82,7 @@ data class WalletErrorInfo(
append(talerErrorCode)
append(" ")
append(message)
- details?.let { it ->
- val details = JSONObject(it)
+ details?.let { details ->
details.optJSONObject("errorResponse")?.let { errorResponse ->
append("\n\n")
append(errorResponse.optString("code"))
@@ -80,3 +93,25 @@ data class WalletErrorInfo(
}.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/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/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
index 721522c..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
@@ -74,6 +75,7 @@ sealed class AmountType {
object Neutral : AmountType()
}
+@Serializable
data class TransactionError(
private val ec: Int,
private val hint: String?
diff --git a/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt b/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
index b7d7c68..698c90a 100644
--- a/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
+++ b/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt
@@ -16,17 +16,31 @@
package net.taler.wallet.backend
-import junit.framework.Assert.assertEquals
-import kotlinx.serialization.UnstableDefault
+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
-@UnstableDefault
class WalletResponseTest {
- private val json = Json(JsonConfiguration(ignoreUnknownKeys = true))
+ 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() {
@@ -53,4 +67,24 @@ class WalletResponseTest {
)
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)
+ }
}