summaryrefslogtreecommitdiff
path: root/wallet/src/main/java/net/taler/wallet/transactions
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2020-04-15 11:26:51 -0300
committerTorsten Grote <t@grobox.de>2020-04-15 11:26:51 -0300
commitea3250845fb266a2ecd5ebeba561bc99101bf3de (patch)
treed726f2216efa9b3008741b0037db325725d0d90b /wallet/src/main/java/net/taler/wallet/transactions
parent8e4f85d467c8e8109026a7195757ce9448ad7b19 (diff)
downloadtaler-android-ea3250845fb266a2ecd5ebeba561bc99101bf3de.tar.gz
taler-android-ea3250845fb266a2ecd5ebeba561bc99101bf3de.tar.bz2
taler-android-ea3250845fb266a2ecd5ebeba561bc99101bf3de.zip
[wallet] rename history to transactions
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/transactions')
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/JsonDialogFragment.kt57
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/ReserveTransaction.kt59
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/Transaction.kt459
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt199
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt127
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt83
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt113
7 files changed, 1097 insertions, 0 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/JsonDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/JsonDialogFragment.kt
new file mode 100644
index 0000000..4139da8
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/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.transactions
+
+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 = arguments!!.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/transactions/ReserveTransaction.kt b/wallet/src/main/java/net/taler/wallet/transactions/ReserveTransaction.kt
new file mode 100644
index 0000000..e497e9a
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/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.transactions
+
+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()
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transaction.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transaction.kt
new file mode 100644
index 0000000..5fcabe7
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/Transaction.kt
@@ -0,0 +1,459 @@
+/*
+ * 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.transactions
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.LayoutRes
+import androidx.annotation.StringRes
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+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 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
+)
+
+typealias Transactions = ArrayList<Transaction>
+
+@JsonTypeInfo(
+ use = NAME,
+ include = PROPERTY,
+ property = "type",
+ defaultImpl = UnknownTransaction::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 = ReserveBalanceUpdatedTransaction::class, name = "reserve-balance-updated"),
+ Type(value = WithdrawTransaction::class, name = "withdrawn"),
+ Type(value = OrderAcceptedTransaction::class, name = "order-accepted"),
+ Type(value = OrderRefusedTransaction::class, name = "order-refused"),
+ Type(value = OrderRedirectedTransaction::class, name = "order-redirected"),
+ Type(value = PaymentTransaction::class, name = "payment-sent"),
+ Type(value = PaymentAbortedTransaction::class, name = "payment-aborted"),
+ Type(value = TipAcceptedTransaction::class, name = "tip-accepted"),
+ Type(value = TipDeclinedTransaction::class, name = "tip-declined"),
+ Type(value = RefundTransaction::class, name = "refund"),
+ Type(value = RefreshTransaction::class, name = "refreshed")
+)
+@JsonIgnoreProperties(
+ value = [
+ "eventId"
+ ]
+)
+abstract class Transaction(
+ val timestamp: Timestamp,
+ @get:LayoutRes
+ open val layout: Int = R.layout.transaction_row,
+ @get:LayoutRes
+ open val detailPageLayout: Int = 0,
+ @get:StringRes
+ open val title: Int = 0,
+ @get:DrawableRes
+ open val icon: Int = R.drawable.ic_account_balance,
+ open val showToUser: Boolean = false
+) {
+ open lateinit var json: JSONObject
+}
+
+
+class UnknownTransaction(timestamp: Timestamp) : Transaction(timestamp) {
+ override val title = R.string.transaction_unknown
+}
+
+@JsonTypeName("exchange-added")
+class ExchangeAddedEvent(
+ timestamp: Timestamp,
+ val exchangeBaseUrl: String,
+ val builtIn: Boolean
+) : Transaction(timestamp) {
+ override val title = R.string.history_event_exchange_added
+}
+
+@JsonTypeName("exchange-updated")
+class ExchangeUpdatedEvent(
+ timestamp: Timestamp,
+ val exchangeBaseUrl: String
+) : Transaction(timestamp) {
+ override val title = R.string.history_event_exchange_updated
+}
+
+
+@JsonTypeName("reserve-balance-updated")
+class ReserveBalanceUpdatedTransaction(
+ timestamp: Timestamp,
+ /**
+ * 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
+) : Transaction(timestamp) {
+ override val title = R.string.transaction_reserve_balance_updated
+}
+
+@JsonTypeName("withdrawn")
+class WithdrawTransaction(
+ timestamp: Timestamp,
+ /**
+ * 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
+) : Transaction(timestamp) {
+ override val layout = R.layout.transaction_in
+ override val detailPageLayout = R.layout.fragment_event_withdraw
+ override val title = R.string.transaction_withdrawal
+ override val icon = R.drawable.transaction_withdrawal
+ override val showToUser = true
+}
+
+@JsonTypeName("order-accepted")
+class OrderAcceptedTransaction(
+ timestamp: Timestamp,
+ /**
+ * Condensed info about the order.
+ */
+ val orderShortInfo: OrderShortInfo
+) : Transaction(timestamp) {
+ override val icon = R.drawable.ic_add_circle
+ override val title = R.string.transaction_order_accepted
+}
+
+@JsonTypeName("order-refused")
+class OrderRefusedTransaction(
+ timestamp: Timestamp,
+ /**
+ * Condensed info about the order.
+ */
+ val orderShortInfo: OrderShortInfo
+) : Transaction(timestamp) {
+ override val icon = R.drawable.ic_cancel
+ override val title = R.string.transaction_order_refused
+}
+
+@JsonTypeName("payment-sent")
+class PaymentTransaction(
+ timestamp: Timestamp,
+ /**
+ * 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?
+) : Transaction(timestamp) {
+ override val layout = R.layout.transaction_out
+ override val detailPageLayout = R.layout.fragment_event_paid
+ override val title = R.string.transaction_payment
+ override val icon = R.drawable.ic_cash_usd_outline
+ override val showToUser = true
+}
+
+@JsonTypeName("payment-aborted")
+class PaymentAbortedTransaction(
+ timestamp: Timestamp,
+ /**
+ * 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
+) : Transaction(timestamp) {
+ override val layout = R.layout.transaction_out
+ override val title = R.string.transaction_payment_aborted
+ override val icon = R.drawable.transaction_payment_aborted
+ override val showToUser = true
+}
+
+@JsonTypeName("refreshed")
+class RefreshTransaction(
+ timestamp: Timestamp,
+ /**
+ * 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
+) : Transaction(timestamp) {
+ override val layout = R.layout.transaction_out
+ override val icon = R.drawable.transaction_refresh
+ override val title = R.string.transaction_refresh
+ override val showToUser = !(amountRefreshedRaw - amountRefreshedEffective).isZero()
+}
+
+@JsonTypeName("order-redirected")
+class OrderRedirectedTransaction(
+ timestamp: Timestamp,
+ /**
+ * 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
+) : Transaction(timestamp) {
+ override val icon = R.drawable.ic_directions
+ override val title = R.string.transaction_order_redirected
+}
+
+@JsonTypeName("tip-accepted")
+class TipAcceptedTransaction(
+ timestamp: Timestamp,
+ /**
+ * 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
+) : Transaction(timestamp) {
+ override val icon = R.drawable.transaction_tip_accepted
+ override val title = R.string.transaction_tip_accepted
+ override val layout = R.layout.transaction_in
+ override val showToUser = true
+}
+
+@JsonTypeName("tip-declined")
+class TipDeclinedTransaction(
+ timestamp: Timestamp,
+ /**
+ * 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
+) : Transaction(timestamp) {
+ override val icon = R.drawable.transaction_tip_declined
+ override val title = R.string.transaction_tip_declined
+ override val layout = R.layout.transaction_in
+ override val showToUser = true
+}
+
+@JsonTypeName("refund")
+class RefundTransaction(
+ timestamp: Timestamp,
+ 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
+) : Transaction(timestamp) {
+ override val icon = R.drawable.transaction_refund
+ override val title = R.string.transaction_refund
+ override val layout = R.layout.transaction_in
+ override val detailPageLayout = R.layout.fragment_event_paid
+ override val showToUser = true
+}
+
+@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/transactions/TransactionAdapter.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
new file mode 100644
index 0000000..beebcda
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
@@ -0,0 +1,199 @@
+/*
+ * 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.transactions
+
+import android.content.Context
+import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.CallSuper
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import net.taler.common.toRelativeTime
+import net.taler.wallet.R
+import net.taler.wallet.cleanExchange
+import net.taler.wallet.transactions.TransactionAdapter.TransactionViewHolder
+
+
+internal class TransactionAdapter(
+ private val devMode: Boolean,
+ private val listener: OnEventClickListener,
+ private var transactions: Transactions = Transactions()
+) : Adapter<TransactionViewHolder>() {
+
+ init {
+ setHasStableIds(false)
+ }
+
+ override fun getItemViewType(position: Int): Int = transactions[position].layout
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransactionViewHolder {
+ val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
+ return when (viewType) {
+ R.layout.transaction_in -> TransactionInViewHolder(view)
+ R.layout.transaction_out -> TransactionOutViewHolder(view)
+ else -> GenericTransactionViewHolder(view)
+ }
+ }
+
+ override fun getItemCount(): Int = transactions.size
+
+ override fun onBindViewHolder(holder: TransactionViewHolder, position: Int) {
+ val event = transactions[position]
+ holder.bind(event)
+ }
+
+ fun update(updatedTransactions: Transactions) {
+ this.transactions = updatedTransactions
+ this.notifyDataSetChanged()
+ }
+
+ internal abstract inner class TransactionViewHolder(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 selectableBackground = v.background
+
+ @CallSuper
+ open fun bind(event: Transaction) {
+ if (devMode || event.detailPageLayout != 0) {
+ v.background = selectableBackground
+ v.setOnClickListener { listener.onEventClicked(event) }
+ } else {
+ v.background = null
+ v.setOnClickListener(null)
+ }
+ icon.setImageResource(event.icon)
+ if (event.title == 0) title.text = event::class.java.simpleName
+ else title.setText(event.title)
+ time.text = event.timestamp.ms.toRelativeTime(context)
+ }
+
+ }
+
+ internal inner class GenericTransactionViewHolder(v: View) : TransactionViewHolder(v) {
+
+ private val info: TextView = v.findViewById(R.id.info)
+
+ override fun bind(transaction: Transaction) {
+ super.bind(transaction)
+ info.text = when (transaction) {
+ is ExchangeAddedEvent -> cleanExchange(transaction.exchangeBaseUrl)
+ is ExchangeUpdatedEvent -> cleanExchange(transaction.exchangeBaseUrl)
+ is ReserveBalanceUpdatedTransaction -> transaction.reserveBalance.toString()
+ is PaymentTransaction -> transaction.orderShortInfo.summary
+ is OrderAcceptedTransaction -> transaction.orderShortInfo.summary
+ is OrderRefusedTransaction -> transaction.orderShortInfo.summary
+ is OrderRedirectedTransaction -> transaction.newOrderShortInfo.summary
+ else -> ""
+ }
+ }
+
+ }
+
+ internal inner class TransactionInViewHolder(v: View) : TransactionViewHolder(v) {
+
+ private val summary: TextView = v.findViewById(R.id.summary)
+ private val amountWithdrawn: TextView = v.findViewById(R.id.amountWithdrawn)
+ private val paintFlags = amountWithdrawn.paintFlags
+
+ override fun bind(event: Transaction) {
+ super.bind(event)
+ when (event) {
+ is WithdrawTransaction -> bind(event)
+ is RefundTransaction -> bind(event)
+ is TipAcceptedTransaction -> bind(event)
+ is TipDeclinedTransaction -> bind(event)
+ }
+ }
+
+ private fun bind(event: WithdrawTransaction) {
+ summary.text = cleanExchange(event.exchangeBaseUrl)
+ amountWithdrawn.text =
+ context.getString(R.string.amount_positive, event.amountWithdrawnEffective)
+ amountWithdrawn.paintFlags = paintFlags
+ }
+
+ private fun bind(event: RefundTransaction) {
+ summary.text = event.orderShortInfo.summary
+ amountWithdrawn.text =
+ context.getString(R.string.amount_positive, event.amountRefundedEffective)
+ amountWithdrawn.paintFlags = paintFlags
+ }
+
+ private fun bind(transaction: TipAcceptedTransaction) {
+ summary.text = null
+ amountWithdrawn.text = context.getString(R.string.amount_positive, transaction.tipRaw)
+ amountWithdrawn.paintFlags = paintFlags
+ }
+
+ private fun bind(transaction: TipDeclinedTransaction) {
+ summary.text = null
+ amountWithdrawn.text = context.getString(R.string.amount_positive, transaction.tipAmount)
+ amountWithdrawn.paintFlags = amountWithdrawn.paintFlags or STRIKE_THRU_TEXT_FLAG
+ }
+
+ }
+
+ internal inner class TransactionOutViewHolder(v: View) : TransactionViewHolder(v) {
+
+ private val summary: TextView = v.findViewById(R.id.summary)
+ private val amountPaidWithFees: TextView = v.findViewById(R.id.amountPaidWithFees)
+
+ override fun bind(event: Transaction) {
+ super.bind(event)
+ when (event) {
+ is PaymentTransaction -> bind(event)
+ is PaymentAbortedTransaction -> bind(event)
+ is RefreshTransaction -> bind(event)
+ }
+ }
+
+ private fun bind(event: PaymentTransaction) {
+ summary.text = event.orderShortInfo.summary
+ amountPaidWithFees.text =
+ context.getString(R.string.amount_negative, event.amountPaidWithFees)
+ }
+
+ private fun bind(transaction: PaymentAbortedTransaction) {
+ summary.text = transaction.orderShortInfo.summary
+ amountPaidWithFees.text = context.getString(R.string.amount_negative, transaction.amountLost)
+ }
+
+ private fun bind(event: RefreshTransaction) {
+ val res = when (event.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
+ }
+ summary.text = context.getString(res)
+ val fee = event.amountRefreshedRaw - event.amountRefreshedEffective
+ if (fee.isZero()) amountPaidWithFees.text = null
+ else amountPaidWithFees.text = context.getString(R.string.amount_negative, fee)
+ }
+
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt
new file mode 100644
index 0000000..f198215
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.transactions
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import android.widget.Toast.LENGTH_LONG
+import androidx.core.content.ContextCompat.getColor
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import kotlinx.android.synthetic.main.fragment_event_paid.*
+import kotlinx.android.synthetic.main.fragment_event_withdraw.*
+import kotlinx.android.synthetic.main.fragment_event_withdraw.feeView
+import kotlinx.android.synthetic.main.fragment_event_withdraw.timeView
+import net.taler.common.Amount
+import net.taler.common.toAbsoluteTime
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.cleanExchange
+
+class TransactionDetailFragment : Fragment() {
+
+ private val model: MainViewModel by activityViewModels()
+ private val transactionManager by lazy { model.transactionManager }
+ private val event by lazy { requireNotNull(transactionManager.selectedEvent) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(model.devMode.value == true)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(event.detailPageLayout, container, false)
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ requireActivity().title =
+ getString(if (event.title != 0) event.title else R.string.transactions_detail_title)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ timeView.text = event.timestamp.ms.toAbsoluteTime(requireContext())
+ when (val e = event) {
+ is WithdrawTransaction -> bind(e)
+ is PaymentTransaction -> bind(e)
+ is RefundTransaction -> bind(e)
+ else -> Toast.makeText(
+ requireContext(),
+ "event ${e.javaClass} not implement",
+ LENGTH_LONG
+ ).show()
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.transactions_detail, menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.show_json -> {
+ JsonDialogFragment.new(event.json.toString(2)).show(parentFragmentManager, null)
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ private fun bind(event: WithdrawTransaction) {
+ effectiveAmountLabel.text = getString(R.string.withdraw_total)
+ effectiveAmountView.text = event.amountWithdrawnEffective.toString()
+ chosenAmountLabel.text = getString(R.string.amount_chosen)
+ chosenAmountView.text =
+ getString(R.string.amount_positive, event.amountWithdrawnRaw.toString())
+ val fee = event.amountWithdrawnRaw - event.amountWithdrawnEffective
+ feeView.text = getString(R.string.amount_negative, fee.toString())
+ exchangeView.text = cleanExchange(event.exchangeBaseUrl)
+ }
+
+ private fun bind(event: PaymentTransaction) {
+ amountPaidWithFeesView.text = event.amountPaidWithFees.toString()
+ val fee = event.amountPaidWithFees - event.orderShortInfo.amount
+ bindOrderAndFee(event.orderShortInfo, fee)
+ }
+
+ private fun bind(event: RefundTransaction) {
+ amountPaidWithFeesLabel.text = getString(R.string.transaction_refund)
+ amountPaidWithFeesView.setTextColor(getColor(requireContext(), R.color.green))
+ amountPaidWithFeesView.text =
+ getString(R.string.amount_positive, event.amountRefundedEffective.toString())
+ val fee = event.orderShortInfo.amount - event.amountRefundedEffective
+ bindOrderAndFee(event.orderShortInfo, fee)
+ }
+
+ private fun bindOrderAndFee(orderShortInfo: OrderShortInfo, fee: Amount) {
+ orderAmountView.text = orderShortInfo.amount.toString()
+ feeView.text = getString(R.string.amount_negative, fee.toString())
+ orderSummaryView.text = orderShortInfo.summary
+ orderIdView.text =
+ getString(R.string.transaction_order_id, orderShortInfo.orderId)
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
new file mode 100644
index 0000000..c4ab785
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.transactions
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.asLiveData
+import androidx.lifecycle.switchMap
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import net.taler.wallet.backend.WalletBackendApi
+
+sealed class TransactionsResult {
+ object Error : TransactionsResult()
+ class Success(val transactions: Transactions) : TransactionsResult()
+}
+
+@Suppress("EXPERIMENTAL_API_USAGE")
+class TransactionManager(
+ private val walletBackendApi: WalletBackendApi,
+ private val mapper: ObjectMapper
+) {
+
+ private val mProgress = MutableLiveData<Boolean>()
+ val progress: LiveData<Boolean> = mProgress
+
+ val showAll = MutableLiveData<Boolean>()
+
+ var selectedEvent: Transaction? = null
+
+ val transactions: LiveData<TransactionsResult> = showAll.switchMap { showAll ->
+ loadTransactions(showAll)
+ .onStart { mProgress.postValue(true) }
+ .onCompletion { mProgress.postValue(false) }
+ .asLiveData(Dispatchers.IO)
+ }
+
+ private fun loadTransactions(showAll: Boolean) = callbackFlow {
+ walletBackendApi.sendRequest("getHistory", null) { isError, result ->
+ launch(Dispatchers.Default) {
+ if (isError) {
+ offer(TransactionsResult.Error)
+ close()
+ return@launch
+ }
+ val transactions = Transactions()
+ val json = result.getJSONArray("history")
+ for (i in 0 until json.length()) {
+ val event: Transaction = mapper.readValue(json.getString(i))
+ event.json = json.getJSONObject(i)
+ transactions.add(event)
+ }
+ transactions.reverse() // show latest first
+ val filtered =
+ if (showAll) transactions else transactions.filter { it.showToUser } as Transactions
+ offer(TransactionsResult.Success(filtered))
+ close()
+ }
+ }
+ awaitClose()
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
new file mode 100644
index 0000000..663a5aa
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.transactions
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+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.navigation.fragment.findNavController
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+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
+
+interface OnEventClickListener {
+ fun onEventClicked(event: Transaction)
+}
+
+class TransactionsFragment : Fragment(), OnEventClickListener {
+
+ private val model: MainViewModel by activityViewModels()
+ private val transactionManager by lazy { model.transactionManager }
+ private val transactionAdapter by lazy { TransactionAdapter(model.devMode.value == true, this) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
+ }
+
+ 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?) {
+ list.apply {
+ layoutManager = LinearLayoutManager(context)
+ adapter = transactionAdapter
+ addItemDecoration(DividerItemDecoration(context, VERTICAL))
+ }
+
+ transactionManager.progress.observe(viewLifecycleOwner, Observer { show ->
+ progressBar.visibility = if (show) VISIBLE else INVISIBLE
+ })
+ transactionManager.transactions.observe(viewLifecycleOwner, Observer { result ->
+ onTransactionsResult(result)
+ })
+
+ // kicks off initial load, needs to be adapted if showAll state is ever saved
+ if (savedInstanceState == null) transactionManager.showAll.value = model.devMode.value
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.transactions, menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onEventClicked(event: Transaction) {
+ if (event.detailPageLayout != 0) {
+ transactionManager.selectedEvent = event
+ findNavController().navigate(R.id.action_nav_transactions_to_nav_transaction_detail)
+ } else if (model.devMode.value == true) {
+ JsonDialogFragment.new(event.json.toString(2))
+ .show(parentFragmentManager, null)
+ }
+ }
+
+ private fun onTransactionsResult(result: TransactionsResult) = when (result) {
+ TransactionsResult.Error -> {
+ list.fadeOut()
+ emptyState.text = getString(R.string.transactions_error)
+ emptyState.fadeIn()
+ }
+ is TransactionsResult.Success -> {
+ emptyState.visibility = if (result.transactions.isEmpty()) VISIBLE else INVISIBLE
+ transactionAdapter.update(result.transactions)
+ }
+ }
+
+}