taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

commit ea3250845fb266a2ecd5ebeba561bc99101bf3de
parent 8e4f85d467c8e8109026a7195757ce9448ad7b19
Author: Torsten Grote <t@grobox.de>
Date:   Wed, 15 Apr 2020 11:26:51 -0300

[wallet] rename history to transactions

Diffstat:
Mwallet/src/main/java/net/taler/wallet/MainFragment.kt | 4++--
Mwallet/src/main/java/net/taler/wallet/MainViewModel.kt | 4++--
Dwallet/src/main/java/net/taler/wallet/history/HistoryAdapter.kt | 199-------------------------------------------------------------------------------
Dwallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt | 459-------------------------------------------------------------------------------
Dwallet/src/main/java/net/taler/wallet/history/HistoryEventFragment.kt | 127-------------------------------------------------------------------------------
Dwallet/src/main/java/net/taler/wallet/history/HistoryFragment.kt | 132-------------------------------------------------------------------------------
Dwallet/src/main/java/net/taler/wallet/history/HistoryManager.kt | 79-------------------------------------------------------------------------------
Dwallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt | 57---------------------------------------------------------
Dwallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt | 59-----------------------------------------------------------
Awallet/src/main/java/net/taler/wallet/transactions/JsonDialogFragment.kt | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/java/net/taler/wallet/transactions/ReserveTransaction.kt | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/java/net/taler/wallet/transactions/Transaction.kt | 459+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rwallet/src/main/res/drawable/history_payment_aborted.xml -> wallet/src/main/res/drawable/transaction_payment_aborted.xml | 0
Rwallet/src/main/res/drawable/history_refresh.xml -> wallet/src/main/res/drawable/transaction_refresh.xml | 0
Rwallet/src/main/res/drawable/history_refund.xml -> wallet/src/main/res/drawable/transaction_refund.xml | 0
Rwallet/src/main/res/drawable/history_tip_accepted.xml -> wallet/src/main/res/drawable/transaction_tip_accepted.xml | 0
Rwallet/src/main/res/drawable/history_tip_declined.xml -> wallet/src/main/res/drawable/transaction_tip_declined.xml | 0
Rwallet/src/main/res/drawable/history_withdrawn.xml -> wallet/src/main/res/drawable/transaction_withdrawal.xml | 0
Mwallet/src/main/res/layout/fragment_event_paid.xml | 30+++++++++++++++---------------
Mwallet/src/main/res/layout/fragment_event_withdraw.xml | 12++++++------
Dwallet/src/main/res/layout/fragment_show_history.xml | 46----------------------------------------------
Awallet/src/main/res/layout/fragment_transactions.xml | 46++++++++++++++++++++++++++++++++++++++++++++++
Dwallet/src/main/res/layout/history_payment.xml | 87-------------------------------------------------------------------------------
Dwallet/src/main/res/layout/history_receive.xml | 92-------------------------------------------------------------------------------
Dwallet/src/main/res/layout/history_row.xml | 77-----------------------------------------------------------------------------
Awallet/src/main/res/layout/transaction_in.xml | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/res/layout/transaction_out.xml | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/main/res/layout/transaction_row.xml | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rwallet/src/main/res/menu/balance.xml -> wallet/src/main/res/menu/fragment_main.xml | 0
Dwallet/src/main/res/menu/history.xml | 30------------------------------
Dwallet/src/main/res/menu/history_event.xml | 23-----------------------
Rwallet/src/main/res/menu/balance.xml -> wallet/src/main/res/menu/transactions.xml | 0
Awallet/src/main/res/menu/transactions_detail.xml | 23+++++++++++++++++++++++
Mwallet/src/main/res/navigation/nav_graph.xml | 21++++++++++-----------
Mwallet/src/main/res/values/strings.xml | 74+++++++++++++++++++++++++++++++++-----------------------------------------
Mwallet/src/main/res/values/styles.xml | 8++++----
Mwallet/src/main/res/xml/settings_main.xml | 2+-
Dwallet/src/test/java/net/taler/wallet/history/HistoryEventTest.kt | 470-------------------------------------------------------------------------------
Dwallet/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt | 52----------------------------------------------------
Awallet/src/test/java/net/taler/wallet/transactions/ReserveTransactionTest.kt | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Awallet/src/test/java/net/taler/wallet/transactions/TransactionTest.kt | 470+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
45 files changed, 2017 insertions(+), 2071 deletions(-)

