summaryrefslogtreecommitdiff
path: root/wallet/src/main/java/net/taler/wallet/history
diff options
context:
space:
mode:
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/history')
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/DevHistoryAdapter.kt133
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/DevHistoryFragment.kt87
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/DevHistoryManager.kt78
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt518
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt57
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt59
6 files changed, 932 insertions, 0 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/history/DevHistoryAdapter.kt b/wallet/src/main/java/net/taler/wallet/history/DevHistoryAdapter.kt
new file mode 100644
index 0000000..88db90c
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/DevHistoryAdapter.kt
@@ -0,0 +1,133 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.history
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import net.taler.common.exhaustive
+import net.taler.common.toRelativeTime
+import net.taler.wallet.R
+import net.taler.wallet.history.DevHistoryAdapter.HistoryViewHolder
+
+@Deprecated("Replaced by TransactionAdapter")
+internal class DevHistoryAdapter(
+ private val listener: OnEventClickListener,
+ private var history: History = History()
+) : Adapter<HistoryViewHolder>() {
+
+ init {
+ setHasStableIds(false)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.list_item_history, parent, false)
+ return HistoryViewHolder(view)
+ }
+
+ override fun getItemCount(): Int = history.size
+
+ override fun onBindViewHolder(holder: HistoryViewHolder, position: Int) {
+ val transaction = history[position]
+ holder.bind(transaction)
+ }
+
+ fun update(updatedHistory: History) {
+ this.history = updatedHistory
+ this.notifyDataSetChanged()
+ }
+
+ internal open inner class HistoryViewHolder(private val v: View) : ViewHolder(v) {
+
+ protected val context: Context = v.context
+
+ private val icon: ImageView = v.findViewById(R.id.icon)
+ protected val title: TextView = v.findViewById(R.id.title)
+ private val time: TextView = v.findViewById(R.id.time)
+ private val amount: TextView = v.findViewById(R.id.amount)
+
+ private val amountColor = amount.currentTextColor
+
+ open fun bind(historyEvent: HistoryEvent) {
+ v.setOnClickListener { listener.onTransactionClicked(historyEvent) }
+ icon.setImageResource(historyEvent.icon)
+
+ title.text = if (historyEvent.title == null) {
+ when (historyEvent) {
+ is RefreshHistoryEvent -> getRefreshTitle(historyEvent)
+ is OrderAcceptedHistoryEvent -> context.getString(R.string.transaction_order_accepted)
+ is OrderRefusedHistoryEvent -> context.getString(R.string.transaction_order_refused)
+ is TipAcceptedHistoryEvent -> context.getString(R.string.transaction_tip_accepted)
+ is TipDeclinedHistoryEvent -> context.getString(R.string.transaction_tip_declined)
+ is ReserveBalanceUpdatedHistoryEvent -> context.getString(R.string.transaction_reserve_balance_updated)
+ else -> historyEvent::class.java.simpleName
+ }
+ } else historyEvent.title
+
+ time.text = historyEvent.timestamp.ms.toRelativeTime(context)
+ bindAmount(historyEvent.displayAmount)
+ }
+
+ private fun bindAmount(displayAmount: DisplayAmount?) {
+ if (displayAmount == null) {
+ amount.visibility = GONE
+ } else {
+ amount.visibility = VISIBLE
+ when (displayAmount.type) {
+ AmountType.Positive -> {
+ amount.text = context.getString(
+ R.string.amount_positive, displayAmount.amount.amountStr
+ )
+ amount.setTextColor(context.getColor(R.color.green))
+ }
+ AmountType.Negative -> {
+ amount.text = context.getString(
+ R.string.amount_negative, displayAmount.amount.amountStr
+ )
+ amount.setTextColor(context.getColor(R.color.red))
+ }
+ AmountType.Neutral -> {
+ amount.text = displayAmount.amount.amountStr
+ amount.setTextColor(amountColor)
+ }
+ }.exhaustive
+ }
+ }
+
+ private fun getRefreshTitle(transaction: RefreshHistoryEvent): String {
+ val res = when (transaction.refreshReason) {
+ RefreshReason.MANUAL -> R.string.transaction_refresh_reason_manual
+ RefreshReason.PAY -> R.string.transaction_refresh_reason_pay
+ RefreshReason.REFUND -> R.string.transaction_refresh_reason_refund
+ RefreshReason.ABORT_PAY -> R.string.transaction_refresh_reason_abort_pay
+ RefreshReason.RECOUP -> R.string.transaction_refresh_reason_recoup
+ RefreshReason.BACKUP_RESTORED -> R.string.transaction_refresh_reason_backup_restored
+ }
+ return context.getString(R.string.transaction_refresh) + " " + context.getString(res)
+ }
+
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/history/DevHistoryFragment.kt b/wallet/src/main/java/net/taler/wallet/history/DevHistoryFragment.kt
new file mode 100644
index 0000000..c3c07a3
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/DevHistoryFragment.kt
@@ -0,0 +1,87 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.history
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
+import kotlinx.android.synthetic.main.fragment_transactions.*
+import net.taler.common.fadeIn
+import net.taler.common.fadeOut
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+
+internal interface OnEventClickListener {
+ fun onTransactionClicked(historyEvent: HistoryEvent)
+}
+
+class DevHistoryFragment : Fragment(),
+ OnEventClickListener {
+
+ private val model: MainViewModel by activityViewModels()
+ private val historyManager by lazy { model.historyManager }
+ private val historyAdapter by lazy { DevHistoryAdapter(this) }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_transactions, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ if (savedInstanceState == null) historyManager.loadHistory()
+
+ list.apply {
+ adapter = historyAdapter
+ addItemDecoration(DividerItemDecoration(context, VERTICAL))
+ }
+ historyManager.progress.observe(viewLifecycleOwner, Observer { show ->
+ progressBar.visibility = if (show) VISIBLE else INVISIBLE
+ })
+ historyManager.history.observe(viewLifecycleOwner, Observer { result ->
+ onHistoryResult(result)
+ })
+ }
+
+ override fun onTransactionClicked(historyEvent: HistoryEvent) {
+ JsonDialogFragment.new(historyEvent.json.toString(2))
+ .show(parentFragmentManager, null)
+ }
+
+ private fun onHistoryResult(result: HistoryResult) = when (result) {
+ HistoryResult.Error -> {
+ list.fadeOut()
+ emptyState.text = getString(R.string.transactions_error)
+ emptyState.fadeIn()
+ }
+ is HistoryResult.Success -> {
+ emptyState.visibility = if (result.history.isEmpty()) VISIBLE else INVISIBLE
+ historyAdapter.update(result.history)
+ list.fadeIn()
+ }
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/history/DevHistoryManager.kt b/wallet/src/main/java/net/taler/wallet/history/DevHistoryManager.kt
new file mode 100644
index 0000000..72967b2
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/DevHistoryManager.kt
@@ -0,0 +1,78 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.history
+
+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.Dispatchers
+import kotlinx.coroutines.launch
+import net.taler.wallet.backend.WalletBackendApi
+import org.json.JSONObject
+
+sealed class HistoryResult {
+ object Error : HistoryResult()
+ class Success(val history: History) : HistoryResult()
+}
+
+class DevHistoryManager(
+ private val walletBackendApi: WalletBackendApi,
+ private val scope: CoroutineScope,
+ private val mapper: ObjectMapper
+) {
+
+ private val mProgress = MutableLiveData<Boolean>()
+ val progress: LiveData<Boolean> = mProgress
+
+ private val mHistory = MutableLiveData<HistoryResult>()
+ val history: LiveData<HistoryResult> = mHistory
+
+ @UiThread
+ internal fun loadHistory() {
+ mProgress.value = true
+ walletBackendApi.sendRequest("getHistory", null) { isError, result ->
+ scope.launch(Dispatchers.Default) {
+ onEventsLoaded(isError, result)
+ }
+ }
+ }
+
+ private fun onEventsLoaded(isError: Boolean, result: JSONObject) {
+ if (isError) {
+ mHistory.postValue(HistoryResult.Error)
+ return
+ }
+ val history = History()
+ val json = result.getJSONArray("history")
+ for (i in 0 until json.length()) {
+ val event: HistoryEvent = mapper.readValue(json.getString(i))
+ event.json = json.getJSONObject(i)
+ history.add(event)
+ }
+ history.reverse() // show latest first
+ mProgress.postValue(false)
+ mHistory.postValue(
+ HistoryResult.Success(
+ history
+ )
+ )
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt b/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt
new file mode 100644
index 0000000..acca679
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt
@@ -0,0 +1,518 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.history
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.LayoutRes
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY
+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 net.taler.common.Amount
+import net.taler.common.Timestamp
+import net.taler.wallet.R
+import net.taler.wallet.cleanExchange
+import org.json.JSONObject
+
+enum class ReserveType {
+ /**
+ * Manually created.
+ */
+ @JsonProperty("manual")
+ MANUAL,
+
+ /**
+ * Withdrawn from a bank that has "tight" Taler integration
+ */
+ @JsonProperty("taler-bank-withdraw")
+ @Suppress("unused")
+ TALER_BANK_WITHDRAW,
+}
+
+@JsonInclude(NON_EMPTY)
+class ReserveCreationDetail(val type: ReserveType, val bankUrl: String?)
+
+enum class RefreshReason {
+ @JsonProperty("manual")
+ @Suppress("unused")
+ MANUAL,
+
+ @JsonProperty("pay")
+ PAY,
+
+ @JsonProperty("refund")
+ @Suppress("unused")
+ REFUND,
+
+ @JsonProperty("abort-pay")
+ @Suppress("unused")
+ ABORT_PAY,
+
+ @JsonProperty("recoup")
+ @Suppress("unused")
+ RECOUP,
+
+ @JsonProperty("backup-restored")
+ @Suppress("unused")
+ BACKUP_RESTORED
+}
+
+@JsonInclude(NON_EMPTY)
+class ReserveShortInfo(
+ /**
+ * The exchange that the reserve will be at.
+ */
+ val exchangeBaseUrl: String,
+ /**
+ * Key to query more details
+ */
+ val reservePub: String,
+ /**
+ * Detail about how the reserve has been created.
+ */
+ val reserveCreationDetail: ReserveCreationDetail
+)
+
+sealed class AmountType {
+ object Positive : AmountType()
+ object Negative : AmountType()
+ object Neutral : AmountType()
+}
+
+class DisplayAmount(
+ val amount: Amount,
+ val type: AmountType
+)
+
+typealias History = ArrayList<HistoryEvent>
+
+@JsonTypeInfo(
+ use = NAME,
+ include = PROPERTY,
+ property = "type",
+ defaultImpl = UnknownHistoryEvent::class
+)
+/** missing:
+AuditorComplaintSent = "auditor-complained-sent",
+AuditorComplaintProcessed = "auditor-complaint-processed",
+AuditorTrustAdded = "auditor-trust-added",
+AuditorTrustRemoved = "auditor-trust-removed",
+ExchangeTermsAccepted = "exchange-terms-accepted",
+ExchangePolicyChanged = "exchange-policy-changed",
+ExchangeTrustAdded = "exchange-trust-added",
+ExchangeTrustRemoved = "exchange-trust-removed",
+FundsDepositedToSelf = "funds-deposited-to-self",
+FundsRecouped = "funds-recouped",
+ReserveCreated = "reserve-created",
+ */
+@JsonSubTypes(
+ Type(value = ExchangeAddedEvent::class, name = "exchange-added"),
+ Type(value = ExchangeUpdatedEvent::class, name = "exchange-updated"),
+ Type(value = ReserveBalanceUpdatedHistoryEvent::class, name = "reserve-balance-updated"),
+ Type(value = WithdrawHistoryEvent::class, name = "withdrawn"),
+ Type(value = OrderAcceptedHistoryEvent::class, name = "order-accepted"),
+ Type(value = OrderRefusedHistoryEvent::class, name = "order-refused"),
+ Type(value = OrderRedirectedHistoryEvent::class, name = "order-redirected"),
+ Type(value = PaymentHistoryEvent::class, name = "payment-sent"),
+ Type(value = PaymentAbortedHistoryEvent::class, name = "payment-aborted"),
+ Type(value = TipAcceptedHistoryEvent::class, name = "tip-accepted"),
+ Type(value = TipDeclinedHistoryEvent::class, name = "tip-declined"),
+ Type(value = RefundHistoryEvent::class, name = "refund"),
+ Type(value = RefreshHistoryEvent::class, name = "refreshed")
+)
+abstract class HistoryEvent(
+ val timestamp: Timestamp,
+ val eventId: String,
+ @get:LayoutRes
+ open val detailPageLayout: Int = 0,
+ @get:DrawableRes
+ open val icon: Int = R.drawable.ic_account_balance,
+ open val showToUser: Boolean = false
+) {
+ abstract val title: String?
+ open lateinit var json: JSONObject
+ open val displayAmount: DisplayAmount? = null
+ open fun isCurrency(currency: String): Boolean = true
+}
+
+
+class UnknownHistoryEvent(timestamp: Timestamp, eventId: String) : HistoryEvent(timestamp, eventId) {
+ override val title: String? = null
+}
+
+@JsonTypeName("exchange-added")
+class ExchangeAddedEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ val exchangeBaseUrl: String,
+ val builtIn: Boolean
+) : HistoryEvent(timestamp, eventId) {
+ override val title = cleanExchange(exchangeBaseUrl)
+}
+
+@JsonTypeName("exchange-updated")
+class ExchangeUpdatedEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ val exchangeBaseUrl: String
+) : HistoryEvent(timestamp, eventId) {
+ override val title = cleanExchange(exchangeBaseUrl)
+}
+
+
+@JsonTypeName("reserve-balance-updated")
+class ReserveBalanceUpdatedHistoryEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ /**
+ * Condensed information about the reserve.
+ */
+ val reserveShortInfo: ReserveShortInfo,
+ /**
+ * Amount currently left in the reserve.
+ */
+ val reserveBalance: Amount,
+ /**
+ * Amount we expected to be in the reserve at that time,
+ * considering ongoing withdrawals from that reserve.
+ */
+ val reserveAwaitedAmount: Amount,
+ /**
+ * Amount that hasn't been withdrawn yet.
+ */
+ val reserveUnclaimedAmount: Amount
+) : HistoryEvent(timestamp, eventId) {
+ override val title: String? = null
+ override val displayAmount = DisplayAmount(
+ reserveBalance,
+ AmountType.Neutral
+ )
+ override fun isCurrency(currency: String) = reserveBalance.currency == currency
+}
+
+@JsonTypeName("withdrawn")
+class WithdrawHistoryEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ /**
+ * Exchange that was withdrawn from.
+ */
+ val exchangeBaseUrl: String,
+ /**
+ * Unique identifier for the withdrawal session, can be used to
+ * query more detailed information from the wallet.
+ */
+ val withdrawalGroupId: String,
+ val withdrawalSource: WithdrawalSource,
+ /**
+ * Amount that has been subtracted from the reserve's balance
+ * for this withdrawal.
+ */
+ val amountWithdrawnRaw: Amount,
+ /**
+ * Amount that actually was added to the wallet's balance.
+ */
+ val amountWithdrawnEffective: Amount
+) : HistoryEvent(timestamp, eventId) {
+ override val detailPageLayout = R.layout.fragment_event_withdraw
+ override val title = cleanExchange(exchangeBaseUrl)
+ override val icon = R.drawable.transaction_withdrawal
+ override val showToUser = true
+ override val displayAmount = DisplayAmount(
+ amountWithdrawnEffective,
+ AmountType.Positive
+ )
+ override fun isCurrency(currency: String) = amountWithdrawnRaw.currency == currency
+}
+
+@JsonTypeName("order-accepted")
+class OrderAcceptedHistoryEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ /**
+ * Condensed info about the order.
+ */
+ val orderShortInfo: OrderShortInfo
+) : HistoryEvent(timestamp, eventId) {
+ override val icon = R.drawable.ic_add_circle
+ override val title: String? = null
+ override fun isCurrency(currency: String) = orderShortInfo.amount.currency == currency
+}
+
+@JsonTypeName("order-refused")
+class OrderRefusedHistoryEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ /**
+ * Condensed info about the order.
+ */
+ val orderShortInfo: OrderShortInfo
+) : HistoryEvent(timestamp, eventId) {
+ override val icon = R.drawable.ic_cancel
+ override val title: String? = null
+ override fun isCurrency(currency: String) = orderShortInfo.amount.currency == currency
+}
+
+@JsonTypeName("payment-sent")
+class PaymentHistoryEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ /**
+ * Condensed info about the order that we already paid for.
+ */
+ val orderShortInfo: OrderShortInfo,
+ /**
+ * Set to true if the payment has been previously sent
+ * to the merchant successfully, possibly with a different session ID.
+ */
+ val replay: Boolean,
+ /**
+ * Number of coins that were involved in the payment.
+ */
+ val numCoins: Int,
+ /**
+ * Amount that was paid, including deposit and wire fees.
+ */
+ val amountPaidWithFees: Amount,
+ /**
+ * Session ID that the payment was (re-)submitted under.
+ */
+ val sessionId: String?
+) : HistoryEvent(timestamp, eventId) {
+ override val detailPageLayout = R.layout.fragment_event_paid
+ override val title = orderShortInfo.summary
+ override val icon = R.drawable.ic_cash_usd_outline
+ override val showToUser = true
+ override val displayAmount = DisplayAmount(
+ amountPaidWithFees,
+ AmountType.Negative
+ )
+ override fun isCurrency(currency: String) = orderShortInfo.amount.currency == currency
+}
+
+@JsonTypeName("payment-aborted")
+class PaymentAbortedHistoryEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ /**
+ * Condensed info about the order that we already paid for.
+ */
+ val orderShortInfo: OrderShortInfo,
+ /**
+ * Amount that was lost due to refund and refreshing fees.
+ */
+ val amountLost: Amount
+) : HistoryEvent(timestamp, eventId) {
+ override val title = orderShortInfo.summary
+ override val icon = R.drawable.transaction_payment_aborted
+ override val showToUser = true
+ override val displayAmount = DisplayAmount(
+ amountLost,
+ AmountType.Negative
+ )
+ override fun isCurrency(currency: String) = orderShortInfo.amount.currency == currency
+}
+
+@JsonTypeName("refreshed")
+class RefreshHistoryEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ /**
+ * Amount that is now available again because it has
+ * been refreshed.
+ */
+ val amountRefreshedEffective: Amount,
+ /**
+ * Amount that we spent for refreshing.
+ */
+ val amountRefreshedRaw: Amount,
+ /**
+ * Why was the refreshing done?
+ */
+ val refreshReason: RefreshReason,
+ val numInputCoins: Int,
+ val numRefreshedInputCoins: Int,
+ val numOutputCoins: Int,
+ /**
+ * Identifier for a refresh group, contains one or
+ * more refresh session IDs.
+ */
+ val refreshGroupId: String
+) : HistoryEvent(timestamp, eventId) {
+ override val icon = R.drawable.transaction_refresh
+ override val title: String? = null
+ override val showToUser = !(amountRefreshedRaw - amountRefreshedEffective).isZero()
+ override val displayAmount: DisplayAmount?
+ get() {
+ return if (showToUser) DisplayAmount(
+ amountRefreshedRaw - amountRefreshedEffective,
+ AmountType.Negative
+ )
+ else null
+ }
+
+ override fun isCurrency(currency: String) = amountRefreshedRaw.currency == currency
+}
+
+@JsonTypeName("order-redirected")
+class OrderRedirectedHistoryEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ /**
+ * Condensed info about the new order that contains a
+ * product (identified by the fulfillment URL) that we've already paid for.
+ */
+ val newOrderShortInfo: OrderShortInfo,
+ /**
+ * Condensed info about the order that we already paid for.
+ */
+ val alreadyPaidOrderShortInfo: OrderShortInfo
+) : HistoryEvent(timestamp, eventId) {
+ override val icon = R.drawable.ic_directions
+ override val title = newOrderShortInfo.summary
+ override fun isCurrency(currency: String) = newOrderShortInfo.amount.currency == currency
+}
+
+@JsonTypeName("tip-accepted")
+class TipAcceptedHistoryEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ /**
+ * Unique identifier for the tip to query more information.
+ */
+ val tipId: String,
+ /**
+ * Raw amount of the tip, without extra fees that apply.
+ */
+ val tipRaw: Amount
+) : HistoryEvent(timestamp, eventId) {
+ override val icon = R.drawable.transaction_tip_accepted
+ override val title: String? = null
+ override val showToUser = true
+ override val displayAmount = DisplayAmount(
+ tipRaw,
+ AmountType.Positive
+ )
+ override fun isCurrency(currency: String) = tipRaw.currency == currency
+}
+
+@JsonTypeName("tip-declined")
+class TipDeclinedHistoryEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ /**
+ * Unique identifier for the tip to query more information.
+ */
+ val tipId: String,
+ /**
+ * Raw amount of the tip, without extra fees that apply.
+ */
+ val tipAmount: Amount
+) : HistoryEvent(timestamp, eventId) {
+ override val icon = R.drawable.transaction_tip_declined
+ override val title: String? = null
+ override val showToUser = true
+ override val displayAmount = DisplayAmount(
+ tipAmount,
+ AmountType.Neutral
+ )
+ override fun isCurrency(currency: String) = tipAmount.currency == currency
+}
+
+@JsonTypeName("refund")
+class RefundHistoryEvent(
+ timestamp: Timestamp,
+ eventId: String,
+ val orderShortInfo: OrderShortInfo,
+ /**
+ * Unique identifier for this refund.
+ * (Identifies multiple refund permissions that were obtained at once.)
+ */
+ val refundGroupId: String,
+ /**
+ * Part of the refund that couldn't be applied because
+ * the refund permissions were expired.
+ */
+ val amountRefundedInvalid: Amount,
+ /**
+ * Amount that has been refunded by the merchant.
+ */
+ val amountRefundedRaw: Amount,
+ /**
+ * Amount will be added to the wallet's balance after fees and refreshing.
+ */
+ val amountRefundedEffective: Amount
+) : HistoryEvent(timestamp, eventId) {
+ override val icon = R.drawable.transaction_refund
+ override val title = orderShortInfo.summary
+ override val detailPageLayout = R.layout.fragment_event_paid
+ override val showToUser = true
+ override val displayAmount = DisplayAmount(
+ amountRefundedEffective,
+ AmountType.Positive
+ )
+ override fun isCurrency(currency: String) = amountRefundedRaw.currency == currency
+}
+
+@JsonTypeInfo(
+ use = NAME,
+ include = PROPERTY,
+ property = "type"
+)
+@JsonSubTypes(
+ Type(value = WithdrawalSourceReserve::class, name = "reserve")
+)
+abstract class WithdrawalSource
+
+@Suppress("unused")
+@JsonTypeName("tip")
+class WithdrawalSourceTip(
+ val tipId: String
+) : WithdrawalSource()
+
+@JsonTypeName("reserve")
+class WithdrawalSourceReserve(
+ val reservePub: String
+) : WithdrawalSource()
+
+data class OrderShortInfo(
+ /**
+ * Wallet-internal identifier of the proposal.
+ */
+ val proposalId: String,
+ /**
+ * Order ID, uniquely identifies the order within a merchant instance.
+ */
+ val orderId: String,
+ /**
+ * Base URL of the merchant.
+ */
+ val merchantBaseUrl: String,
+ /**
+ * Amount that must be paid for the contract.
+ */
+ val amount: Amount,
+ /**
+ * Summary of the proposal, given by the merchant.
+ */
+ val summary: String
+)
diff --git a/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt
new file mode 100644
index 0000000..31c2b93
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt
@@ -0,0 +1,57 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.history
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import androidx.fragment.app.DialogFragment
+import kotlinx.android.synthetic.main.fragment_json.*
+import net.taler.wallet.R
+
+class JsonDialogFragment : DialogFragment() {
+
+ companion object {
+ fun new(json: String): JsonDialogFragment {
+ return JsonDialogFragment().apply {
+ arguments = Bundle().apply { putString("json", json) }
+ }
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_json, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val json = requireArguments().getString("json")
+ jsonView.text = json
+ }
+
+ override fun onStart() {
+ super.onStart()
+ dialog?.window?.setLayout(MATCH_PARENT, WRAP_CONTENT)
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt b/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
new file mode 100644
index 0000000..6c8fdaa
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
@@ -0,0 +1,59 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.history
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSubTypes
+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 net.taler.common.Timestamp
+
+
+@JsonTypeInfo(
+ use = NAME,
+ include = PROPERTY,
+ property = "type"
+)
+@JsonSubTypes(
+ JsonSubTypes.Type(value = ReserveDepositTransaction::class, name = "DEPOSIT")
+)
+abstract class ReserveTransaction
+
+
+@JsonTypeName("DEPOSIT")
+class ReserveDepositTransaction(
+ /**
+ * Amount withdrawn.
+ */
+ val amount: String,
+ /**
+ * Sender account payto://-URL
+ */
+ @JsonProperty("sender_account_url")
+ val senderAccountUrl: String,
+ /**
+ * Transfer details uniquely identifying the transfer.
+ */
+ @JsonProperty("wire_reference")
+ val wireReference: String,
+ /**
+ * Timestamp of the incoming wire transfer.
+ */
+ val timestamp: Timestamp
+) : ReserveTransaction()