diff options
20 files changed, 165 insertions, 390 deletions
diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 2b48706..9389bf3 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -12,6 +12,10 @@ <entry name="!?*.clj" /> </wildcardResourcePatterns> <bytecodeTargetLevel> + <module name="common_commonMain" target="1.6" /> + <module name="common_commonTest" target="1.6" /> + <module name="common_jvmMain" target="1.6" /> + <module name="common_jvmTest" target="1.6" /> <module name="taler-kotlin-common_jvmMain" target="1.6" /> <module name="taler-kotlin-common_jvmTest" target="1.6" /> </bytecodeTargetLevel> 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 ea5b996..a467c41 100644 --- a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt +++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt @@ -118,5 +118,6 @@ fun getSerializer() = KotlinxSerializer( Json { encodeDefaults = false ignoreUnknownKeys = true + classDiscriminator = "order_status" } ) diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Orders.kt b/merchant-lib/src/main/java/net/taler/merchantlib/Orders.kt index 9c23ef1..9242df3 100644 --- a/merchant-lib/src/main/java/net/taler/merchantlib/Orders.kt +++ b/merchant-lib/src/main/java/net/taler/merchantlib/Orders.kt @@ -16,17 +16,10 @@ package net.taler.merchantlib -import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.Serializer -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.boolean -import kotlinx.serialization.json.jsonPrimitive import net.taler.common.ContractTerms +import net.taler.lib.android.CustomClassDiscriminator import net.taler.lib.common.Duration @Serializable @@ -44,33 +37,12 @@ data class PostOrderResponse( ) @Serializable -sealed class CheckPaymentResponse { +sealed class CheckPaymentResponse: CustomClassDiscriminator { + override val discriminator: String = "order_status" abstract val paid: Boolean - @Suppress("EXPERIMENTAL_API_USAGE") - @Serializer(forClass = CheckPaymentResponse::class) - companion object : KSerializer<CheckPaymentResponse> { - override fun deserialize(decoder: Decoder): CheckPaymentResponse { - val input = decoder as JsonDecoder - val tree = input.decodeJsonElement() as JsonObject - val orderStatus = tree.getValue("order_status").jsonPrimitive.content -// return if (orderStatus == "paid") decoder.json.decodeFromJsonElement(Paid.serializer(), tree) -// else decoder.json.decodeFromJsonElement(Unpaid.serializer(), tree) - // manual parsing due to https://github.com/Kotlin/kotlinx.serialization/issues/576 - return if (orderStatus == "paid") Paid( - refunded = tree.getValue("refunded").jsonPrimitive.boolean - ) else Unpaid( - talerPayUri = tree.getValue("taler_pay_uri").jsonPrimitive.content - ) - } - - override fun serialize(encoder: Encoder, value: CheckPaymentResponse) = when (value) { - is Unpaid -> Unpaid.serializer().serialize(encoder, value) - is Paid -> Paid.serializer().serialize(encoder, value) - } - } - @Serializable + @SerialName("unpaid") data class Unpaid( override val paid: Boolean = false, @SerialName("taler_pay_uri") @@ -80,6 +52,7 @@ sealed class CheckPaymentResponse { ) : CheckPaymentResponse() @Serializable + @SerialName("paid") data class Paid( override val paid: Boolean = true, val refunded: Boolean diff --git a/taler-kotlin-android/build.gradle b/taler-kotlin-android/build.gradle index 5cb0b0e..6d992a0 100644 --- a/taler-kotlin-android/build.gradle +++ b/taler-kotlin-android/build.gradle @@ -67,8 +67,6 @@ dependencies { // JSON parsing and serialization api "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC" - implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2" - implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" lintPublish 'com.github.thirdegg:lint-rules:0.0.4-alpha' diff --git a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt index 8bf77e8..2c50fa9 100644 --- a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt +++ b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt @@ -17,10 +17,6 @@ package net.taler.common import androidx.annotation.RequiresApi -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL -import com.fasterxml.jackson.annotation.JsonProperty import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import net.taler.common.TalerUtils.getLocalizedString @@ -31,36 +27,24 @@ import net.taler.lib.common.Timestamp data class ContractTerms( val summary: String, @SerialName("summary_i18n") - @get:JsonProperty("summary_i18n") val summaryI18n: Map<String, String>? = null, val amount: Amount, @SerialName("fulfillment_url") - @get:JsonProperty("fulfillment_url") val fulfillmentUrl: String, val products: List<ContractProduct>, @SerialName("wire_transfer_deadline") - @get:JsonProperty("wire_transfer_deadline") val wireTransferDeadline: Timestamp? = null, @SerialName("refund_deadline") - @get:JsonProperty("refund_deadline") val refundDeadline: Timestamp? = null ) -@JsonInclude(NON_NULL) abstract class Product { - @get:JsonProperty("product_id") abstract val productId: String? abstract val description: String - - @get:JsonProperty("description_i18n") abstract val descriptionI18n: Map<String, String>? abstract val price: Amount - - @get:JsonProperty("delivery_location") abstract val location: String? abstract val image: String? - - @get:JsonIgnore val localizedDescription: String @RequiresApi(26) get() = getLocalizedString(descriptionI18n, description) @@ -79,12 +63,12 @@ data class ContractProduct( override val image: String? = null, val quantity: Int ) : Product() { - @get:JsonIgnore val totalPrice: Amount by lazy { price * quantity } } +@Serializable data class ContractMerchant( val name: String ) diff --git a/taler-kotlin-android/src/main/java/net/taler/lib/android/Serialization.kt b/taler-kotlin-android/src/main/java/net/taler/lib/android/Serialization.kt new file mode 100644 index 0000000..7eb4480 --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/lib/android/Serialization.kt @@ -0,0 +1,21 @@ +/* + * 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.lib.android + +interface CustomClassDiscriminator { + val discriminator: String +} diff --git a/taler-kotlin-android/src/main/java/net/taler/lib/common/AmountMixin.kt b/taler-kotlin-android/src/main/java/net/taler/lib/common/AmountMixin.kt deleted file mode 100644 index 59285b6..0000000 --- a/taler-kotlin-android/src/main/java/net/taler/lib/common/AmountMixin.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.lib.common - -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonMappingException -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonSerialize -import com.fasterxml.jackson.databind.deser.std.StdDeserializer -import com.fasterxml.jackson.databind.ser.std.StdSerializer - -/** - * Used to support Jackson serialization along with KotlinX. - */ -@JsonSerialize(using = AmountSerializer::class) -@JsonDeserialize(using = AmountDeserializer::class) -abstract class AmountMixin - -class AmountSerializer : StdSerializer<Amount>(Amount::class.java) { - override fun serialize(value: Amount, gen: JsonGenerator, provider: SerializerProvider) { - gen.writeString(value.toJSONString()) - } -} - -class AmountDeserializer : StdDeserializer<Amount>(Amount::class.java) { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Amount { - val node = p.codec.readValue(p, String::class.java) - try { - return Amount.fromJSONString(node) - } catch (e: AmountParserException) { - throw JsonMappingException(p, "Error parsing Amount", e) - } - } -} diff --git a/taler-kotlin-android/src/main/java/net/taler/lib/common/TimestampMixin.kt b/taler-kotlin-android/src/main/java/net/taler/lib/common/TimestampMixin.kt deleted file mode 100644 index 40c03f6..0000000 --- a/taler-kotlin-android/src/main/java/net/taler/lib/common/TimestampMixin.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.lib.common - -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.deser.std.StdDeserializer - -/** - * Used to support Jackson serialization along with KotlinX. - */ -abstract class TimestampMixin( - @get:JsonDeserialize(using = NeverDeserializer::class) - @get:JsonProperty("t_ms") - val ms: Long -) - -class NeverDeserializer : StdDeserializer<Long>(Long::class.java) { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Long { - return if (p.text == "never") -1 - else p.longValue - } -} diff --git a/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsTest.kt b/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsTest.kt index 0e410ca..3a2cdb4 100644 --- a/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsTest.kt +++ b/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsTest.kt @@ -16,28 +16,21 @@ package net.taler.common -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 net.taler.lib.common.Amount -import net.taler.lib.common.AmountMixin +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import net.taler.lib.common.Timestamp -import net.taler.lib.common.TimestampMixin import org.junit.Assert.assertEquals import org.junit.Test class ContractTermsTest { - 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) + private val json = Json { + ignoreUnknownKeys = true + } @Test fun test() { - val json = """ + val jsonStr = """ { "amount":"TESTKUDOS:0.5", "extra":{ @@ -72,7 +65,7 @@ class ContractTermsTest { "nonce":"FK8ZKJRV6VX6YFAG4CDSC6W0DWD084Q09DP81ANF30GRFQYM2KPG" } """.trimIndent() - val contractTerms: ContractTerms = mapper.readValue(json) + val contractTerms: ContractTerms = json.decodeFromString(jsonStr) assertEquals("Essay: 1. The Free Software Definition", contractTerms.summary) assertEquals(Timestamp.never(), contractTerms.refundDeadline) } diff --git a/wallet/build.gradle b/wallet/build.gradle index 8806a5a..ad3ba29 100644 --- a/wallet/build.gradle +++ b/wallet/build.gradle @@ -102,6 +102,8 @@ dependencies { implementation project(":anastasis-ui") implementation 'net.taler:akono:0.1' + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + implementation 'androidx.preference:preference:1.1.1' implementation 'com.google.android.material:material:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.0' @@ -129,9 +131,6 @@ dependencies { implementation "io.noties.markwon:ext-tables:$markwon_version" implementation "io.noties.markwon:recycler:$markwon_version" - // JSON parsing and serialization - implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2' - testImplementation 'junit:junit:4.13' testImplementation 'org.json:json:20200518' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/wallet/proguard-rules.pro b/wallet/proguard-rules.pro index 147334d..27f3799 100644 --- a/wallet/proguard-rules.pro +++ b/wallet/proguard-rules.pro @@ -23,36 +23,3 @@ -keep class akono.AkonoJni {*;} -keep class net.taler.wallet.** {*;} - -# Jackson --keep @com.fasterxml.jackson.annotation.JsonIgnoreProperties class * { *; } --keep @com.fasterxml.jackson.annotation.JsonCreator class * { *; } --keep @com.fasterxml.jackson.annotation.JsonValue class * { *; } --keep class com.fasterxml.** { *; } --keep class org.codehaus.** { *; } --keepnames class com.fasterxml.jackson.** { *; } --keepclassmembers public final enum com.fasterxml.jackson.annotation.JsonAutoDetect$Visibility { - public static final com.fasterxml.jackson.annotation.JsonAutoDetect$Visibility *; -} - --keep class * extends com.fasterxml.** { *; } --keep class * implements com.fasterxml.** { *; } - --keep class * { - @com.fasterxml.** *; -} - -# KotlinX serialization --keep @kotlinx.serialization.Serializable class * { *; } - -# Kotlin reflection --dontwarn kotlin.reflect.** --keep class kotlin.** { *; } --keep class org.jetbrains.annotations.** { *; } - - -# General --keepattributes SourceFile,LineNumberTable,*Annotation*,EnclosingMethod,Signature,Exceptions,InnerClasses --dontobfuscate --dontoptimize --dontshrink
\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt index 3fc49a9..9e49f54 100644 --- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt +++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt @@ -24,18 +24,11 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.distinctUntilChanged 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 kotlinx.coroutines.Job import kotlinx.coroutines.launch import net.taler.common.Event import net.taler.common.assertUiThread import net.taler.common.toEvent -import net.taler.lib.common.Amount -import net.taler.lib.common.AmountMixin -import net.taler.lib.common.Timestamp -import net.taler.lib.common.TimestampMixin import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.balances.BalanceItem import net.taler.wallet.balances.BalanceResponse @@ -70,12 +63,6 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) { var merchantVersion: String? = null private set - 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") @@ -99,9 +86,9 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) { } val withdrawManager = WithdrawManager(api, viewModelScope) - val paymentManager = PaymentManager(api, viewModelScope, mapper) + val paymentManager = PaymentManager(api, viewModelScope) val pendingOperationsManager: PendingOperationsManager = PendingOperationsManager(api) - val transactionManager: TransactionManager = TransactionManager(api, viewModelScope, mapper) + val transactionManager: TransactionManager = TransactionManager(api, viewModelScope) val refundManager = RefundManager(api, viewModelScope) val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope) 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 693fe7e..c6261bf 100644 --- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt +++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt @@ -26,12 +26,11 @@ 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 import kotlinx.serialization.json.Json +import net.taler.lib.android.CustomClassDiscriminator 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 @@ -43,14 +42,12 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +import kotlin.reflect.full.companionObjectInstance class WalletBackendApi( private val app: Application, private val notificationHandler: ((payload: JSONObject) -> Unit) ) { - private val json = Json { - ignoreUnknownKeys = true - } private var walletBackendMessenger: Messenger? = null private val queuedMessages = LinkedList<Message>() private val handlers = ConcurrentHashMap<Int, (isError: Boolean, message: JSONObject) -> Unit>() @@ -147,12 +144,18 @@ class WalletBackendApi( } } - suspend fun <T> request( + suspend inline fun <reified T> request( operation: String, serializer: KSerializer<T>? = null, - args: (JSONObject.() -> JSONObject)? = null + noinline args: (JSONObject.() -> JSONObject)? = null ): WalletResponse<T> = withContext(Dispatchers.Default) { suspendCoroutine { cont -> + val json = Json { + ignoreUnknownKeys = true + (T::class.companionObjectInstance as? CustomClassDiscriminator)?.let { + classDiscriminator = it.discriminator + } + } sendRequest(operation, args?.invoke(JSONObject())) { isError, message -> val response = if (isError) { val error = json.decodeFromString(WalletErrorInfo.serializer(), message.toString()) @@ -167,25 +170,6 @@ class WalletBackendApi( } } - suspend inline fun <reified T> request( - operation: String, - mapper: ObjectMapper, - noinline args: (JSONObject.() -> JSONObject)? = null - ): WalletResponse<T> = 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 57ce82e..4b39ff8 100644 --- a/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt +++ b/wallet/src/main/java/net/taler/wallet/backend/WalletResponse.kt @@ -16,11 +16,6 @@ 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.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -72,7 +67,6 @@ data class WalletErrorInfo( // Error details, type depends on talerErrorCode @Serializable(JSONObjectDeserializer::class) - @JsonDeserialize(using = JsonObjectDeserializer::class) val details: JSONObject? ) { val userFacingMsg: String @@ -107,10 +101,3 @@ class JSONObjectDeserializer : KSerializer<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 4924752..befcd83 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt @@ -20,12 +20,10 @@ import android.util.Log import androidx.annotation.UiThread 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.lib.common.Amount import net.taler.common.ContractTerms +import net.taler.lib.common.Amount import net.taler.wallet.TAG import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.backend.WalletErrorInfo @@ -34,8 +32,6 @@ import net.taler.wallet.payment.PayStatus.InsufficientBalance import net.taler.wallet.payment.PreparePayResponse.AlreadyConfirmedResponse import net.taler.wallet.payment.PreparePayResponse.InsufficientBalanceResponse import net.taler.wallet.payment.PreparePayResponse.PaymentPossibleResponse -import org.json.JSONObject -import java.net.MalformedURLException val REGEX_PRODUCT_IMAGE = Regex("^data:image/(jpeg|png);base64,([A-Za-z0-9+/=]+)$") @@ -63,7 +59,6 @@ sealed class PayStatus { class PaymentManager( private val api: WalletBackendApi, private val scope: CoroutineScope, - private val mapper: ObjectMapper ) { private val mPayStatus = MutableLiveData<PayStatus>(PayStatus.None) @@ -76,7 +71,7 @@ class PaymentManager( fun preparePay(url: String) = scope.launch { mPayStatus.value = PayStatus.Loading mDetailsShown.value = false - api.request<PreparePayResponse>("preparePay", mapper) { + api.request("preparePay", PreparePayResponse.serializer()) { put("talerPayUri", url) }.onError { handleError("preparePay", it) @@ -93,20 +88,6 @@ class PaymentManager( } } - // TODO validate product images (or leave to wallet-core?) - private fun getContractTerms(json: JSONObject): ContractTerms { - val terms: ContractTerms = mapper.readValue(json.getString("contractTermsRaw")) - // validate product images - terms.products.forEach { product -> - product.image?.let { image -> - if (REGEX_PRODUCT_IMAGE.matchEntire(image) == null) { - throw MalformedURLException("Invalid image data URL for ${product.description}") - } - } - } - return terms - } - fun confirmPay(proposalId: String, currency: String) = scope.launch { api.request("confirmPay", ConfirmPayResult.serializer()) { put("proposalId", proposalId) @@ -128,7 +109,7 @@ class PaymentManager( internal fun abortProposal(proposalId: String) = scope.launch { Log.i(TAG, "aborting proposal") - api.request<String>("abortProposal", mapper) { + api.request<Unit>("abortProposal") { put("proposalId", proposalId) }.onError { Log.e(TAG, "received error response to abortProposal") 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 c490654..19007b0 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt @@ -16,42 +16,47 @@ 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.ContractTerms +import net.taler.lib.android.CustomClassDiscriminator import net.taler.lib.common.Amount import net.taler.wallet.transactions.TransactionError -@JsonTypeInfo(use = NAME, property = "status") -sealed class PreparePayResponse(open val proposalId: String) { - @JsonTypeName("payment-possible") +@Serializable +sealed class PreparePayResponse { + companion object : CustomClassDiscriminator { + override val discriminator: String = "status" + } + + @Serializable + @SerialName("payment-possible") data class PaymentPossibleResponse( - override val proposalId: String, + val proposalId: String, val amountRaw: Amount, val amountEffective: Amount, - val contractTerms: ContractTerms - ) : PreparePayResponse(proposalId) { + val contractTerms: ContractTerms, + ) : PreparePayResponse() { fun toPayStatusPrepared() = PayStatus.Prepared( contractTerms = contractTerms, proposalId = proposalId, amountRaw = amountRaw, - amountEffective = amountEffective + amountEffective = amountEffective, ) } - @JsonTypeName("insufficient-balance") + @Serializable + @SerialName("insufficient-balance") data class InsufficientBalanceResponse( - override val proposalId: String, + val proposalId: String, val amountRaw: Amount, - val contractTerms: ContractTerms - ) : PreparePayResponse(proposalId) + val contractTerms: ContractTerms, + ) : PreparePayResponse() - @JsonTypeName("already-confirmed") + @Serializable + @SerialName("already-confirmed") data class AlreadyConfirmedResponse( - override val proposalId: String, + val proposalId: String, /** * Did the payment succeed? */ @@ -62,8 +67,8 @@ sealed class PreparePayResponse(open val proposalId: String) { /** * Redirect URL for the fulfillment page, only given if paid==true. */ - val nextUrl: String? - ) : PreparePayResponse(proposalId) + val nextUrl: String?, + ) : PreparePayResponse() } @Serializable 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 f494b05..9dc2d23 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt @@ -104,7 +104,7 @@ internal class TransactionAdapter( private fun bindExtraInfo(transaction: Transaction) { if (transaction.error != null) { extraInfoView.text = - context.getString(R.string.payment_error, transaction.error.text) + context.getString(R.string.payment_error, transaction.error!!.text) extraInfoView.setTextColor(red) extraInfoView.visibility = VISIBLE } else if (transaction is TransactionWithdrawal && !transaction.confirmed) { 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 e9b1b71..6b5a79b 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt @@ -20,11 +20,11 @@ import androidx.annotation.UiThread import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.switchMap -import com.fasterxml.jackson.databind.ObjectMapper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import net.taler.wallet.backend.WalletBackendApi import java.util.HashMap +import java.util.LinkedList sealed class TransactionsResult { class Error(val msg: String) : TransactionsResult() @@ -33,8 +33,7 @@ sealed class TransactionsResult { class TransactionManager( private val api: WalletBackendApi, - private val scope: CoroutineScope, - private val mapper: ObjectMapper + private val scope: CoroutineScope ) { private val mProgress = MutableLiveData<Boolean>() @@ -64,14 +63,14 @@ class TransactionManager( } if (liveData.value == null) mProgress.value = true - api.request<Transactions>("getTransactions", mapper) { + api.request("getTransactions", Transactions.serializer()) { if (searchQuery != null) put("search", searchQuery) put("currency", currency) }.onError { liveData.postValue(TransactionsResult.Error(it.userFacingMsg)) mProgress.postValue(false) }.onSuccess { result -> - val transactions = result.transactions + val transactions = LinkedList(result.transactions) // TODO remove when fixed in wallet-core val comparator = compareBy<Transaction>( { it.pending }, 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 1ed6788..8c00540 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -20,42 +20,30 @@ import android.content.Context import androidx.annotation.DrawableRes import androidx.annotation.LayoutRes import androidx.annotation.StringRes -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonSubTypes.Type -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.SerialName import kotlinx.serialization.Serializable -import net.taler.lib.common.Amount +import kotlinx.serialization.Transient import net.taler.common.ContractMerchant import net.taler.common.ContractProduct +import net.taler.lib.common.Amount import net.taler.lib.common.Timestamp import net.taler.wallet.R import net.taler.wallet.cleanExchange import net.taler.wallet.transactions.WithdrawalDetails.ManualTransfer import net.taler.wallet.transactions.WithdrawalDetails.TalerBankIntegrationApi -import java.util.LinkedList -data class Transactions(val transactions: LinkedList<Transaction>) +@Serializable +data class Transactions(val transactions: List<Transaction>) + +@Serializable +sealed class Transaction { + abstract val transactionId: String + abstract val timestamp: Timestamp + abstract val pending: Boolean + abstract val error: TransactionError? + abstract val amountRaw: Amount + abstract val amountEffective: Amount -@JsonTypeInfo(use = NAME, include = PROPERTY, property = "type") -@JsonSubTypes( - Type(value = TransactionWithdrawal::class, name = "withdrawal"), - Type(value = TransactionPayment::class, name = "payment"), - Type(value = TransactionRefund::class, name = "refund"), - Type(value = TransactionTip::class, name = "tip"), - Type(value = TransactionRefresh::class, name = "refresh") -) -abstract class Transaction( - val transactionId: String, - val timestamp: Timestamp, - val pending: Boolean, - val error: TransactionError? = null, - val amountRaw: Amount, - val amountEffective: Amount -) { @get:DrawableRes abstract val icon: Int @@ -79,24 +67,26 @@ sealed class AmountType { @Serializable data class TransactionError( private val ec: Int, - private val hint: String? + private val hint: String? = null, ) { val text get() = if (hint == null) "$ec" else "$ec $hint" } -@JsonTypeName("withdrawal") +@Serializable +@SerialName("withdrawal") class TransactionWithdrawal( - transactionId: String, - timestamp: Timestamp, - pending: Boolean, + override val transactionId: String, + override val timestamp: Timestamp, + override val pending: Boolean, val exchangeBaseUrl: String, val withdrawalDetails: WithdrawalDetails, - error: TransactionError? = null, - amountRaw: Amount, - amountEffective: Amount -) : Transaction(transactionId, timestamp, pending, error, amountRaw, amountEffective) { + override val error: TransactionError? = null, + override val amountRaw: Amount, + override val amountEffective: Amount +) : Transaction() { override val icon = R.drawable.transaction_withdrawal override val detailPageLayout = R.layout.fragment_transaction_withdrawal + @Transient override val amountType = AmountType.Positive override fun getTitle(context: Context) = cleanExchange(exchangeBaseUrl) override val generalTitleRes = R.string.withdraw_title @@ -107,13 +97,10 @@ class TransactionWithdrawal( ) } -@JsonTypeInfo(use = NAME, include = PROPERTY, property = "type") -@JsonSubTypes( - Type(value = TalerBankIntegrationApi::class, name = "taler-bank-integration-api"), - Type(value = ManualTransfer::class, name = "manual-transfer") -) +@Serializable sealed class WithdrawalDetails { - @JsonTypeName("manual-transfer") + @Serializable + @SerialName("manual-transfer") class ManualTransfer( /** * Payto URIs that the exchange supports. @@ -123,7 +110,8 @@ sealed class WithdrawalDetails { val exchangePaytoUris: List<String> ) : WithdrawalDetails() - @JsonTypeName("taler-bank-integration-api") + @Serializable + @SerialName("taler-bank-integration-api") class TalerBankIntegrationApi( /** * Set to true if the bank has confirmed the withdrawal, false if not. @@ -136,71 +124,77 @@ sealed class WithdrawalDetails { /** * If the withdrawal is unconfirmed, this can include a URL for user-initiated confirmation. */ - val bankConfirmationUrl: String? + val bankConfirmationUrl: String? = null, ) : WithdrawalDetails() } -@JsonTypeName("payment") +@Serializable +@SerialName("payment") class TransactionPayment( - transactionId: String, - timestamp: Timestamp, - pending: Boolean, + override val transactionId: String, + override val timestamp: Timestamp, + override val pending: Boolean, val info: TransactionInfo, val status: PaymentStatus, - error: TransactionError? = null, - amountRaw: Amount, - amountEffective: Amount -) : Transaction(transactionId, timestamp, pending, error, amountRaw, amountEffective) { + override val error: TransactionError? = null, + override val amountRaw: Amount, + override val amountEffective: Amount +) : Transaction() { override val icon = R.drawable.ic_cash_usd_outline override val detailPageLayout = R.layout.fragment_transaction_payment + @Transient override val amountType = AmountType.Negative override fun getTitle(context: Context) = info.merchant.name override val generalTitleRes = R.string.payment_title } +@Serializable class TransactionInfo( val orderId: String, val merchant: ContractMerchant, val summary: String, - @get:JsonProperty("summary_i18n") - val summaryI18n: Map<String, String>?, + @SerialName("summary_i18n") + val summaryI18n: Map<String, String>? = null, val products: List<ContractProduct>, val fulfillmentUrl: String ) +@Serializable enum class PaymentStatus { - @JsonProperty("aborted") + @SerialName("aborted") Aborted, - @JsonProperty("failed") + @SerialName("failed") Failed, - @JsonProperty("paid") + @SerialName("paid") Paid, - @JsonProperty("accepted") + @SerialName("accepted") Accepted } -@JsonTypeName("refund") +@Serializable +@SerialName("refund") class TransactionRefund( - transactionId: String, - timestamp: Timestamp, - pending: Boolean, + override val transactionId: String, + override val timestamp: Timestamp, + override val pending: Boolean, val refundedTransactionId: String, val info: TransactionInfo, /** * Part of the refund that couldn't be applied because the refund permissions were expired */ val amountInvalid: Amount? = null, - error: TransactionError? = null, - @JsonProperty("amountEffective") // TODO remove when fixed in wallet-core - amountRaw: Amount, - @JsonProperty("amountRaw") // TODO remove when fixed in wallet-core - amountEffective: Amount -) : Transaction(transactionId, timestamp, pending, error, amountRaw, amountEffective) { + override val error: TransactionError? = null, + @SerialName("amountEffective") // TODO remove when fixed in wallet-core + override val amountRaw: Amount, + @SerialName("amountRaw") // TODO remove when fixed in wallet-core + override val amountEffective: Amount +) : Transaction() { override val icon = R.drawable.transaction_refund override val detailPageLayout = R.layout.fragment_transaction_payment + @Transient override val amountType = AmountType.Positive override fun getTitle(context: Context): String { return context.getString(R.string.transaction_refund_from, info.merchant.name) @@ -209,20 +203,22 @@ class TransactionRefund( override val generalTitleRes = R.string.refund_title } -@JsonTypeName("tip") +@Serializable +@SerialName("tip") class TransactionTip( - transactionId: String, - timestamp: Timestamp, - pending: Boolean, + override val transactionId: String, + override val timestamp: Timestamp, + override val pending: Boolean, // TODO status: TipStatus, val exchangeBaseUrl: String, val merchant: ContractMerchant, - error: TransactionError? = null, - amountRaw: Amount, - amountEffective: Amount -) : Transaction(transactionId, timestamp, pending, error, amountRaw, amountEffective) { + override val error: TransactionError? = null, + override val amountRaw: Amount, + override val amountEffective: Amount +) : Transaction() { override val icon = R.drawable.transaction_tip_accepted // TODO different when declined override val detailPageLayout = R.layout.fragment_transaction_payment + @Transient override val amountType = AmountType.Positive override fun getTitle(context: Context): String { return context.getString(R.string.transaction_tip_from, merchant.name) @@ -231,18 +227,20 @@ class TransactionTip( override val generalTitleRes = R.string.tip_title } -@JsonTypeName("refresh") +@Serializable +@SerialName("refresh") class TransactionRefresh( - transactionId: String, - timestamp: Timestamp, - pending: Boolean, + override val transactionId: String, + override val timestamp: Timestamp, + override val pending: Boolean, val exchangeBaseUrl: String, - error: TransactionError? = null, - amountRaw: Amount, - amountEffective: Amount -) : Transaction(transactionId, timestamp, pending, error, amountRaw, amountEffective) { + override val error: TransactionError? = null, + override val amountRaw: Amount, + override val amountEffective: Amount +) : Transaction() { override val icon = R.drawable.transaction_refresh override val detailPageLayout = R.layout.fragment_transaction_withdrawal + @Transient override val amountType = AmountType.Negative override fun getTitle(context: Context): String { return context.getString(R.string.transaction_refresh) 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 a81626c..4872149 100644 --- a/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt +++ b/wallet/src/test/java/net/taler/wallet/backend/WalletResponseTest.kt @@ -16,15 +16,7 @@ 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 net.taler.lib.common.Amount -import net.taler.lib.common.AmountMixin -import net.taler.lib.common.Timestamp -import net.taler.lib.common.TimestampMixin import net.taler.wallet.balances.BalanceResponse import org.junit.Assert.assertEquals import org.junit.Test @@ -35,12 +27,6 @@ class WalletResponseTest { 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()) @@ -82,8 +68,6 @@ class WalletResponseTest { } """.trimIndent() val info = json.decodeFromString(WalletErrorInfo.serializer(), infoJson) - val infoJackson: WalletErrorInfo = mapper.readValue(infoJson) println(info.userFacingMsg) - assertEquals(info.userFacingMsg, infoJackson.userFacingMsg) } } |