From 3ab6f1569b307b155de6049ad7207e10bdf97567 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 11 Aug 2020 17:35:16 -0300 Subject: [wallet] upgrade wallet-core and adapt payment API --- wallet/build.gradle | 6 +- .../main/java/net/taler/wallet/MainViewModel.kt | 2 +- .../net/taler/wallet/backend/WalletBackendApi.kt | 21 +++++++ .../net/taler/wallet/backend/WalletResponse.kt | 45 ++++++++++++-- .../net/taler/wallet/payment/PaymentManager.kt | 72 ++++++++++++---------- .../net/taler/wallet/payment/PaymentResponses.kt | 17 +++++ .../taler/wallet/payment/PromptPaymentFragment.kt | 7 +-- .../net/taler/wallet/transactions/Transactions.kt | 2 + .../net/taler/wallet/backend/WalletResponseTest.kt | 42 +++++++++++-- 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 request( + operation: String, + mapper: ObjectMapper, + noinline args: (JSONObject.() -> JSONObject)? = null + ): WalletResponse = withContext(Dispatchers.Default) { + suspendCoroutine> { cont -> + sendRequest(operation, args?.invoke(JSONObject())) { isError, message -> + val response = if (isError) { + val error: WalletErrorInfo = mapper.readValue(message.toString()) + WalletResponse.Error(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 { @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 { + + 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::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 = 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("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("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) + } } -- cgit v1.2.3