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.SendInputDetector
|
||||||
import com.bitcointxoko.gudariwallet.util.SendInputType
|
import com.bitcointxoko.gudariwallet.util.SendInputType
|
||||||
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||||
import com.bitcointxoko.gudariwallet.util.satsToFiat
|
|
||||||
import com.bitcointxoko.gudariwallet.util.parseApiError
|
import com.bitcointxoko.gudariwallet.util.parseApiError
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
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 com.bitcointxoko.gudariwallet.util.lnurlProtocolError
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.bitcointxoko.gudariwallet.api.RouteHintHop
|
import com.bitcointxoko.gudariwallet.api.RouteHintHop
|
||||||
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
||||||
import com.bitcointxoko.gudariwallet.data.FiatRateCache
|
|
||||||
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
|
||||||
import com.bitcointxoko.gudariwallet.util.BiometricHelper
|
import com.bitcointxoko.gudariwallet.util.BiometricHelper
|
||||||
|
|
||||||
private const val TAG = "WalletViewModel"
|
private const val TAG = "WalletViewModel"
|
||||||
@@ -40,60 +36,22 @@ class WalletViewModel(
|
|||||||
val paymentCache: PaymentCacheRepository,
|
val paymentCache: PaymentCacheRepository,
|
||||||
private val balancePrefs: BalancePrefsStore,
|
private val balancePrefs: BalancePrefsStore,
|
||||||
val balanceVm : BalanceViewModel,
|
val balanceVm : BalanceViewModel,
|
||||||
|
val fiatVm : FiatViewModel
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// ── Balance state ─────────────────────────────────────────────────────────
|
// ── Balance state ─────────────────────────────────────────────────────────
|
||||||
val balanceState: StateFlow<BalanceState> get() = balanceVm.balanceState
|
val balanceState: StateFlow<BalanceState> get() = balanceVm.balanceState
|
||||||
// ── Balance visible state ─────────────────────────────────────────────────
|
val balanceHidden: StateFlow<Boolean> get() = fiatVm.balanceHidden
|
||||||
private val _balanceHidden = MutableStateFlow(balancePrefs.isBalanceHidden)
|
val selectedCurrency: StateFlow<String?> get() = fiatVm.selectedCurrency
|
||||||
val balanceHidden: StateFlow<Boolean> = _balanceHidden
|
val fiatSatsPerUnit: StateFlow<Double?> get() = fiatVm.fiatSatsPerUnit
|
||||||
fun toggleBalanceVisibility() {
|
fun toggleBalanceVisibility() = fiatVm.toggleBalanceVisibility()
|
||||||
val next = !_balanceHidden.value
|
fun setSelectedCurrency(c: String?) = fiatVm.setSelectedCurrency(c)
|
||||||
_balanceHidden.value = next
|
suspend fun getSupportedCurrencies() = fiatVm.getSupportedCurrencies()
|
||||||
balancePrefs.setBalanceHidden(next)
|
val fiatRate: Double? get() = fiatVm.fiatRate
|
||||||
}
|
val fiatCurrency: String? get() = fiatVm.fiatCurrency
|
||||||
|
|
||||||
// ── 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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ── Other states ──────────────────────────────────────────────────────────
|
// ── Other states ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private val _receiveState = MutableStateFlow<ReceiveState>(ReceiveState.Idle)
|
private val _receiveState = MutableStateFlow<ReceiveState>(ReceiveState.Idle)
|
||||||
val receiveState: StateFlow<ReceiveState> = _receiveState
|
val receiveState: StateFlow<ReceiveState> = _receiveState
|
||||||
private val _sendState = MutableStateFlow<SendState>(SendState.Idle)
|
private val _sendState = MutableStateFlow<SendState>(SendState.Idle)
|
||||||
@@ -110,48 +68,7 @@ class WalletViewModel(
|
|||||||
fetchLightningAddress()
|
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 ────────────────────────────────────────────
|
// ── WebSocket event collection ────────────────────────────────────────────
|
||||||
|
|
||||||
private fun observePaymentEvents() {
|
private fun observePaymentEvents() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
balanceVm.incomingPayment.collect { event ->
|
balanceVm.incomingPayment.collect { event ->
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
|||||||
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
|
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
|
||||||
import com.bitcointxoko.gudariwallet.service.NodeAliasService
|
import com.bitcointxoko.gudariwallet.service.NodeAliasService
|
||||||
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
|
||||||
|
|
||||||
class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
|
class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
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 repo = LNbitsWalletRepository(api = api, secrets = secrets, context = app)
|
||||||
val balancePrefs = BalancePrefsStore(app)
|
val balancePrefs = BalancePrefsStore(app)
|
||||||
val balanceVm = BalanceViewModel(repo, balancePrefs)
|
val balanceVm = BalanceViewModel(repo, balancePrefs)
|
||||||
|
val fiatVm = FiatViewModel(repo, balancePrefs, balanceVm)
|
||||||
|
balanceVm.onBalanceRefreshed = { fiatVm.onBalanceRefreshed() }
|
||||||
val lnurlCache = LnurlCacheRepository(app)
|
val lnurlCache = LnurlCacheRepository(app)
|
||||||
val paymentCache = PaymentCacheRepository(app)
|
val paymentCache = PaymentCacheRepository(app)
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@@ -32,7 +35,8 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
|||||||
lnurlCache,
|
lnurlCache,
|
||||||
paymentCache,
|
paymentCache,
|
||||||
balancePrefs,
|
balancePrefs,
|
||||||
balanceVm
|
balanceVm,
|
||||||
|
fiatVm
|
||||||
) as T
|
) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,12 +27,13 @@ class BalanceViewModel(
|
|||||||
// Emits after every successful balance refresh so other ViewModels
|
// Emits after every successful balance refresh so other ViewModels
|
||||||
// (e.g. FiatViewModel) can react without polling.
|
// (e.g. FiatViewModel) can react without polling.
|
||||||
private val _balanceRefreshed = MutableSharedFlow<Long>(extraBufferCapacity = 1)
|
private val _balanceRefreshed = MutableSharedFlow<Long>(extraBufferCapacity = 1)
|
||||||
val balanceRefreshed: SharedFlow<Long> = _balanceRefreshed
|
|
||||||
|
|
||||||
// Emits incoming payment events so ReceiveViewModel can update its state.
|
// Emits incoming payment events so ReceiveViewModel can update its state.
|
||||||
private val _incomingPayment = MutableSharedFlow<IncomingPaymentEvent>(extraBufferCapacity = 1)
|
private val _incomingPayment = MutableSharedFlow<IncomingPaymentEvent>(extraBufferCapacity = 1)
|
||||||
val incomingPayment: SharedFlow<IncomingPaymentEvent> = _incomingPayment
|
val incomingPayment: SharedFlow<IncomingPaymentEvent> = _incomingPayment
|
||||||
|
|
||||||
|
var onBalanceRefreshed: (() -> Unit)? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadBalanceWithCache()
|
loadBalanceWithCache()
|
||||||
observePaymentEvents()
|
observePaymentEvents()
|
||||||
@@ -85,7 +86,8 @@ class BalanceViewModel(
|
|||||||
lastUpdated = System.currentTimeMillis()
|
lastUpdated = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
persistBalance(sats)
|
persistBalance(sats)
|
||||||
_balanceRefreshed.tryEmit(sats) // notify FiatViewModel
|
_balanceRefreshed.tryEmit(sats)
|
||||||
|
onBalanceRefreshed?.invoke()
|
||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
Log.w(TAG, "BALANCE [NET ERROR ] ${e.message}")
|
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