diff options
Diffstat (limited to 'wallet/src/main/java')
8 files changed, 124 insertions, 183 deletions
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) |