summaryrefslogtreecommitdiff
path: root/wallet/src/main/java/net/taler/wallet/balances
diff options
context:
space:
mode:
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/balances')
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt40
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt137
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt24
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/Balances.kt57
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt50
5 files changed, 251 insertions, 57 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
index 09ae353..f40def4 100644
--- a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
@@ -24,20 +24,12 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
-import kotlinx.serialization.Serializable
-import net.taler.lib.common.Amount
import net.taler.wallet.R
import net.taler.wallet.balances.BalanceAdapter.BalanceViewHolder
-
-@Serializable
-data class BalanceItem(
- val available: Amount,
- val pendingIncoming: Amount,
- val pendingOutgoing: Amount
-) {
- val currency: String get() = available.currency
- val hasPending: Boolean get() = !pendingIncoming.isZero() || !pendingOutgoing.isZero()
-}
+import net.taler.wallet.balances.ScopeInfo.Auditor
+import net.taler.wallet.balances.ScopeInfo.Exchange
+import net.taler.wallet.balances.ScopeInfo.Global
+import net.taler.wallet.cleanExchange
class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<BalanceViewHolder>() {
@@ -66,16 +58,15 @@ class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<Balan
}
inner class BalanceViewHolder(private val v: View) : RecyclerView.ViewHolder(v) {
- private val currencyView: TextView = v.findViewById(R.id.balanceCurrencyView)
private val amountView: TextView = v.findViewById(R.id.balanceAmountView)
+ private val scopeView: TextView = v.findViewById(R.id.scopeView)
private val balanceInboundAmount: TextView = v.findViewById(R.id.balanceInboundAmount)
private val balanceInboundLabel: TextView = v.findViewById(R.id.balanceInboundLabel)
private val pendingView: TextView = v.findViewById(R.id.pendingView)
fun bind(item: BalanceItem) {
- v.setOnClickListener { listener.onBalanceClick(item.available.currency) }
- currencyView.text = item.currency
- amountView.text = item.available.amountStr
+ v.setOnClickListener { listener.onBalanceClick(item.scopeInfo) }
+ amountView.text = item.available.toString()
val amountIncoming = item.pendingIncoming
if (amountIncoming.isZero()) {
@@ -84,9 +75,22 @@ class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<Balan
} else {
balanceInboundAmount.visibility = VISIBLE
balanceInboundLabel.visibility = VISIBLE
- balanceInboundAmount.text =
- v.context.getString(R.string.amount_positive, amountIncoming)
+ balanceInboundAmount.text = v.context.getString(R.string.amount_positive, amountIncoming.toString(showSymbol = false))
}
+
+ val scopeInfo = item.scopeInfo
+ scopeView.visibility = when (scopeInfo) {
+ is Global -> GONE
+ is Exchange -> {
+ scopeView.text = v.context.getString(R.string.balance_scope_exchange, cleanExchange(scopeInfo.url))
+ VISIBLE
+ }
+ is Auditor -> {
+ scopeView.text = v.context.getString(R.string.balance_scope_auditor, cleanExchange(scopeInfo.url))
+ VISIBLE
+ }
+ }
+
pendingView.visibility = if (item.hasPending) VISIBLE else GONE
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt
new file mode 100644
index 0000000..42e67cf
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt
@@ -0,0 +1,137 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 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.balances
+
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.distinctUntilChanged
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.common.CurrencySpecification
+import net.taler.wallet.TAG
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.backend.WalletBackendApi
+import org.json.JSONObject
+
+@Serializable
+data class BalanceResponse(
+ val balances: List<BalanceItem>
+)
+
+@Serializable
+data class GetCurrencySpecificationResponse(
+ val currencySpecification: CurrencySpecification,
+)
+
+sealed class BalanceState {
+ data object None: BalanceState()
+ data object Loading: BalanceState()
+
+ data class Success(
+ val balances: List<BalanceItem>,
+ ): BalanceState()
+
+ data class Error(
+ val error: TalerErrorInfo,
+ ): BalanceState()
+}
+
+class BalanceManager(
+ private val api: WalletBackendApi,
+ private val scope: CoroutineScope,
+) {
+ private val mBalances = MutableLiveData<List<BalanceItem>>(emptyList())
+ val balances: LiveData<List<BalanceItem>> = mBalances
+
+ private val mState = MutableLiveData<BalanceState>(BalanceState.None)
+ val state: LiveData<BalanceState> = mState.distinctUntilChanged()
+
+ private val currencySpecs: MutableMap<ScopeInfo, CurrencySpecification?> = mutableMapOf()
+
+ @UiThread
+ fun loadBalances() {
+ mState.value = BalanceState.Loading
+ scope.launch {
+ val response = api.request("getBalances", BalanceResponse.serializer())
+ response.onError {
+ Log.e(TAG, "Error retrieving balances: $it")
+ mState.postValue(BalanceState.Error(it))
+ }
+ response.onSuccess {
+ mBalances.postValue(it.balances)
+ scope.launch {
+ // Fetch missing currency specs for all balances
+ it.balances.forEach { balance ->
+ if (!currencySpecs.containsKey(balance.scopeInfo)) {
+ currencySpecs[balance.scopeInfo] = getCurrencySpecification(balance.scopeInfo)
+ }
+ }
+
+ mState.postValue(
+ BalanceState.Success(it.balances.map { balance ->
+ val spec = currencySpecs[balance.scopeInfo]
+ balance.copy(
+ available = balance.available.withSpec(spec),
+ pendingIncoming = balance.pendingIncoming.withSpec(spec),
+ pendingOutgoing = balance.pendingOutgoing.withSpec(spec),
+ )
+ }),
+ )
+ }
+ }
+ }
+ }
+
+ private suspend fun getCurrencySpecification(scopeInfo: ScopeInfo): CurrencySpecification? {
+ var spec: CurrencySpecification? = null
+ api.request("getCurrencySpecification", GetCurrencySpecificationResponse.serializer()) {
+ val json = Json.encodeToString(scopeInfo)
+ Log.d(TAG, "BalanceManager: $json")
+ put("scope", JSONObject(json))
+ }.onSuccess {
+ spec = it.currencySpecification
+ }.onError {
+ Log.e(TAG, "Error getting currency spec for scope $scopeInfo: $it")
+ }
+
+ return spec
+ }
+
+ @Deprecated("Please find spec via scopeInfo instead", ReplaceWith("getSpecForScopeInfo"))
+ fun getSpecForCurrency(currency: String): CurrencySpecification? {
+ val state = mState.value
+ if (state !is BalanceState.Success) return null
+
+ return state.balances.find { it.currency == currency }?.available?.spec
+ }
+
+ fun getSpecForScopeInfo(scopeInfo: ScopeInfo): CurrencySpecification? {
+ val state = mState.value
+ if (state !is BalanceState.Success) return null
+
+ return state.balances.find { it.scopeInfo == scopeInfo }?.available?.spec
+ }
+
+ fun resetBalances() {
+ mState.value = BalanceState.None
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt
deleted file mode 100644
index d1a111f..0000000
--- a/wallet/src/main/java/net/taler/wallet/balances/BalanceResponse.kt
+++ /dev/null
@@ -1,24 +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.balances
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class BalanceResponse(
- val balances: List<BalanceItem>
-)
diff --git a/wallet/src/main/java/net/taler/wallet/balances/Balances.kt b/wallet/src/main/java/net/taler/wallet/balances/Balances.kt
new file mode 100644
index 0000000..dff2ffb
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/balances/Balances.kt
@@ -0,0 +1,57 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 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.balances
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.taler.common.Amount
+
+@Serializable
+data class BalanceItem(
+ val scopeInfo: ScopeInfo,
+ val available: Amount,
+ val pendingIncoming: Amount,
+ val pendingOutgoing: Amount,
+) {
+ val currency: String get() = available.currency
+ val hasPending: Boolean get() = !pendingIncoming.isZero() || !pendingOutgoing.isZero()
+}
+
+@Serializable
+sealed class ScopeInfo {
+ abstract val currency: String
+
+ @Serializable
+ @SerialName("global")
+ data class Global(
+ override val currency: String
+ ): ScopeInfo()
+
+ @Serializable
+ @SerialName("exchange")
+ data class Exchange(
+ override val currency: String,
+ val url: String,
+ ): ScopeInfo()
+
+ @Serializable
+ @SerialName("auditor")
+ data class Auditor(
+ override val currency: String,
+ val url: String,
+ ): ScopeInfo()
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt b/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt
index afd9a23..93636ea 100644
--- a/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt
@@ -29,11 +29,17 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
import net.taler.common.fadeIn
+import net.taler.common.showError
import net.taler.wallet.MainViewModel
+import net.taler.wallet.balances.BalanceState.Error
+import net.taler.wallet.balances.BalanceState.Loading
+import net.taler.wallet.balances.BalanceState.None
+import net.taler.wallet.balances.BalanceState.Success
import net.taler.wallet.databinding.FragmentBalancesBinding
+import net.taler.wallet.showError
interface BalanceClickListener {
- fun onBalanceClick(currency: String)
+ fun onBalanceClick(scopeInfo: ScopeInfo)
}
class BalancesFragment : Fragment(),
@@ -48,7 +54,7 @@ class BalancesFragment : Fragment(),
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View? {
+ ): View {
ui = FragmentBalancesBinding.inflate(inflater, container, false)
return ui.root
}
@@ -59,25 +65,39 @@ class BalancesFragment : Fragment(),
addItemDecoration(DividerItemDecoration(context, VERTICAL))
}
- model.balances.observe(viewLifecycleOwner, {
+ model.balanceManager.state.observe(viewLifecycleOwner) {
onBalancesChanged(it)
- })
+ }
}
- private fun onBalancesChanged(balances: List<BalanceItem>) {
- beginDelayedTransition(view as ViewGroup)
- if (balances.isEmpty()) {
- ui.mainEmptyState.visibility = VISIBLE
- ui.mainList.visibility = GONE
- } else {
- balancesAdapter.setItems(balances)
- ui.mainEmptyState.visibility = INVISIBLE
- ui.mainList.fadeIn()
+ private fun onBalancesChanged(state: BalanceState) {
+ model.showProgressBar.value = false
+ when (state) {
+ is None -> {}
+ is Loading -> {
+ model.showProgressBar.value = true
+ }
+ is Success -> {
+ beginDelayedTransition(view as ViewGroup)
+ if (state.balances.isEmpty()) {
+ ui.mainEmptyState.visibility = VISIBLE
+ ui.mainList.visibility = GONE
+ } else {
+ balancesAdapter.setItems(state.balances)
+ ui.mainEmptyState.visibility = INVISIBLE
+ ui.mainList.fadeIn()
+ }
+ }
+ is Error -> if (model.devMode.value == true) {
+ showError(state.error)
+ } else {
+ showError(state.error.userFacingMsg)
+ }
}
}
- override fun onBalanceClick(currency: String) {
- model.showTransactions(currency)
+ override fun onBalanceClick(scopeInfo: ScopeInfo) {
+ model.showTransactions(scopeInfo)
}
}