refactor: WalletViewModel extract FiatViewModel
This commit is contained in:
@@ -13,22 +13,18 @@ import com.bitcointxoko.gudariwallet.util.RouteHintAnalyzer
|
||||
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
||||
import com.bitcointxoko.gudariwallet.util.SendInputType
|
||||
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||
import com.bitcointxoko.gudariwallet.util.satsToFiat
|
||||
import com.bitcointxoko.gudariwallet.util.parseApiError
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import com.bitcointxoko.gudariwallet.util.lnurlProtocolError
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.bitcointxoko.gudariwallet.api.RouteHintHop
|
||||
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
||||
import com.bitcointxoko.gudariwallet.data.FiatRateCache
|
||||
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
||||
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
|
||||
import com.bitcointxoko.gudariwallet.util.BiometricHelper
|
||||
|
||||
private const val TAG = "WalletViewModel"
|
||||
@@ -40,60 +36,22 @@ class WalletViewModel(
|
||||
val paymentCache: PaymentCacheRepository,
|
||||
private val balancePrefs: BalancePrefsStore,
|
||||
val balanceVm : BalanceViewModel,
|
||||
val fiatVm : FiatViewModel
|
||||
) : ViewModel() {
|
||||
|
||||
// ── Balance state ─────────────────────────────────────────────────────────
|
||||
val balanceState: StateFlow<BalanceState> get() = balanceVm.balanceState
|
||||
// ── Balance visible state ─────────────────────────────────────────────────
|
||||
private val _balanceHidden = MutableStateFlow(balancePrefs.isBalanceHidden)
|
||||
val balanceHidden: StateFlow<Boolean> = _balanceHidden
|
||||
fun toggleBalanceVisibility() {
|
||||
val next = !_balanceHidden.value
|
||||
_balanceHidden.value = next
|
||||
balancePrefs.setBalanceHidden(next)
|
||||
}
|
||||
|
||||
// ── Fiat cache ────────────────────────────────────────────────────────────
|
||||
private val fiatCache = FiatRateCache(balancePrefs)
|
||||
val fiatRate: Double?
|
||||
get() = fiatCache.currentRate
|
||||
val fiatCurrency: String? get() = _selectedCurrency.value
|
||||
val fiatSatsPerUnit: StateFlow<Double?> = balanceVm.balanceState
|
||||
.mapNotNull { state ->
|
||||
(state as? BalanceState.Success)?.let { s ->
|
||||
if (s.fiatAmount != null && s.sats > 0)
|
||||
s.sats.toDouble() / s.fiatAmount
|
||||
else null
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = null
|
||||
)
|
||||
private val _selectedCurrency = MutableStateFlow(balancePrefs.selectedCurrency)
|
||||
val selectedCurrency: StateFlow<String?> = _selectedCurrency
|
||||
|
||||
fun setSelectedCurrency(currency: String?) {
|
||||
if (currency == _selectedCurrency.value) return
|
||||
_selectedCurrency.value = currency
|
||||
balancePrefs.setSelectedCurrency(currency)
|
||||
fiatCache.invalidateRate()
|
||||
Log.d(TAG, "FIAT [CURRENCY ] changed to ${currency ?: "none"} — cache invalidated")
|
||||
if (currency != null) {
|
||||
viewModelScope.launch { fetchAndApplyFiatRate() }
|
||||
} else {
|
||||
balanceVm.applyFiat(null, null) // ← fix
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RATE_TTL_MS = 5 * 60 * 1_000L // 5 minutes
|
||||
}
|
||||
val balanceHidden: StateFlow<Boolean> get() = fiatVm.balanceHidden
|
||||
val selectedCurrency: StateFlow<String?> get() = fiatVm.selectedCurrency
|
||||
val fiatSatsPerUnit: StateFlow<Double?> get() = fiatVm.fiatSatsPerUnit
|
||||
fun toggleBalanceVisibility() = fiatVm.toggleBalanceVisibility()
|
||||
fun setSelectedCurrency(c: String?) = fiatVm.setSelectedCurrency(c)
|
||||
suspend fun getSupportedCurrencies() = fiatVm.getSupportedCurrencies()
|
||||
val fiatRate: Double? get() = fiatVm.fiatRate
|
||||
val fiatCurrency: String? get() = fiatVm.fiatCurrency
|
||||
|
||||
|
||||
// ── Other states ──────────────────────────────────────────────────────────
|
||||
|
||||
private val _receiveState = MutableStateFlow<ReceiveState>(ReceiveState.Idle)
|
||||
val receiveState: StateFlow<ReceiveState> = _receiveState
|
||||
private val _sendState = MutableStateFlow<SendState>(SendState.Idle)
|
||||
@@ -110,48 +68,7 @@ class WalletViewModel(
|
||||
fetchLightningAddress()
|
||||
}
|
||||
|
||||
// ── Fiat rate fetch + apply ───────────────────────────────────────────────
|
||||
private suspend fun fetchAndApplyFiatRate() {
|
||||
val currency = _selectedCurrency.value ?: return
|
||||
|
||||
val rate: Double = fiatCache.getRate(currency, RATE_TTL_MS)
|
||||
?: run {
|
||||
runCatching { repo.getFiatRate(currency) }
|
||||
.getOrElse { e ->
|
||||
Log.w(TAG, "FIAT [RATE ERROR ] $currency — ${e.message}")
|
||||
Log.w(TAG, "FIAT [RATE MISS ] $currency — no fallback available, suppressing fiat display")
|
||||
return
|
||||
}
|
||||
.also { fresh -> fiatCache.putRate(currency, fresh) }
|
||||
}
|
||||
|
||||
// ← fix: delegate to balanceVm instead of writing to _balanceState
|
||||
val currentSats = (balanceVm.balanceState.value as? BalanceState.Success)?.sats ?: return
|
||||
val fiat = satsToFiat(currentSats, rate)
|
||||
Log.d(TAG, "FIAT [APPLY ] $currentSats sats ÷ $rate sats/$currency = $fiat $currency")
|
||||
balanceVm.applyFiat(fiat, currency)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── Currency list ────────────────────────────────────────────────────────
|
||||
suspend fun getSupportedCurrencies(): List<String> {
|
||||
fiatCache.getCurrencies()?.let { return it }
|
||||
// Cache miss (memory + disk) — fetch from network
|
||||
return fetchAndCacheCurrencies()
|
||||
}
|
||||
|
||||
private suspend fun fetchAndCacheCurrencies(): List<String> {
|
||||
return runCatching { repo.getSupportedCurrencies() }
|
||||
.getOrElse { e ->
|
||||
Log.w(TAG, "FIAT [CURRENCIES] network fetch failed — ${e.message}")
|
||||
emptyList()
|
||||
}
|
||||
.also { list -> fiatCache.putCurrencies(list) }
|
||||
}
|
||||
|
||||
// ── WebSocket event collection ────────────────────────────────────────────
|
||||
|
||||
private fun observePaymentEvents() {
|
||||
viewModelScope.launch {
|
||||
balanceVm.incomingPayment.collect { event ->
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
||||
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
|
||||
import com.bitcointxoko.gudariwallet.service.NodeAliasService
|
||||
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
||||
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
|
||||
|
||||
class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
@@ -23,6 +24,8 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
||||
val repo = LNbitsWalletRepository(api = api, secrets = secrets, context = app)
|
||||
val balancePrefs = BalancePrefsStore(app)
|
||||
val balanceVm = BalanceViewModel(repo, balancePrefs)
|
||||
val fiatVm = FiatViewModel(repo, balancePrefs, balanceVm)
|
||||
balanceVm.onBalanceRefreshed = { fiatVm.onBalanceRefreshed() }
|
||||
val lnurlCache = LnurlCacheRepository(app)
|
||||
val paymentCache = PaymentCacheRepository(app)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -32,7 +35,8 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
||||
lnurlCache,
|
||||
paymentCache,
|
||||
balancePrefs,
|
||||
balanceVm
|
||||
balanceVm,
|
||||
fiatVm
|
||||
) as T
|
||||
}
|
||||
}
|
||||
@@ -27,12 +27,13 @@ class BalanceViewModel(
|
||||
// Emits after every successful balance refresh so other ViewModels
|
||||
// (e.g. FiatViewModel) can react without polling.
|
||||
private val _balanceRefreshed = MutableSharedFlow<Long>(extraBufferCapacity = 1)
|
||||
val balanceRefreshed: SharedFlow<Long> = _balanceRefreshed
|
||||
|
||||
// Emits incoming payment events so ReceiveViewModel can update its state.
|
||||
private val _incomingPayment = MutableSharedFlow<IncomingPaymentEvent>(extraBufferCapacity = 1)
|
||||
val incomingPayment: SharedFlow<IncomingPaymentEvent> = _incomingPayment
|
||||
|
||||
var onBalanceRefreshed: (() -> Unit)? = null
|
||||
|
||||
init {
|
||||
loadBalanceWithCache()
|
||||
observePaymentEvents()
|
||||
@@ -85,7 +86,8 @@ class BalanceViewModel(
|
||||
lastUpdated = System.currentTimeMillis()
|
||||
)
|
||||
persistBalance(sats)
|
||||
_balanceRefreshed.tryEmit(sats) // notify FiatViewModel
|
||||
_balanceRefreshed.tryEmit(sats)
|
||||
onBalanceRefreshed?.invoke()
|
||||
}
|
||||
.onFailure { e ->
|
||||
Log.w(TAG, "BALANCE [NET ERROR ] ${e.message}")
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.bitcointxoko.gudariwallet.ui.fiat
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
||||
import com.bitcointxoko.gudariwallet.data.FiatRateCache
|
||||
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||
import com.bitcointxoko.gudariwallet.ui.BalanceState
|
||||
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
||||
import com.bitcointxoko.gudariwallet.util.satsToFiat
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val TAG = "FiatViewModel"
|
||||
private const val RATE_TTL_MS = 5 * 60 * 1_000L // 5 minutes
|
||||
|
||||
class FiatViewModel(
|
||||
private val repo : WalletRepository,
|
||||
private val balancePrefs: BalancePrefsStore,
|
||||
private val balanceVm : BalanceViewModel,
|
||||
) : ViewModel() {
|
||||
|
||||
private val fiatCache = FiatRateCache(balancePrefs)
|
||||
|
||||
val fiatRate: Double?
|
||||
get() = fiatCache.currentRate
|
||||
|
||||
val fiatCurrency: String?
|
||||
get() = _selectedCurrency.value
|
||||
|
||||
// ── Balance hidden ───────────────────────────────────────────────────────
|
||||
private val _balanceHidden = MutableStateFlow(balancePrefs.isBalanceHidden)
|
||||
val balanceHidden: StateFlow<Boolean> = _balanceHidden
|
||||
|
||||
fun toggleBalanceVisibility() {
|
||||
val next = !_balanceHidden.value
|
||||
_balanceHidden.value = next
|
||||
balancePrefs.setBalanceHidden(next)
|
||||
}
|
||||
|
||||
// ── Selected currency ────────────────────────────────────────────────────
|
||||
private val _selectedCurrency = MutableStateFlow(balancePrefs.selectedCurrency)
|
||||
val selectedCurrency: StateFlow<String?> = _selectedCurrency
|
||||
|
||||
fun setSelectedCurrency(currency: String?) {
|
||||
if (currency == _selectedCurrency.value) return
|
||||
_selectedCurrency.value = currency
|
||||
balancePrefs.setSelectedCurrency(currency)
|
||||
fiatCache.invalidateRate()
|
||||
Log.d(TAG, "FIAT [CURRENCY ] changed to ${currency ?: "none"} — cache invalidated")
|
||||
if (currency != null) {
|
||||
viewModelScope.launch { fetchAndApplyFiatRate() }
|
||||
} else {
|
||||
balanceVm.applyFiat(null, null)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sats-per-fiat-unit derived flow ──────────────────────────────────────
|
||||
// Used by UnitWheelPicker to convert between sats and fiat amounts.
|
||||
val fiatSatsPerUnit: StateFlow<Double?> = balanceVm.balanceState
|
||||
.mapNotNull { state ->
|
||||
(state as? BalanceState.Success)?.let { s ->
|
||||
if (s.fiatAmount != null && s.sats > 0)
|
||||
s.sats.toDouble() / s.fiatAmount
|
||||
else null
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
// ── Called by BalanceViewModel after every successful balance refresh ────
|
||||
// Keeps fiat display in sync whenever the balance changes.
|
||||
fun onBalanceRefreshed() {
|
||||
viewModelScope.launch { fetchAndApplyFiatRate() }
|
||||
}
|
||||
|
||||
// ── Rate fetch + apply ───────────────────────────────────────────────────
|
||||
private suspend fun fetchAndApplyFiatRate() {
|
||||
val currency = _selectedCurrency.value ?: return
|
||||
|
||||
val rate: Double = fiatCache.getRate(currency, RATE_TTL_MS)
|
||||
?: run {
|
||||
runCatching { repo.getFiatRate(currency) }
|
||||
.getOrElse { e ->
|
||||
Log.w(TAG, "FIAT [RATE ERROR ] $currency — ${e.message}")
|
||||
Log.w(TAG, "FIAT [RATE MISS ] $currency — no fallback available, suppressing fiat display")
|
||||
return
|
||||
}
|
||||
.also { fresh -> fiatCache.putRate(currency, fresh) }
|
||||
}
|
||||
|
||||
val currentSats = (balanceVm.balanceState.value as? BalanceState.Success)?.sats ?: return
|
||||
val fiat = satsToFiat(currentSats, rate)
|
||||
Log.d(TAG, "FIAT [APPLY ] $currentSats sats ÷ $rate sats/$currency = $fiat $currency")
|
||||
balanceVm.applyFiat(fiat, currency)
|
||||
}
|
||||
|
||||
// ── Currency list ────────────────────────────────────────────────────────
|
||||
suspend fun getSupportedCurrencies(): List<String> {
|
||||
fiatCache.getCurrencies()?.let { return it }
|
||||
return fetchAndCacheCurrencies()
|
||||
}
|
||||
|
||||
private suspend fun fetchAndCacheCurrencies(): List<String> {
|
||||
return runCatching { repo.getSupportedCurrencies() }
|
||||
.getOrElse { e ->
|
||||
Log.w(TAG, "FIAT [CURRENCIES] network fetch failed — ${e.message}")
|
||||
emptyList()
|
||||
}
|
||||
.also { list -> fiatCache.putCurrencies(list) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user