diff options
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/balances')
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) } } |