diff --git a/wallet/src/main/java/net/taler/wallet/MainFragment.kt b/wallet/src/main/java/net/taler/wallet/MainFragment.kt @@ -88,7 +88,7 @@ class MainFragment : Fragment(), BalanceClickListener { } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.balance, menu) + inflater.inflate(R.menu.fragment_main, menu) super.onCreateOptionsMenu(menu, inflater) } @@ -117,7 +117,7 @@ class MainFragment : Fragment(), BalanceClickListener { } override fun onBalanceClick(currency: String) { - findNavController().navigate(R.id.walletHistory) + findNavController().navigate(R.id.nav_transactions) } } diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt @@ -28,7 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import net.taler.common.Amount import net.taler.wallet.backend.WalletBackendApi -import net.taler.wallet.history.HistoryManager +import net.taler.wallet.transactions.TransactionManager import net.taler.wallet.payment.PaymentManager import net.taler.wallet.pending.PendingOperationsManager import net.taler.wallet.refund.RefundManager @@ -66,7 +66,7 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) { val withdrawManager = WithdrawManager(walletBackendApi) val paymentManager = PaymentManager(walletBackendApi, mapper) val pendingOperationsManager = PendingOperationsManager(walletBackendApi) - val historyManager = HistoryManager(walletBackendApi, mapper) + val transactionManager = TransactionManager(walletBackendApi, mapper) val refundManager = RefundManager(walletBackendApi) override fun onCleared() { diff --git a/wallet/src/main/java/net/taler/wallet/history/HistoryAdapter.kt b/wallet/src/main/java/net/taler/wallet/history/HistoryAdapter.kt @@ -1,199 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet.history - -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.history.HistoryAdapter.HistoryEventViewHolder - - -internal class HistoryAdapter( - private val devMode: Boolean, - private val listener: OnEventClickListener, - private var history: History = History() -) : Adapter<HistoryEventViewHolder>() { - - init { - setHasStableIds(false) - } - - override fun getItemViewType(position: Int): Int = history[position].layout - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryEventViewHolder { - val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) - return when (viewType) { - R.layout.history_receive -> HistoryReceiveViewHolder(view) - R.layout.history_payment -> HistoryPaymentViewHolder(view) - else -> GenericHistoryEventViewHolder(view) - } - } - - override fun getItemCount(): Int = history.size - - override fun onBindViewHolder(holder: HistoryEventViewHolder, position: Int) { - val event = history[position] - holder.bind(event) - } - - fun update(updatedHistory: History) { - this.history = updatedHistory - this.notifyDataSetChanged() - } - - internal abstract inner class HistoryEventViewHolder(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: HistoryEvent) { - 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 GenericHistoryEventViewHolder(v: View) : HistoryEventViewHolder(v) { - - private val info: TextView = v.findViewById(R.id.info) - - override fun bind(event: HistoryEvent) { - super.bind(event) - info.text = when (event) { - is ExchangeAddedEvent -> cleanExchange(event.exchangeBaseUrl) - is ExchangeUpdatedEvent -> cleanExchange(event.exchangeBaseUrl) - is ReserveBalanceUpdatedEvent -> event.reserveBalance.toString() - is HistoryPaymentSentEvent -> event.orderShortInfo.summary - is HistoryOrderAcceptedEvent -> event.orderShortInfo.summary - is HistoryOrderRefusedEvent -> event.orderShortInfo.summary - is HistoryOrderRedirectedEvent -> event.newOrderShortInfo.summary - else -> "" - } - } - - } - - internal inner class HistoryReceiveViewHolder(v: View) : HistoryEventViewHolder(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: HistoryEvent) { - super.bind(event) - when (event) { - is HistoryWithdrawnEvent -> bind(event) - is HistoryRefundedEvent -> bind(event) - is HistoryTipAcceptedEvent -> bind(event) - is HistoryTipDeclinedEvent -> bind(event) - } - } - - private fun bind(event: HistoryWithdrawnEvent) { - summary.text = cleanExchange(event.exchangeBaseUrl) - amountWithdrawn.text = - context.getString(R.string.amount_positive, event.amountWithdrawnEffective) - amountWithdrawn.paintFlags = paintFlags - } - - private fun bind(event: HistoryRefundedEvent) { - summary.text = event.orderShortInfo.summary - amountWithdrawn.text = - context.getString(R.string.amount_positive, event.amountRefundedEffective) - amountWithdrawn.paintFlags = paintFlags - } - - private fun bind(event: HistoryTipAcceptedEvent) { - summary.text = null - amountWithdrawn.text = context.getString(R.string.amount_positive, event.tipRaw) - amountWithdrawn.paintFlags = paintFlags - } - - private fun bind(event: HistoryTipDeclinedEvent) { - summary.text = null - amountWithdrawn.text = context.getString(R.string.amount_positive, event.tipAmount) - amountWithdrawn.paintFlags = amountWithdrawn.paintFlags or STRIKE_THRU_TEXT_FLAG - } - - } - - internal inner class HistoryPaymentViewHolder(v: View) : HistoryEventViewHolder(v) { - - private val summary: TextView = v.findViewById(R.id.summary) - private val amountPaidWithFees: TextView = v.findViewById(R.id.amountPaidWithFees) - - override fun bind(event: HistoryEvent) { - super.bind(event) - when (event) { - is HistoryPaymentSentEvent -> bind(event) - is HistoryPaymentAbortedEvent -> bind(event) - is HistoryRefreshedEvent -> bind(event) - } - } - - private fun bind(event: HistoryPaymentSentEvent) { - summary.text = event.orderShortInfo.summary - amountPaidWithFees.text = - context.getString(R.string.amount_negative, event.amountPaidWithFees) - } - - private fun bind(event: HistoryPaymentAbortedEvent) { - summary.text = event.orderShortInfo.summary - amountPaidWithFees.text = context.getString(R.string.amount_negative, event.amountLost) - } - - private fun bind(event: HistoryRefreshedEvent) { - val res = when (event.refreshReason) { - RefreshReason.MANUAL -> R.string.history_event_refresh_reason_manual - RefreshReason.PAY -> R.string.history_event_refresh_reason_pay - RefreshReason.REFUND -> R.string.history_event_refresh_reason_refund - RefreshReason.ABORT_PAY -> R.string.history_event_refresh_reason_abort_pay - RefreshReason.RECOUP -> R.string.history_event_refresh_reason_recoup - RefreshReason.BACKUP_RESTORED -> R.string.history_event_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/history/HistoryEvent.kt b/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt @@ -1,459 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet.history - -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 History = ArrayList<HistoryEvent> - -@JsonTypeInfo( - use = NAME, - include = PROPERTY, - property = "type", - defaultImpl = HistoryUnknownEvent::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 = ReserveBalanceUpdatedEvent::class, name = "reserve-balance-updated"), - Type(value = HistoryWithdrawnEvent::class, name = "withdrawn"), - Type(value = HistoryOrderAcceptedEvent::class, name = "order-accepted"), - Type(value = HistoryOrderRefusedEvent::class, name = "order-refused"), - Type(value = HistoryOrderRedirectedEvent::class, name = "order-redirected"), - Type(value = HistoryPaymentSentEvent::class, name = "payment-sent"), - Type(value = HistoryPaymentAbortedEvent::class, name = "payment-aborted"), - Type(value = HistoryTipAcceptedEvent::class, name = "tip-accepted"), - Type(value = HistoryTipDeclinedEvent::class, name = "tip-declined"), - Type(value = HistoryRefundedEvent::class, name = "refund"), - Type(value = HistoryRefreshedEvent::class, name = "refreshed") -) -@JsonIgnoreProperties( - value = [ - "eventId" - ] -) -abstract class HistoryEvent( - val timestamp: Timestamp, - @get:LayoutRes - open val layout: Int = R.layout.history_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 HistoryUnknownEvent(timestamp: Timestamp) : HistoryEvent(timestamp) { - override val title = R.string.history_event_unknown -} - -@JsonTypeName("exchange-added") -class ExchangeAddedEvent( - timestamp: Timestamp, - val exchangeBaseUrl: String, - val builtIn: Boolean -) : HistoryEvent(timestamp) { - override val title = R.string.history_event_exchange_added -} - -@JsonTypeName("exchange-updated") -class ExchangeUpdatedEvent( - timestamp: Timestamp, - val exchangeBaseUrl: String -) : HistoryEvent(timestamp) { - override val title = R.string.history_event_exchange_updated -} - - -@JsonTypeName("reserve-balance-updated") -class ReserveBalanceUpdatedEvent( - 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 -) : HistoryEvent(timestamp) { - override val title = R.string.history_event_reserve_balance_updated -} - -@JsonTypeName("withdrawn") -class HistoryWithdrawnEvent( - 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 -) : HistoryEvent(timestamp) { - override val layout = R.layout.history_receive - override val detailPageLayout = R.layout.fragment_event_withdraw - override val title = R.string.history_event_withdrawn - override val icon = R.drawable.history_withdrawn - override val showToUser = true -} - -@JsonTypeName("order-accepted") -class HistoryOrderAcceptedEvent( - timestamp: Timestamp, - /** - * Condensed info about the order. - */ - val orderShortInfo: OrderShortInfo -) : HistoryEvent(timestamp) { - override val icon = R.drawable.ic_add_circle - override val title = R.string.history_event_order_accepted -} - -@JsonTypeName("order-refused") -class HistoryOrderRefusedEvent( - timestamp: Timestamp, - /** - * Condensed info about the order. - */ - val orderShortInfo: OrderShortInfo -) : HistoryEvent(timestamp) { - override val icon = R.drawable.ic_cancel - override val title = R.string.history_event_order_refused -} - -@JsonTypeName("payment-sent") -class HistoryPaymentSentEvent( - 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? -) : HistoryEvent(timestamp) { - override val layout = R.layout.history_payment - override val detailPageLayout = R.layout.fragment_event_paid - override val title = R.string.history_event_payment_sent - override val icon = R.drawable.ic_cash_usd_outline - override val showToUser = true -} - -@JsonTypeName("payment-aborted") -class HistoryPaymentAbortedEvent( - 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 -) : HistoryEvent(timestamp) { - override val layout = R.layout.history_payment - override val title = R.string.history_event_payment_aborted - override val icon = R.drawable.history_payment_aborted - override val showToUser = true -} - -@JsonTypeName("refreshed") -class HistoryRefreshedEvent( - 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 -) : HistoryEvent(timestamp) { - override val layout = R.layout.history_payment - override val icon = R.drawable.history_refresh - override val title = R.string.history_event_refreshed - override val showToUser = !(amountRefreshedRaw - amountRefreshedEffective).isZero() -} - -@JsonTypeName("order-redirected") -class HistoryOrderRedirectedEvent( - 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 -) : HistoryEvent(timestamp) { - override val icon = R.drawable.ic_directions - override val title = R.string.history_event_order_redirected -} - -@JsonTypeName("tip-accepted") -class HistoryTipAcceptedEvent( - 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 -) : HistoryEvent(timestamp) { - override val icon = R.drawable.history_tip_accepted - override val title = R.string.history_event_tip_accepted - override val layout = R.layout.history_receive - override val showToUser = true -} - -@JsonTypeName("tip-declined") -class HistoryTipDeclinedEvent( - 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 -) : HistoryEvent(timestamp) { - override val icon = R.drawable.history_tip_declined - override val title = R.string.history_event_tip_declined - override val layout = R.layout.history_receive - override val showToUser = true -} - -@JsonTypeName("refund") -class HistoryRefundedEvent( - 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 -) : HistoryEvent(timestamp) { - override val icon = R.drawable.history_refund - override val title = R.string.history_event_refund - override val layout = R.layout.history_receive - 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/history/HistoryEventFragment.kt b/wallet/src/main/java/net/taler/wallet/history/HistoryEventFragment.kt @@ -1,127 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet.history - -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.R -import net.taler.wallet.MainViewModel -import net.taler.wallet.cleanExchange - -class HistoryEventFragment : Fragment() { - - private val model: MainViewModel by activityViewModels() - private val historyManager by lazy { model.historyManager } - private val event by lazy { requireNotNull(historyManager.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.history_detail_title) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - timeView.text = event.timestamp.ms.toAbsoluteTime(requireContext()) - when (val e = event) { - is HistoryWithdrawnEvent -> bind(e) - is HistoryPaymentSentEvent -> bind(e) - is HistoryRefundedEvent -> 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.history_event, 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: HistoryWithdrawnEvent) { - 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: HistoryPaymentSentEvent) { - amountPaidWithFeesView.text = event.amountPaidWithFees.toString() - val fee = event.amountPaidWithFees - event.orderShortInfo.amount - bindOrderAndFee(event.orderShortInfo, fee) - } - - private fun bind(event: HistoryRefundedEvent) { - amountPaidWithFeesLabel.text = getString(R.string.history_event_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.history_event_payment_sent_order_id, orderShortInfo.orderId) - } - -} diff --git a/wallet/src/main/java/net/taler/wallet/history/HistoryFragment.kt b/wallet/src/main/java/net/taler/wallet/history/HistoryFragment.kt @@ -1,132 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet.history - -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_show_history.* -import net.taler.common.fadeIn -import net.taler.common.fadeOut -import net.taler.wallet.R -import net.taler.wallet.MainViewModel - -interface OnEventClickListener { - fun onEventClicked(event: HistoryEvent) -} - -class HistoryFragment : Fragment(), OnEventClickListener { - - private val model: MainViewModel by activityViewModels() - private val historyManager by lazy { model.historyManager } - private lateinit var showAllItem: MenuItem - private var reloadHistoryItem: MenuItem? = null - private val historyAdapter by lazy { HistoryAdapter(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_show_history, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - historyList.apply { - layoutManager = LinearLayoutManager(context) - adapter = historyAdapter - addItemDecoration(DividerItemDecoration(context, VERTICAL)) - } - - model.devMode.observe(viewLifecycleOwner, Observer { enabled -> - reloadHistoryItem?.isVisible = enabled - }) - historyManager.progress.observe(viewLifecycleOwner, Observer { show -> - historyProgressBar.visibility = if (show) VISIBLE else INVISIBLE - }) - historyManager.history.observe(viewLifecycleOwner, Observer { history -> - onHistoryResult(history) - }) - - // kicks off initial load, needs to be adapted if showAll state is ever saved - if (savedInstanceState == null) historyManager.showAll.value = model.devMode.value - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.history, menu) - showAllItem = menu.findItem(R.id.show_all_history) - showAllItem.isChecked = historyManager.showAll.value == true - reloadHistoryItem = menu.findItem(R.id.reload_history).apply { - isVisible = model.devMode.value!! - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.show_all_history -> { - item.isChecked = !item.isChecked - historyManager.showAll.value = item.isChecked - true - } - R.id.reload_history -> { - historyManager.showAll.value = showAllItem.isChecked - true - } - else -> super.onOptionsItemSelected(item) - } - } - - override fun onEventClicked(event: HistoryEvent) { - if (event.detailPageLayout != 0) { - historyManager.selectedEvent = event - findNavController().navigate(R.id.action_walletHistory_to_historyEventFragment) - } else if (model.devMode.value == true) { - JsonDialogFragment.new(event.json.toString(2)) - .show(parentFragmentManager, null) - } - } - - private fun onHistoryResult(result: HistoryResult) = when (result) { - HistoryResult.Error -> { - historyList.fadeOut() - historyEmptyState.text = getString(R.string.history_error) - historyEmptyState.fadeIn() - } - is HistoryResult.Success -> { - historyEmptyState.visibility = if (result.history.isEmpty()) VISIBLE else INVISIBLE - historyAdapter.update(result.history) - } - } - -} diff --git a/wallet/src/main/java/net/taler/wallet/history/HistoryManager.kt b/wallet/src/main/java/net/taler/wallet/history/HistoryManager.kt @@ -1,79 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet.history - -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 net.taler.wallet.backend.WalletBackendApi - -sealed class HistoryResult { - object Error : HistoryResult() - class Success(val history: History) : HistoryResult() -} - -@Suppress("EXPERIMENTAL_API_USAGE") -class HistoryManager( - private val walletBackendApi: WalletBackendApi, - private val mapper: ObjectMapper -) { - - private val mProgress = MutableLiveData<Boolean>() - val progress: LiveData<Boolean> = mProgress - - val showAll = MutableLiveData<Boolean>() - - var selectedEvent: HistoryEvent? = null - - val history: LiveData<HistoryResult> = showAll.switchMap { showAll -> - loadHistory(showAll) - .onStart { mProgress.postValue(true) } - .onCompletion { mProgress.postValue(false) } - .asLiveData(Dispatchers.IO) - } - - private fun loadHistory(showAll: Boolean) = callbackFlow { - walletBackendApi.sendRequest("getHistory", null) { isError, result -> - if (isError) { - offer(HistoryResult.Error) - close() - return@sendRequest - } - 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 - val filtered = if (showAll) history else history.filter { it.showToUser } as History - offer(HistoryResult.Success(filtered)) - close() - } - awaitClose() - } - -} diff --git a/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt @@ -1,57 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.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 = 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/history/ReserveTransaction.kt b/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt @@ -1,59 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.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() diff --git a/wallet/src/main/java/net/taler/wallet/transactions/JsonDialogFragment.kt 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) + } + } + +} diff --git a/wallet/src/main/res/drawable/history_payment_aborted.xml b/wallet/src/main/res/drawable/transaction_payment_aborted.xml diff --git a/wallet/src/main/res/drawable/history_refresh.xml b/wallet/src/main/res/drawable/transaction_refresh.xml diff --git a/wallet/src/main/res/drawable/history_refund.xml b/wallet/src/main/res/drawable/transaction_refund.xml diff --git a/wallet/src/main/res/drawable/history_tip_accepted.xml b/wallet/src/main/res/drawable/transaction_tip_accepted.xml diff --git a/wallet/src/main/res/drawable/history_tip_declined.xml b/wallet/src/main/res/drawable/transaction_tip_declined.xml diff --git a/wallet/src/main/res/drawable/history_withdrawn.xml b/wallet/src/main/res/drawable/transaction_withdrawal.xml diff --git a/wallet/src/main/res/layout/fragment_event_paid.xml b/wallet/src/main/res/layout/fragment_event_paid.xml @@ -20,7 +20,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" - tools:context=".history.HistoryEventFragment"> + tools:context=".transactions.TransactionDetailFragment"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" @@ -28,7 +28,7 @@ <TextView android:id="@+id/timeView" - style="@style/HistoryEventLabel.Time" + style="@style/TransactionLabel.Time" app:layout_constraintBottom_toTopOf="@+id/amountPaidWithFeesLabel" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -38,8 +38,8 @@ <TextView android:id="@+id/amountPaidWithFeesLabel" - style="@style/HistoryEventLabel" - android:text="@string/history_event_payment_sent_paid" + style="@style/TransactionLabel" + android:text="@string/transaction_paid" app:layout_constraintBottom_toTopOf="@+id/amountPaidWithFeesView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -47,7 +47,7 @@ <TextView android:id="@+id/amountPaidWithFeesView" - style="@style/HistoryEventContent" + style="@style/TransactionContent" android:textColor="@color/red" app:layout_constraintBottom_toTopOf="@+id/orderAmountLabel" app:layout_constraintEnd_toEndOf="parent" @@ -57,8 +57,8 @@ <TextView android:id="@+id/orderAmountLabel" - style="@style/HistoryEventLabel" - android:text="@string/history_event_payment_sent_amount" + style="@style/TransactionLabel" + android:text="@string/transaction_order_total" app:layout_constraintBottom_toTopOf="@+id/orderAmountView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -66,7 +66,7 @@ <TextView android:id="@+id/orderAmountView" - style="@style/HistoryEventContent" + style="@style/TransactionContent" app:layout_constraintBottom_toTopOf="@+id/feeLabel" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -75,7 +75,7 @@ <TextView android:id="@+id/feeLabel" - style="@style/HistoryEventLabel" + style="@style/TransactionLabel" android:text="@string/withdraw_fees" app:layout_constraintBottom_toTopOf="@+id/feeView" app:layout_constraintEnd_toEndOf="parent" @@ -84,7 +84,7 @@ <TextView android:id="@+id/feeView" - style="@style/HistoryEventContent" + style="@style/TransactionContent" android:textColor="@color/red" app:layout_constraintBottom_toTopOf="@+id/orderSummaryLabel" app:layout_constraintEnd_toEndOf="parent" @@ -94,8 +94,8 @@ <TextView android:id="@+id/orderSummaryLabel" - style="@style/HistoryEventLabel" - android:text="@string/history_event_payment_sent_order" + style="@style/TransactionLabel" + android:text="@string/transaction_order" app:layout_constraintBottom_toTopOf="@+id/orderSummaryView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -103,7 +103,7 @@ <TextView android:id="@+id/orderSummaryView" - style="@style/HistoryEventContent" + style="@style/TransactionContent" app:layout_constraintBottom_toTopOf="@+id/orderIdView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -112,8 +112,8 @@ <TextView android:id="@+id/orderIdView" - style="@style/HistoryEventLabel" - android:text="@string/history_event_payment_sent_order_id" + style="@style/TransactionLabel" + android:text="@string/transaction_order_id" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/wallet/src/main/res/layout/fragment_event_withdraw.xml b/wallet/src/main/res/layout/fragment_event_withdraw.xml @@ -20,7 +20,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" - tools:context=".history.HistoryEventFragment"> + tools:context=".transactions.TransactionDetailFragment"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" @@ -28,7 +28,7 @@ <TextView android:id="@+id/timeView" - style="@style/HistoryEventLabel.Time" + style="@style/TransactionLabel.Time" app:layout_constraintBottom_toTopOf="@+id/effectiveAmountLabel" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -38,7 +38,7 @@ <TextView android:id="@+id/effectiveAmountLabel" - style="@style/HistoryEventLabel" + style="@style/TransactionLabel" app:layout_constraintBottom_toTopOf="@+id/effectiveAmountView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -64,7 +64,7 @@ <TextView android:id="@+id/chosenAmountLabel" - style="@style/HistoryEventLabel" + style="@style/TransactionLabel" app:layout_constraintBottom_toTopOf="@+id/chosenAmountView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -89,7 +89,7 @@ <TextView android:id="@+id/feeLabel" - style="@style/HistoryEventLabel" + style="@style/TransactionLabel" android:text="@string/withdraw_fees" app:layout_constraintBottom_toTopOf="@+id/feeView" app:layout_constraintEnd_toEndOf="parent" @@ -115,7 +115,7 @@ <TextView android:id="@+id/exchangeLabel" - style="@style/HistoryEventLabel" + style="@style/TransactionLabel" android:text="@string/withdraw_exchange" app:layout_constraintBottom_toTopOf="@+id/exchangeView" app:layout_constraintEnd_toEndOf="parent" diff --git a/wallet/src/main/res/layout/fragment_show_history.xml b/wallet/src/main/res/layout/fragment_show_history.xml @@ -1,46 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ 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/> - --> - -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <androidx.recyclerview.widget.RecyclerView - android:id="@+id/historyList" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:scrollbars="vertical" /> - - <TextView - android:id="@+id/historyEmptyState" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:text="@string/history_empty" - android:visibility="invisible" - tools:visibility="visible" /> - - <ProgressBar - android:id="@+id/historyProgressBar" - style="?android:progressBarStyleLarge" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:visibility="invisible" - tools:visibility="visible" /> - -</FrameLayout> diff --git a/wallet/src/main/res/layout/fragment_transactions.xml b/wallet/src/main/res/layout/fragment_transactions.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/> + --> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="vertical" /> + + <TextView + android:id="@+id/emptyState" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:text="@string/transactions_empty" + android:visibility="invisible" + tools:visibility="visible" /> + + <ProgressBar + android:id="@+id/progressBar" + style="?android:progressBarStyleLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:visibility="invisible" + tools:visibility="visible" /> + +</FrameLayout> diff --git a/wallet/src/main/res/layout/history_payment.xml b/wallet/src/main/res/layout/history_payment.xml @@ -1,87 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ 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/> - --> - -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="?attr/selectableItemBackground" - android:paddingStart="16dp" - android:paddingTop="8dp" - android:paddingEnd="16dp" - android:paddingBottom="8dp"> - - <ImageView - android:id="@+id/icon" - android:layout_width="32dp" - android:layout_height="32dp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:tint="?android:colorControlNormal" - tools:ignore="ContentDescription" - tools:src="@drawable/ic_cash_usd_outline" /> - - <TextView - android:id="@+id/title" - style="@style/HistoryTitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginEnd="8dp" - app:layout_constraintEnd_toStartOf="@+id/amountPaidWithFees" - app:layout_constraintStart_toEndOf="@+id/icon" - app:layout_constraintTop_toTopOf="parent" - tools:text="@string/history_event_payment_sent" /> - - <TextView - android:id="@+id/summary" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginTop="8dp" - app:layout_constrainedWidth="true" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/time" - app:layout_constraintHorizontal_bias="0.0" - app:layout_constraintStart_toEndOf="@+id/icon" - app:layout_constraintTop_toBottomOf="@+id/title" - app:layout_constraintVertical_bias="0.0" - tools:text="Lots of books with very long titles" /> - - <TextView - android:id="@+id/amountPaidWithFees" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textColor="@color/red" - android:textSize="16sp" - app:layout_constraintBottom_toTopOf="@+id/time" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.0" - tools:text="0.2 TESTKUDOS" /> - - <TextView - android:id="@+id/time" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textSize="14sp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - tools:text="23 min ago" /> - -</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/wallet/src/main/res/layout/history_receive.xml b/wallet/src/main/res/layout/history_receive.xml @@ -1,92 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ 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/> - --> - -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="?attr/selectableItemBackground" - android:paddingStart="16dp" - android:paddingTop="8dp" - android:paddingEnd="16dp" - android:paddingBottom="8dp"> - - <ImageView - android:id="@+id/icon" - android:layout_width="32dp" - android:layout_height="32dp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/history_withdrawn" - app:tint="?android:colorControlNormal" - tools:ignore="ContentDescription" /> - - <TextView - android:id="@+id/title" - style="@style/HistoryTitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginEnd="8dp" - android:text="@string/history_event_withdrawn" - app:layout_constraintEnd_toStartOf="@+id/amountWithdrawn" - app:layout_constraintStart_toEndOf="@+id/icon" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/summary" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:layout_marginBottom="8dp" - app:layout_constrainedWidth="true" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/time" - app:layout_constraintHorizontal_bias="0.5" - app:layout_constraintStart_toEndOf="@+id/icon" - app:layout_constraintTop_toBottomOf="@+id/title" - app:layout_constraintVertical_bias="0.0" - tools:text="exchange.taler.quite-long-domain-name.org" /> - - <TextView - android:id="@+id/amountWithdrawn" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textColor="@color/green" - android:textSize="16sp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:text="10 TESTKUDOS" /> - - <TextView - android:id="@+id/time" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="8dp" - android:layout_marginBottom="8dp" - android:textSize="14sp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/summary" - app:layout_constraintTop_toBottomOf="@+id/amountWithdrawn" - app:layout_constraintVertical_bias="1.0" - tools:text="23 min. ago" /> - -</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/wallet/src/main/res/layout/history_row.xml b/wallet/src/main/res/layout/history_row.xml @@ -1,76 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ 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/> - --> - -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="?attr/selectableItemBackground" - android:paddingStart="16dp" - android:paddingTop="8dp" - android:paddingEnd="16dp" - android:paddingBottom="8dp"> - - <ImageView - android:id="@+id/icon" - android:layout_width="32dp" - android:layout_height="32dp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/ic_account_balance" - app:tint="?android:colorControlNormal" - tools:ignore="ContentDescription" /> - - <TextView - android:id="@+id/title" - style="@style/HistoryTitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="16dp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/icon" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_chainStyle="packed" - tools:text="My History Event" /> - - <TextView - android:id="@+id/info" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/time" - app:layout_constraintStart_toEndOf="@+id/icon" - app:layout_constraintTop_toBottomOf="@+id/title" - tools:text="TextView" /> - - <TextView - android:id="@+id/time" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="8dp" - android:gravity="end" - android:textSize="14sp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@+id/title" - tools:text="3 days ago" /> - -</androidx.constraintlayout.widget.ConstraintLayout> -\ No newline at end of file diff --git a/wallet/src/main/res/layout/transaction_in.xml b/wallet/src/main/res/layout/transaction_in.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/> + --> + +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="8dp"> + + <ImageView + android:id="@+id/icon" + android:layout_width="32dp" + android:layout_height="32dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/transaction_withdrawal" + app:tint="?android:colorControlNormal" + tools:ignore="ContentDescription" /> + + <TextView + android:id="@+id/title" + style="@style/TransactionTitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="8dp" + android:text="@string/transaction_withdrawal" + app:layout_constraintEnd_toStartOf="@+id/amountWithdrawn" + app:layout_constraintStart_toEndOf="@+id/icon" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/summary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="8dp" + app:layout_constrainedWidth="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/time" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/icon" + app:layout_constraintTop_toBottomOf="@+id/title" + app:layout_constraintVertical_bias="0.0" + tools:text="exchange.taler.quite-long-domain-name.org" /> + + <TextView + android:id="@+id/amountWithdrawn" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/green" + android:textSize="16sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="10 TESTKUDOS" /> + + <TextView + android:id="@+id/time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:textSize="14sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/summary" + app:layout_constraintTop_toBottomOf="@+id/amountWithdrawn" + app:layout_constraintVertical_bias="1.0" + tools:text="23 min. ago" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/wallet/src/main/res/layout/transaction_out.xml b/wallet/src/main/res/layout/transaction_out.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/> + --> + +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="8dp"> + + <ImageView + android:id="@+id/icon" + android:layout_width="32dp" + android:layout_height="32dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:tint="?android:colorControlNormal" + tools:ignore="ContentDescription" + tools:src="@drawable/ic_cash_usd_outline" /> + + <TextView + android:id="@+id/title" + style="@style/TransactionTitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="8dp" + app:layout_constraintEnd_toStartOf="@+id/amountPaidWithFees" + app:layout_constraintStart_toEndOf="@+id/icon" + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/transaction_payment" /> + + <TextView + android:id="@+id/summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="8dp" + app:layout_constrainedWidth="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/time" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toEndOf="@+id/icon" + app:layout_constraintTop_toBottomOf="@+id/title" + app:layout_constraintVertical_bias="0.0" + tools:text="Lots of books with very long titles" /> + + <TextView + android:id="@+id/amountPaidWithFees" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/red" + android:textSize="16sp" + app:layout_constraintBottom_toTopOf="@+id/time" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.0" + tools:text="0.2 TESTKUDOS" /> + + <TextView + android:id="@+id/time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="14sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + tools:text="23 min ago" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/wallet/src/main/res/layout/transaction_row.xml b/wallet/src/main/res/layout/transaction_row.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/> + --> + +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="8dp"> + + <ImageView + android:id="@+id/icon" + android:layout_width="32dp" + android:layout_height="32dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_account_balance" + app:tint="?android:colorControlNormal" + tools:ignore="ContentDescription" /> + + <TextView + android:id="@+id/title" + style="@style/TransactionTitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/icon" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" + tools:text="My Transaction" /> + + <TextView + android:id="@+id/info" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/time" + app:layout_constraintStart_toEndOf="@+id/icon" + app:layout_constraintTop_toBottomOf="@+id/title" + tools:text="TextView" /> + + <TextView + android:id="@+id/time" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:gravity="end" + android:textSize="14sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/title" + tools:text="3 days ago" /> + +</androidx.constraintlayout.widget.ConstraintLayout> +\ No newline at end of file diff --git a/wallet/src/main/res/menu/balance.xml b/wallet/src/main/res/menu/fragment_main.xml diff --git a/wallet/src/main/res/menu/history.xml b/wallet/src/main/res/menu/history.xml @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ 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/> - --> - -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto"> - <item - android:id="@+id/show_all_history" - android:checkable="true" - android:checked="false" - android:title="@string/history_show_all" - app:showAsAction="never" /> - <item - android:id="@+id/reload_history" - android:orderInCategory="100" - android:title="@string/history_reload" - app:showAsAction="never" /> -</menu> diff --git a/wallet/src/main/res/menu/history_event.xml b/wallet/src/main/res/menu/history_event.xml @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ 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/> - --> - -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto"> - <item - android:id="@+id/show_json" - android:title="@string/history_detail_json" - app:showAsAction="never" /> -</menu> diff --git a/wallet/src/main/res/menu/balance.xml b/wallet/src/main/res/menu/transactions.xml diff --git a/wallet/src/main/res/menu/transactions_detail.xml b/wallet/src/main/res/menu/transactions_detail.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/> + --> + +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/show_json" + android:title="@string/transactions_detail_json" + app:showAsAction="never" /> +</menu> diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml @@ -55,23 +55,22 @@ <fragment android:id="@+id/settings" android:name="net.taler.wallet.SettingsFragment" - android:label="Settings" - tools:layout="@layout/fragment_settings" /> + android:label="Settings" /> <fragment - android:id="@+id/walletHistory" - android:name="net.taler.wallet.history.HistoryFragment" - android:label="@string/history_title" - tools:layout="@layout/fragment_show_history"> + android:id="@+id/nav_transactions" + android:name="net.taler.wallet.transactions.TransactionsFragment" + android:label="@string/transactions_title" + tools:layout="@layout/fragment_transactions"> <action - android:id="@+id/action_walletHistory_to_historyEventFragment" - app:destination="@id/historyEventFragment" /> + android:id="@+id/action_nav_transactions_to_nav_transaction_detail" + app:destination="@id/nav_transactions_detail" /> </fragment> <fragment - android:id="@+id/historyEventFragment" - android:name="net.taler.wallet.history.HistoryEventFragment" - android:label="@string/history_detail_title" + android:id="@+id/nav_transactions_detail" + android:name="net.taler.wallet.transactions.TransactionDetailFragment" + android:label="@string/transactions_detail_title" tools:layout="@layout/fragment_event_withdraw" /> <fragment diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml @@ -30,55 +30,50 @@ <string name="button_continue">Continue</string> <string name="button_scan_qr_code">Scan Taler QR Code</string> - <string name="menu_history">History</string> <string name="menu_settings">Settings</string> - <string name="menu_balance_reload">Reload balances</string> - <string name="menu_developer_mode">Developer Mode</string> <string name="menu_retry_pending_operations">Retry Pending Operations</string> <string name="servicedesc">my service</string> <string name="aiddescription">my aid</string> <string name="balances_title">Balances</string> - <string name="amount_positive">+%s</string> - <string name="amount_negative">-%s</string> + <string name="amount_positive">+ %s</string> + <string name="amount_negative">- %s</string> <string name="amount_chosen">Chosen Amount</string> <string name="balances_inbound_label">inbound</string> <string name="balances_empty_state">There is no digital cash in your wallet.\n\nYou can get test money from the demo bank:\n\nhttps://bank.demo.taler.net</string> - <string name="history_title">History</string> - <string name="history_show_all">Show All</string> - <string name="history_reload">Reload History</string> - <string name="history_empty">The wallet history is empty</string> - <string name="history_error">Could not load history</string> - <string name="history_detail_title">Transaction</string> - <string name="history_detail_json">Show JSON</string> + <string name="transactions_title">Transactions</string> + <string name="transactions_empty">You don\'t have any transactions</string> + <string name="transactions_error">Could not load transactions</string> + <string name="transactions_detail_title">Transaction</string> + <string name="transactions_detail_json">Show JSON</string> - <!-- HistoryEvents --> + <!-- Transactions --> <string name="history_event_exchange_added">Exchange Added</string> <string name="history_event_exchange_updated">Exchange Updated</string> - <string name="history_event_reserve_balance_updated">Reserve Balance Updated</string> - <string name="history_event_payment_sent">Payment</string> - <string name="history_event_payment_sent_paid">Paid</string> - <string name="history_event_payment_sent_amount">Order Total</string> - <string name="history_event_payment_sent_order">Order</string> - <string name="history_event_payment_sent_order_id">Order Reference: %1$s</string> - <string name="history_event_payment_aborted">Payment Aborted</string> - <string name="history_event_withdrawn">Withdraw</string> - <string name="history_event_order_accepted">Purchase Confirmed</string> - <string name="history_event_order_refused">Purchase Cancelled</string> - <string name="history_event_tip_accepted">Tip Accepted</string> - <string name="history_event_tip_declined">Tip Declined</string> - <string name="history_event_order_redirected">Purchase Redirected</string> - <string name="history_event_refund">Refund</string> - <string name="history_event_refreshed">Obtained change</string> - <string name="history_event_refresh_reason_manual">because of manual request</string> - <string name="history_event_refresh_reason_pay">for payment</string> - <string name="history_event_refresh_reason_refund">for refund</string> - <string name="history_event_refresh_reason_abort_pay">to abort payment</string> - <string name="history_event_refresh_reason_recoup">to recoup funds</string> - <string name="history_event_refresh_reason_backup_restored">because of restoring from backup</string> - <string name="history_event_unknown">Unknown Event</string> + <string name="transaction_reserve_balance_updated">Reserve Balance Updated</string> + <string name="transaction_payment">Payment</string> + <string name="transaction_paid">Paid</string> + <string name="transaction_order_total">Order Total</string> + <string name="transaction_order">Order</string> + <string name="transaction_order_id">Order Reference: %1$s</string> + <string name="transaction_payment_aborted">Payment Aborted</string> + <string name="transaction_withdrawal">Withdraw</string> + <string name="transaction_order_accepted">Purchase Confirmed</string> + <string name="transaction_order_refused">Purchase Cancelled</string> + <string name="transaction_tip_accepted">Tip Accepted</string> + <string name="transaction_tip_declined">Tip Declined</string> + <string name="transaction_order_redirected">Purchase Redirected</string> + <string name="transaction_refund">Refund</string> + <string name="transaction_refresh">Obtained change</string> + <string name="transaction_refresh_reason_manual">because of manual request</string> + <string name="transaction_refresh_reason_pay">for payment</string> + <string name="transaction_refresh_reason_refund">for refund</string> + <string name="transaction_refresh_reason_abort_pay">to abort payment</string> + <string name="transaction_refresh_reason_recoup">to recoup funds</string> + <string name="transaction_refresh_reason_backup_restored">because of restoring from backup</string> + <string name="transaction_unknown">Unknown Transaction</string> <string name="payment_fee">+%s payment fee</string> <string name="payment_button_confirm">Confirm Payment</string> @@ -118,21 +113,18 @@ <string name="exchange_fee_wire_fee_timespan">Timespan: %1$s - %2$s</string> <string name="exchange_fee_wire_fee_wire_fee">Wire Fee: %s</string> <string name="exchange_fee_wire_fee_closing_fee">Closing Fee: %s</string> + <string name="exchange_tos_accept">Accept Terms of Service</string> <string name="pending_operations_title">Pending Operations</string> <string name="pending_operations_refuse">Refuse Proposal</string> <string name="pending_operations_no_action">(no action)</string> - <string name="settings_version">Version Information</string> - <string name="exchange_tos_accept">Accept Terms of Service</string> - <string name="settings_backups">Backups</string> - <string name="settings_export_to_file">Export wallet to file</string> - <string name="settings_import_from_file">Import from file</string> + <string name="settings_dev_mode">Developer Mode</string> + <string name="settings_dev_mode_summary">Shows more information intended for debugging</string> <string name="settings_withdraw_testkudos">Withdraw TESTKUDOS</string> <string name="settings_withdraw_testkudos_summary">Get money for testing</string> <string name="settings_reset">Reset Wallet (dangerous!)</string> <string name="settings_reset_summary">Throws away your money</string> - <string name="settings_dev_mode_summary">Shows more information intended for debugging</string> <string name="refund_error">Error processing refund</string> <string name="refund_success">Refund received</string> diff --git a/wallet/src/main/res/values/styles.xml b/wallet/src/main/res/values/styles.xml @@ -33,12 +33,12 @@ <style name="AppTheme.Toolbar" parent="Widget.MaterialComponents.Toolbar.Primary" /> - <style name="HistoryTitle"> + <style name="TransactionTitle"> <item name="android:textSize">16sp</item> <item name="android:textColor">?android:textColorPrimary</item> </style> - <style name="HistoryEventLabel"> + <style name="TransactionLabel"> <item name="android:layout_width">0dp</item> <item name="android:layout_height">wrap_content</item> <item name="android:layout_marginStart">16dp</item> @@ -47,12 +47,12 @@ <item name="android:gravity">center</item> </style> - <style name="HistoryEventLabel.Time"> + <style name="TransactionLabel.Time"> <item name="android:layout_marginBottom">16dp</item> <item name="android:textAppearance">@style/TextAppearance.AppCompat.Medium</item> </style> - <style name="HistoryEventContent"> + <style name="TransactionContent"> <item name="android:layout_width">0dp</item> <item name="android:layout_height">wrap_content</item> <item name="android:layout_marginStart">16dp</item> diff --git a/wallet/src/main/res/xml/settings_main.xml b/wallet/src/main/res/xml/settings_main.xml @@ -21,7 +21,7 @@ app:icon="@drawable/ic_developer_mode" app:key="pref_dev_mode" app:summary="@string/settings_dev_mode_summary" - app:title="@string/menu_developer_mode" /> + app:title="@string/settings_dev_mode" /> <Preference app:icon="@drawable/ic_cash_usd_outline" diff --git a/wallet/src/test/java/net/taler/wallet/history/HistoryEventTest.kt b/wallet/src/test/java/net/taler/wallet/history/HistoryEventTest.kt @@ -1,470 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet.history - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.fasterxml.jackson.module.kotlin.readValue -import net.taler.common.Amount -import net.taler.wallet.history.RefreshReason.PAY -import net.taler.wallet.history.ReserveType.MANUAL -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test -import kotlin.random.Random - -class HistoryEventTest { - - private val mapper = ObjectMapper().registerModule(KotlinModule()) - - private val timestamp = Random.nextLong() - private val exchangeBaseUrl = "https://exchange.test.taler.net/" - private val orderShortInfo = OrderShortInfo( - proposalId = "EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", - orderId = "2019.364-01RAQ68DQ7AWR", - merchantBaseUrl = "https://backend.demo.taler.net/public/instances/FSF/", - amount = Amount.fromJSONString("KUDOS:0.5"), - summary = "Essay: Foreword" - ) - - @Test - fun `test ExchangeAddedEvent`() { - val builtIn = Random.nextBoolean() - val json = """{ - "type": "exchange-added", - "builtIn": $builtIn, - "eventId": "exchange-added;https%3A%2F%2Fexchange.test.taler.net%2F", - "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", - "timestamp": { - "t_ms": $timestamp - } - }""".trimIndent() - val event: ExchangeAddedEvent = mapper.readValue(json) - - assertEquals(builtIn, event.builtIn) - assertEquals(exchangeBaseUrl, event.exchangeBaseUrl) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test ExchangeUpdatedEvent`() { - val json = """{ - "type": "exchange-updated", - "eventId": "exchange-updated;https%3A%2F%2Fexchange.test.taler.net%2F", - "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", - "timestamp": { - "t_ms": $timestamp - } - }""".trimIndent() - val event: ExchangeUpdatedEvent = mapper.readValue(json) - - assertEquals(exchangeBaseUrl, event.exchangeBaseUrl) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test ReserveShortInfo`() { - val json = """{ - "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", - "reserveCreationDetail": { - "type": "manual" - }, - "reservePub": "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G" - }""".trimIndent() - val info: ReserveShortInfo = mapper.readValue(json) - - assertEquals(exchangeBaseUrl, info.exchangeBaseUrl) - assertEquals(MANUAL, info.reserveCreationDetail.type) - assertEquals("BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G", info.reservePub) - } - - @Test - fun `test ReserveBalanceUpdatedEvent`() { - val json = """{ - "type": "reserve-balance-updated", - "eventId": "reserve-balance-updated;K0H10Q6HB9WH0CKHQQMNH5C6GA7A9AR1E2XSS9G1KG3ZXMBVT26G", - "reserveAwaitedAmount": "TESTKUDOS:23", - "reserveUnclaimedAmount": "TESTKUDOS:0.01", - "reserveBalance": "TESTKUDOS:10", - "timestamp": { - "t_ms": $timestamp - }, - "reserveShortInfo": { - "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", - "reserveCreationDetail": { - "type": "manual" - }, - "reservePub": "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G" - } - }""".trimIndent() - val event: ReserveBalanceUpdatedEvent = mapper.readValue(json) - - assertEquals(timestamp, event.timestamp.ms) - assertEquals("TESTKUDOS:23", event.reserveAwaitedAmount.toJSONString()) - assertEquals("TESTKUDOS:10", event.reserveBalance.toJSONString()) - assertEquals("TESTKUDOS:0.01", event.reserveUnclaimedAmount.toJSONString()) - assertEquals(exchangeBaseUrl, event.reserveShortInfo.exchangeBaseUrl) - } - - @Test - fun `test HistoryWithdrawnEvent`() { - val json = """{ - "type": "withdrawn", - "withdrawalGroupId": "974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0", - "eventId": "withdrawn;974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0", - "amountWithdrawnEffective": "TESTKUDOS:9.8", - "amountWithdrawnRaw": "TESTKUDOS:10", - "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", - "timestamp": { - "t_ms": $timestamp - }, - "withdrawalSource": { - "type": "reserve", - "reservePub": "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G" - } - }""".trimIndent() - val event: HistoryWithdrawnEvent = mapper.readValue(json) - - assertEquals( - "974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0", - event.withdrawalGroupId - ) - assertEquals("TESTKUDOS:9.8", event.amountWithdrawnEffective.toJSONString()) - assertEquals("TESTKUDOS:10", event.amountWithdrawnRaw.toJSONString()) - assertTrue(event.withdrawalSource is WithdrawalSourceReserve) - assertEquals( - "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G", - (event.withdrawalSource as WithdrawalSourceReserve).reservePub - ) - assertEquals(exchangeBaseUrl, event.exchangeBaseUrl) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test OrderShortInfo`() { - val json = """{ - "amount": "KUDOS:0.5", - "orderId": "2019.364-01RAQ68DQ7AWR", - "merchantBaseUrl": "https:\/\/backend.demo.taler.net\/public\/instances\/FSF\/", - "proposalId": "EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", - "summary": "Essay: Foreword" - }""".trimIndent() - val info: OrderShortInfo = mapper.readValue(json) - - assertEquals("KUDOS:0.5", info.amount.toJSONString()) - assertEquals("2019.364-01RAQ68DQ7AWR", info.orderId) - assertEquals("Essay: Foreword", info.summary) - } - - @Test - fun `test HistoryOrderAcceptedEvent`() { - val json = """{ - "type": "order-accepted", - "eventId": "order-accepted;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", - "orderShortInfo": { - "amount": "${orderShortInfo.amount.toJSONString()}", - "orderId": "${orderShortInfo.orderId}", - "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", - "proposalId": "${orderShortInfo.proposalId}", - "summary": "${orderShortInfo.summary}" - }, - "timestamp": { - "t_ms": $timestamp - } - }""".trimIndent() - val event: HistoryOrderAcceptedEvent = mapper.readValue(json) - - assertEquals(orderShortInfo, event.orderShortInfo) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test HistoryOrderRefusedEvent`() { - val json = """{ - "type": "order-refused", - "eventId": "order-refused;9RJGAYXKWX0Y3V37H66606SXSA7V2CV255EBFS4G1JSH6W1EG7F0", - "orderShortInfo": { - "amount": "${orderShortInfo.amount.toJSONString()}", - "orderId": "${orderShortInfo.orderId}", - "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", - "proposalId": "${orderShortInfo.proposalId}", - "summary": "${orderShortInfo.summary}" - }, - "timestamp": { - "t_ms": $timestamp - } - }""".trimIndent() - val event: HistoryOrderRefusedEvent = mapper.readValue(json) - - assertEquals(orderShortInfo, event.orderShortInfo) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test HistoryPaymentSentEvent`() { - val json = """{ - "type": "payment-sent", - "eventId": "payment-sent;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", - "orderShortInfo": { - "amount": "${orderShortInfo.amount.toJSONString()}", - "orderId": "${orderShortInfo.orderId}", - "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", - "proposalId": "${orderShortInfo.proposalId}", - "summary": "${orderShortInfo.summary}" - }, - "replay": false, - "sessionId": "e4f436c4-3c5c-4aee-81d2-26e425c09520", - "timestamp": { - "t_ms": $timestamp - }, - "numCoins": 6, - "amountPaidWithFees": "KUDOS:0.6" - }""".trimIndent() - val event: HistoryPaymentSentEvent = mapper.readValue(json) - - assertEquals(orderShortInfo, event.orderShortInfo) - assertEquals(false, event.replay) - assertEquals(6, event.numCoins) - assertEquals("KUDOS:0.6", event.amountPaidWithFees.toJSONString()) - assertEquals("e4f436c4-3c5c-4aee-81d2-26e425c09520", event.sessionId) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test HistoryPaymentSentEvent without sessionId`() { - val json = """{ - "type": "payment-sent", - "eventId": "payment-sent;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", - "orderShortInfo": { - "amount": "${orderShortInfo.amount.toJSONString()}", - "orderId": "${orderShortInfo.orderId}", - "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", - "proposalId": "${orderShortInfo.proposalId}", - "summary": "${orderShortInfo.summary}" - }, - "replay": true, - "timestamp": { - "t_ms": $timestamp - }, - "numCoins": 6, - "amountPaidWithFees": "KUDOS:0.6" - }""".trimIndent() - val event: HistoryPaymentSentEvent = mapper.readValue(json) - - assertEquals(orderShortInfo, event.orderShortInfo) - assertEquals(true, event.replay) - assertEquals(6, event.numCoins) - assertEquals("KUDOS:0.6", event.amountPaidWithFees.toJSONString()) - assertEquals(null, event.sessionId) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test HistoryPaymentAbortedEvent`() { - val json = """{ - "type": "payment-aborted", - "eventId": "payment-sent;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "orderShortInfo": { - "amount": "${orderShortInfo.amount.toJSONString()}", - "orderId": "${orderShortInfo.orderId}", - "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", - "proposalId": "${orderShortInfo.proposalId}", - "summary": "${orderShortInfo.summary}" - }, - "timestamp": { - "t_ms": $timestamp - }, - "amountLost": "KUDOS:0.1" - }""".trimIndent() - val event: HistoryPaymentAbortedEvent = mapper.readValue(json) - - assertEquals(orderShortInfo, event.orderShortInfo) - assertEquals("KUDOS:0.1", event.amountLost.toJSONString()) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test HistoryTipAcceptedEvent`() { - val json = """{ - "type": "tip-accepted", - "timestamp": { - "t_ms": $timestamp - }, - "eventId": "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "tipId": "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "tipRaw": "KUDOS:4" - }""".trimIndent() - val event: HistoryTipAcceptedEvent = mapper.readValue(json) - - assertEquals( - "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - event.tipId - ) - assertEquals("KUDOS:4", event.tipRaw.toJSONString()) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test HistoryTipDeclinedEvent`() { - val json = """{ - "type": "tip-declined", - "timestamp": { - "t_ms": $timestamp - }, - "eventId": "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "tipId": "tip-accepted;998724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "tipAmount": "KUDOS:4" - }""".trimIndent() - val event: HistoryTipDeclinedEvent = mapper.readValue(json) - - assertEquals( - "tip-accepted;998724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - event.tipId - ) - assertEquals("KUDOS:4", event.tipAmount.toJSONString()) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test HistoryRefundedEvent`() { - val json = """{ - "type": "refund", - "eventId": "refund;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "refundGroupId": "refund;998724", - "orderShortInfo": { - "amount": "${orderShortInfo.amount.toJSONString()}", - "orderId": "${orderShortInfo.orderId}", - "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", - "proposalId": "${orderShortInfo.proposalId}", - "summary": "${orderShortInfo.summary}" - }, - "timestamp": { - "t_ms": $timestamp - }, - "amountRefundedRaw": "KUDOS:1", - "amountRefundedInvalid": "KUDOS:0.5", - "amountRefundedEffective": "KUDOS:0.4" - }""".trimIndent() - val event: HistoryRefundedEvent = mapper.readValue(json) - - assertEquals("refund;998724", event.refundGroupId) - assertEquals("KUDOS:1", event.amountRefundedRaw.toJSONString()) - assertEquals("KUDOS:0.5", event.amountRefundedInvalid.toJSONString()) - assertEquals("KUDOS:0.4", event.amountRefundedEffective.toJSONString()) - assertEquals(orderShortInfo, event.orderShortInfo) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test HistoryRefreshedEvent`() { - val json = """{ - "type": "refreshed", - "refreshGroupId": "8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", - "eventId": "refreshed;8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", - "timestamp": { - "t_ms": $timestamp - }, - "refreshReason": "pay", - "amountRefreshedEffective": "KUDOS:0", - "amountRefreshedRaw": "KUDOS:1", - "numInputCoins": 6, - "numOutputCoins": 0, - "numRefreshedInputCoins": 1 - }""".trimIndent() - val event: HistoryRefreshedEvent = mapper.readValue(json) - - assertEquals("KUDOS:0", event.amountRefreshedEffective.toJSONString()) - assertEquals("KUDOS:1", event.amountRefreshedRaw.toJSONString()) - assertEquals(6, event.numInputCoins) - assertEquals(0, event.numOutputCoins) - assertEquals(1, event.numRefreshedInputCoins) - assertEquals("8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", event.refreshGroupId) - assertEquals(PAY, event.refreshReason) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test HistoryOrderRedirectedEvent`() { - val json = """{ - "type": "order-redirected", - "eventId": "order-redirected;621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", - "alreadyPaidOrderShortInfo": { - "amount": "KUDOS:0.5", - "orderId": "2019.354-01P25CD66P8NG", - "merchantBaseUrl": "https://backend.demo.taler.net/public/instances/FSF/", - "proposalId": "898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "summary": "Essay: 1. The Free Software Definition" - }, - "newOrderShortInfo": { - "amount": "KUDOS:0.5", - "orderId": "2019.364-01M4QH6KPMJY4", - "merchantBaseUrl": "https://backend.demo.taler.net/public/instances/FSF/", - "proposalId": "621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", - "summary": "Essay: 1. The Free Software Definition" - }, - "timestamp": { - "t_ms": $timestamp - } - }""".trimIndent() - val event: HistoryOrderRedirectedEvent = mapper.readValue(json) - - assertEquals( - "898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - event.alreadyPaidOrderShortInfo.proposalId - ) - assertEquals( - "https://backend.demo.taler.net/public/instances/FSF/", - event.alreadyPaidOrderShortInfo.merchantBaseUrl - ) - assertEquals("2019.354-01P25CD66P8NG", event.alreadyPaidOrderShortInfo.orderId) - assertEquals("KUDOS:0.5", event.alreadyPaidOrderShortInfo.amount.toJSONString()) - assertEquals( - "Essay: 1. The Free Software Definition", - event.alreadyPaidOrderShortInfo.summary - ) - - assertEquals( - "621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", - event.newOrderShortInfo.proposalId - ) - assertEquals( - "https://backend.demo.taler.net/public/instances/FSF/", - event.newOrderShortInfo.merchantBaseUrl - ) - assertEquals("2019.364-01M4QH6KPMJY4", event.newOrderShortInfo.orderId) - assertEquals("KUDOS:0.5", event.newOrderShortInfo.amount.toJSONString()) - assertEquals("Essay: 1. The Free Software Definition", event.newOrderShortInfo.summary) - - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test HistoryUnknownEvent`() { - val json = """{ - "type": "does not exist", - "timestamp": { - "t_ms": $timestamp - }, - "eventId": "does-not-exist;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0" - }""".trimIndent() - val event: HistoryEvent = mapper.readValue(json) - - assertEquals(HistoryUnknownEvent::class.java, event.javaClass) - assertEquals(timestamp, event.timestamp.ms) - } - -} diff --git a/wallet/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt b/wallet/src/test/java/net/taler/wallet/history/ReserveTransactionTest.kt @@ -1,52 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet.history - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.fasterxml.jackson.module.kotlin.readValue -import org.junit.Assert.assertEquals -import org.junit.Test -import kotlin.random.Random - -class ReserveTransactionTest { - - private val mapper = ObjectMapper().registerModule(KotlinModule()) - - private val timestamp = Random.nextLong() - - @Test - fun `test ExchangeAddedEvent`() { - val senderAccountUrl = "payto://x-taler-bank/bank.test.taler.net/894" - val json = """{ - "amount": "TESTKUDOS:10", - "sender_account_url": "payto:\/\/x-taler-bank\/bank.test.taler.net\/894", - "timestamp": { - "t_ms": $timestamp - }, - "wire_reference": "00000000004TR", - "type": "DEPOSIT" - }""".trimIndent() - val transaction: ReserveDepositTransaction = mapper.readValue(json) - - assertEquals("TESTKUDOS:10", transaction.amount) - assertEquals(senderAccountUrl, transaction.senderAccountUrl) - assertEquals("00000000004TR", transaction.wireReference) - assertEquals(timestamp, transaction.timestamp.ms) - } - -} diff --git a/wallet/src/test/java/net/taler/wallet/transactions/ReserveTransactionTest.kt b/wallet/src/test/java/net/taler/wallet/transactions/ReserveTransactionTest.kt @@ -0,0 +1,52 @@ +/* + * 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.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.readValue +import org.junit.Assert.assertEquals +import org.junit.Test +import kotlin.random.Random + +class ReserveTransactionTest { + + private val mapper = ObjectMapper().registerModule(KotlinModule()) + + private val timestamp = Random.nextLong() + + @Test + fun `test ExchangeAddedEvent`() { + val senderAccountUrl = "payto://x-taler-bank/bank.test.taler.net/894" + val json = """{ + "amount": "TESTKUDOS:10", + "sender_account_url": "payto:\/\/x-taler-bank\/bank.test.taler.net\/894", + "timestamp": { + "t_ms": $timestamp + }, + "wire_reference": "00000000004TR", + "type": "DEPOSIT" + }""".trimIndent() + val transaction: ReserveDepositTransaction = mapper.readValue(json) + + assertEquals("TESTKUDOS:10", transaction.amount) + assertEquals(senderAccountUrl, transaction.senderAccountUrl) + assertEquals("00000000004TR", transaction.wireReference) + assertEquals(timestamp, transaction.timestamp.ms) + } + +} diff --git a/wallet/src/test/java/net/taler/wallet/transactions/TransactionTest.kt b/wallet/src/test/java/net/taler/wallet/transactions/TransactionTest.kt @@ -0,0 +1,470 @@ +/* + * 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.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.readValue +import net.taler.common.Amount +import net.taler.wallet.transactions.RefreshReason.PAY +import net.taler.wallet.transactions.ReserveType.MANUAL +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import kotlin.random.Random + +class TransactionTest { + + private val mapper = ObjectMapper().registerModule(KotlinModule()) + + private val timestamp = Random.nextLong() + private val exchangeBaseUrl = "https://exchange.test.taler.net/" + private val orderShortInfo = OrderShortInfo( + proposalId = "EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", + orderId = "2019.364-01RAQ68DQ7AWR", + merchantBaseUrl = "https://backend.demo.taler.net/public/instances/FSF/", + amount = Amount.fromJSONString("KUDOS:0.5"), + summary = "Essay: Foreword" + ) + + @Test + fun `test ExchangeAddedEvent`() { + val builtIn = Random.nextBoolean() + val json = """{ + "type": "exchange-added", + "builtIn": $builtIn, + "eventId": "exchange-added;https%3A%2F%2Fexchange.test.taler.net%2F", + "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", + "timestamp": { + "t_ms": $timestamp + } + }""".trimIndent() + val event: ExchangeAddedEvent = mapper.readValue(json) + + assertEquals(builtIn, event.builtIn) + assertEquals(exchangeBaseUrl, event.exchangeBaseUrl) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test ExchangeUpdatedEvent`() { + val json = """{ + "type": "exchange-updated", + "eventId": "exchange-updated;https%3A%2F%2Fexchange.test.taler.net%2F", + "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", + "timestamp": { + "t_ms": $timestamp + } + }""".trimIndent() + val event: ExchangeUpdatedEvent = mapper.readValue(json) + + assertEquals(exchangeBaseUrl, event.exchangeBaseUrl) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test ReserveShortInfo`() { + val json = """{ + "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", + "reserveCreationDetail": { + "type": "manual" + }, + "reservePub": "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G" + }""".trimIndent() + val info: ReserveShortInfo = mapper.readValue(json) + + assertEquals(exchangeBaseUrl, info.exchangeBaseUrl) + assertEquals(MANUAL, info.reserveCreationDetail.type) + assertEquals("BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G", info.reservePub) + } + + @Test + fun `test ReserveBalanceUpdatedTransaction`() { + val json = """{ + "type": "reserve-balance-updated", + "eventId": "reserve-balance-updated;K0H10Q6HB9WH0CKHQQMNH5C6GA7A9AR1E2XSS9G1KG3ZXMBVT26G", + "reserveAwaitedAmount": "TESTKUDOS:23", + "reserveUnclaimedAmount": "TESTKUDOS:0.01", + "reserveBalance": "TESTKUDOS:10", + "timestamp": { + "t_ms": $timestamp + }, + "reserveShortInfo": { + "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", + "reserveCreationDetail": { + "type": "manual" + }, + "reservePub": "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G" + } + }""".trimIndent() + val transaction: ReserveBalanceUpdatedTransaction = mapper.readValue(json) + + assertEquals(timestamp, transaction.timestamp.ms) + assertEquals("TESTKUDOS:23", transaction.reserveAwaitedAmount.toJSONString()) + assertEquals("TESTKUDOS:10", transaction.reserveBalance.toJSONString()) + assertEquals("TESTKUDOS:0.01", transaction.reserveUnclaimedAmount.toJSONString()) + assertEquals(exchangeBaseUrl, transaction.reserveShortInfo.exchangeBaseUrl) + } + + @Test + fun `test WithdrawTransaction`() { + val json = """{ + "type": "withdrawn", + "withdrawalGroupId": "974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0", + "eventId": "withdrawn;974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0", + "amountWithdrawnEffective": "TESTKUDOS:9.8", + "amountWithdrawnRaw": "TESTKUDOS:10", + "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", + "timestamp": { + "t_ms": $timestamp + }, + "withdrawalSource": { + "type": "reserve", + "reservePub": "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G" + } + }""".trimIndent() + val event: WithdrawTransaction = mapper.readValue(json) + + assertEquals( + "974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0", + event.withdrawalGroupId + ) + assertEquals("TESTKUDOS:9.8", event.amountWithdrawnEffective.toJSONString()) + assertEquals("TESTKUDOS:10", event.amountWithdrawnRaw.toJSONString()) + assertTrue(event.withdrawalSource is WithdrawalSourceReserve) + assertEquals( + "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G", + (event.withdrawalSource as WithdrawalSourceReserve).reservePub + ) + assertEquals(exchangeBaseUrl, event.exchangeBaseUrl) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test OrderShortInfo`() { + val json = """{ + "amount": "KUDOS:0.5", + "orderId": "2019.364-01RAQ68DQ7AWR", + "merchantBaseUrl": "https:\/\/backend.demo.taler.net\/public\/instances\/FSF\/", + "proposalId": "EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", + "summary": "Essay: Foreword" + }""".trimIndent() + val info: OrderShortInfo = mapper.readValue(json) + + assertEquals("KUDOS:0.5", info.amount.toJSONString()) + assertEquals("2019.364-01RAQ68DQ7AWR", info.orderId) + assertEquals("Essay: Foreword", info.summary) + } + + @Test + fun `test OrderAcceptedTransaction`() { + val json = """{ + "type": "order-accepted", + "eventId": "order-accepted;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", + "orderShortInfo": { + "amount": "${orderShortInfo.amount.toJSONString()}", + "orderId": "${orderShortInfo.orderId}", + "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", + "proposalId": "${orderShortInfo.proposalId}", + "summary": "${orderShortInfo.summary}" + }, + "timestamp": { + "t_ms": $timestamp + } + }""".trimIndent() + val transaction: OrderAcceptedTransaction = mapper.readValue(json) + + assertEquals(orderShortInfo, transaction.orderShortInfo) + assertEquals(timestamp, transaction.timestamp.ms) + } + + @Test + fun `test OrderRefusedTransaction`() { + val json = """{ + "type": "order-refused", + "eventId": "order-refused;9RJGAYXKWX0Y3V37H66606SXSA7V2CV255EBFS4G1JSH6W1EG7F0", + "orderShortInfo": { + "amount": "${orderShortInfo.amount.toJSONString()}", + "orderId": "${orderShortInfo.orderId}", + "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", + "proposalId": "${orderShortInfo.proposalId}", + "summary": "${orderShortInfo.summary}" + }, + "timestamp": { + "t_ms": $timestamp + } + }""".trimIndent() + val transaction: OrderRefusedTransaction = mapper.readValue(json) + + assertEquals(orderShortInfo, transaction.orderShortInfo) + assertEquals(timestamp, transaction.timestamp.ms) + } + + @Test + fun `test PaymentTransaction`() { + val json = """{ + "type": "payment-sent", + "eventId": "payment-sent;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", + "orderShortInfo": { + "amount": "${orderShortInfo.amount.toJSONString()}", + "orderId": "${orderShortInfo.orderId}", + "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", + "proposalId": "${orderShortInfo.proposalId}", + "summary": "${orderShortInfo.summary}" + }, + "replay": false, + "sessionId": "e4f436c4-3c5c-4aee-81d2-26e425c09520", + "timestamp": { + "t_ms": $timestamp + }, + "numCoins": 6, + "amountPaidWithFees": "KUDOS:0.6" + }""".trimIndent() + val event: PaymentTransaction = mapper.readValue(json) + + assertEquals(orderShortInfo, event.orderShortInfo) + assertEquals(false, event.replay) + assertEquals(6, event.numCoins) + assertEquals("KUDOS:0.6", event.amountPaidWithFees.toJSONString()) + assertEquals("e4f436c4-3c5c-4aee-81d2-26e425c09520", event.sessionId) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test PaymentTransaction without sessionId`() { + val json = """{ + "type": "payment-sent", + "eventId": "payment-sent;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", + "orderShortInfo": { + "amount": "${orderShortInfo.amount.toJSONString()}", + "orderId": "${orderShortInfo.orderId}", + "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", + "proposalId": "${orderShortInfo.proposalId}", + "summary": "${orderShortInfo.summary}" + }, + "replay": true, + "timestamp": { + "t_ms": $timestamp + }, + "numCoins": 6, + "amountPaidWithFees": "KUDOS:0.6" + }""".trimIndent() + val event: PaymentTransaction = mapper.readValue(json) + + assertEquals(orderShortInfo, event.orderShortInfo) + assertEquals(true, event.replay) + assertEquals(6, event.numCoins) + assertEquals("KUDOS:0.6", event.amountPaidWithFees.toJSONString()) + assertEquals(null, event.sessionId) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test PaymentAbortedTransaction`() { + val json = """{ + "type": "payment-aborted", + "eventId": "payment-sent;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "orderShortInfo": { + "amount": "${orderShortInfo.amount.toJSONString()}", + "orderId": "${orderShortInfo.orderId}", + "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", + "proposalId": "${orderShortInfo.proposalId}", + "summary": "${orderShortInfo.summary}" + }, + "timestamp": { + "t_ms": $timestamp + }, + "amountLost": "KUDOS:0.1" + }""".trimIndent() + val transaction: PaymentAbortedTransaction = mapper.readValue(json) + + assertEquals(orderShortInfo, transaction.orderShortInfo) + assertEquals("KUDOS:0.1", transaction.amountLost.toJSONString()) + assertEquals(timestamp, transaction.timestamp.ms) + } + + @Test + fun `test TipAcceptedTransaction`() { + val json = """{ + "type": "tip-accepted", + "timestamp": { + "t_ms": $timestamp + }, + "eventId": "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "tipId": "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "tipRaw": "KUDOS:4" + }""".trimIndent() + val transaction: TipAcceptedTransaction = mapper.readValue(json) + + assertEquals( + "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + transaction.tipId + ) + assertEquals("KUDOS:4", transaction.tipRaw.toJSONString()) + assertEquals(timestamp, transaction.timestamp.ms) + } + + @Test + fun `test TipDeclinedTransaction`() { + val json = """{ + "type": "tip-declined", + "timestamp": { + "t_ms": $timestamp + }, + "eventId": "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "tipId": "tip-accepted;998724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "tipAmount": "KUDOS:4" + }""".trimIndent() + val transaction: TipDeclinedTransaction = mapper.readValue(json) + + assertEquals( + "tip-accepted;998724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + transaction.tipId + ) + assertEquals("KUDOS:4", transaction.tipAmount.toJSONString()) + assertEquals(timestamp, transaction.timestamp.ms) + } + + @Test + fun `test RefundTransaction`() { + val json = """{ + "type": "refund", + "eventId": "refund;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "refundGroupId": "refund;998724", + "orderShortInfo": { + "amount": "${orderShortInfo.amount.toJSONString()}", + "orderId": "${orderShortInfo.orderId}", + "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", + "proposalId": "${orderShortInfo.proposalId}", + "summary": "${orderShortInfo.summary}" + }, + "timestamp": { + "t_ms": $timestamp + }, + "amountRefundedRaw": "KUDOS:1", + "amountRefundedInvalid": "KUDOS:0.5", + "amountRefundedEffective": "KUDOS:0.4" + }""".trimIndent() + val event: RefundTransaction = mapper.readValue(json) + + assertEquals("refund;998724", event.refundGroupId) + assertEquals("KUDOS:1", event.amountRefundedRaw.toJSONString()) + assertEquals("KUDOS:0.5", event.amountRefundedInvalid.toJSONString()) + assertEquals("KUDOS:0.4", event.amountRefundedEffective.toJSONString()) + assertEquals(orderShortInfo, event.orderShortInfo) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test RefreshTransaction`() { + val json = """{ + "type": "refreshed", + "refreshGroupId": "8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", + "eventId": "refreshed;8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", + "timestamp": { + "t_ms": $timestamp + }, + "refreshReason": "pay", + "amountRefreshedEffective": "KUDOS:0", + "amountRefreshedRaw": "KUDOS:1", + "numInputCoins": 6, + "numOutputCoins": 0, + "numRefreshedInputCoins": 1 + }""".trimIndent() + val event: RefreshTransaction = mapper.readValue(json) + + assertEquals("KUDOS:0", event.amountRefreshedEffective.toJSONString()) + assertEquals("KUDOS:1", event.amountRefreshedRaw.toJSONString()) + assertEquals(6, event.numInputCoins) + assertEquals(0, event.numOutputCoins) + assertEquals(1, event.numRefreshedInputCoins) + assertEquals("8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", event.refreshGroupId) + assertEquals(PAY, event.refreshReason) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test OrderRedirectedTransaction`() { + val json = """{ + "type": "order-redirected", + "eventId": "order-redirected;621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", + "alreadyPaidOrderShortInfo": { + "amount": "KUDOS:0.5", + "orderId": "2019.354-01P25CD66P8NG", + "merchantBaseUrl": "https://backend.demo.taler.net/public/instances/FSF/", + "proposalId": "898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "summary": "Essay: 1. The Free Software Definition" + }, + "newOrderShortInfo": { + "amount": "KUDOS:0.5", + "orderId": "2019.364-01M4QH6KPMJY4", + "merchantBaseUrl": "https://backend.demo.taler.net/public/instances/FSF/", + "proposalId": "621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", + "summary": "Essay: 1. The Free Software Definition" + }, + "timestamp": { + "t_ms": $timestamp + } + }""".trimIndent() + val transaction: OrderRedirectedTransaction = mapper.readValue(json) + + assertEquals( + "898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + transaction.alreadyPaidOrderShortInfo.proposalId + ) + assertEquals( + "https://backend.demo.taler.net/public/instances/FSF/", + transaction.alreadyPaidOrderShortInfo.merchantBaseUrl + ) + assertEquals("2019.354-01P25CD66P8NG", transaction.alreadyPaidOrderShortInfo.orderId) + assertEquals("KUDOS:0.5", transaction.alreadyPaidOrderShortInfo.amount.toJSONString()) + assertEquals( + "Essay: 1. The Free Software Definition", + transaction.alreadyPaidOrderShortInfo.summary + ) + + assertEquals( + "621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", + transaction.newOrderShortInfo.proposalId + ) + assertEquals( + "https://backend.demo.taler.net/public/instances/FSF/", + transaction.newOrderShortInfo.merchantBaseUrl + ) + assertEquals("2019.364-01M4QH6KPMJY4", transaction.newOrderShortInfo.orderId) + assertEquals("KUDOS:0.5", transaction.newOrderShortInfo.amount.toJSONString()) + assertEquals("Essay: 1. The Free Software Definition", transaction.newOrderShortInfo.summary) + + assertEquals(timestamp, transaction.timestamp.ms) + } + + @Test + fun `test UnknownTransaction`() { + val json = """{ + "type": "does not exist", + "timestamp": { + "t_ms": $timestamp + }, + "eventId": "does-not-exist;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0" + }""".trimIndent() + val event: Transaction = mapper.readValue(json) + + assertEquals(UnknownTransaction::class.java, event.javaClass) + assertEquals(timestamp, event.timestamp.ms) + } + +}