refactor: WalletViewModel BalancePrefs
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
package com.bitcointxoko.gudariwallet.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -23,55 +21,42 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.core.content.edit
|
||||
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.data.BalancePrefsStore
|
||||
import com.bitcointxoko.gudariwallet.data.FiatRateCache
|
||||
import com.bitcointxoko.gudariwallet.util.BiometricHelper
|
||||
|
||||
|
||||
private const val TAG = "WalletViewModel"
|
||||
|
||||
class WalletViewModel(
|
||||
val repo : WalletRepository,
|
||||
context : Context,
|
||||
val repo : WalletRepository,
|
||||
val aliasRepo : NodeAliasRepository,
|
||||
private val lnurlCache: LnurlCacheRepository,
|
||||
val paymentCache: PaymentCacheRepository
|
||||
val lnurlCache: LnurlCacheRepository,
|
||||
val paymentCache: PaymentCacheRepository,
|
||||
private val balancePrefs: BalancePrefsStore,
|
||||
) : ViewModel() {
|
||||
|
||||
// ── Balance prefs ─────────────────────────────────────────────────────────
|
||||
|
||||
private val balancePrefs: SharedPreferences = context.getSharedPreferences(
|
||||
WalletConstants.BALANCE_PREFS_NAME, Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
// ── Balance state ─────────────────────────────────────────────────────────
|
||||
|
||||
private val _balanceState = MutableStateFlow<BalanceState>(BalanceState.Loading)
|
||||
val balanceState: StateFlow<BalanceState> = _balanceState
|
||||
|
||||
// ── Balance visible state ─────────────────────────────────────────────────
|
||||
|
||||
private val _balanceHidden = MutableStateFlow(
|
||||
balancePrefs.getBoolean("balance_hidden", false)
|
||||
)
|
||||
private val _balanceHidden = MutableStateFlow(balancePrefs.isBalanceHidden)
|
||||
val balanceHidden: StateFlow<Boolean> = _balanceHidden
|
||||
|
||||
fun toggleBalanceVisibility() {
|
||||
val next = !_balanceHidden.value
|
||||
_balanceHidden.value = next
|
||||
balancePrefs.edit { putBoolean("balance_hidden", next) }
|
||||
balancePrefs.setBalanceHidden(next)
|
||||
}
|
||||
|
||||
// ── Fiat cache ────────────────────────────────────────────────────────────
|
||||
private var cachedRate : Double? = null // in-memory rate value
|
||||
private var rateLastFetchedAt : Long = 0L // epoch ms of last fetch
|
||||
private var cachedRateCurrency : String = "" // currency the cached rate is for
|
||||
private var cachedCurrencies : List<String>? = null
|
||||
val fiatRate: Double? get() = cachedRate.takeIf { cachedRateCurrency.isNotBlank() }
|
||||
private val fiatCache = FiatRateCache(balancePrefs)
|
||||
val fiatRate: Double?
|
||||
get() = fiatCache.currentRate
|
||||
val fiatCurrency: String? get() = _selectedCurrency.value
|
||||
val fiatSatsPerUnit: StateFlow<Double?> = balanceState
|
||||
.mapNotNull { state ->
|
||||
@@ -86,29 +71,18 @@ class WalletViewModel(
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = null
|
||||
)
|
||||
private val _selectedCurrency = MutableStateFlow(
|
||||
balancePrefs.getString("fiat_currency", null)
|
||||
)
|
||||
private val _selectedCurrency = MutableStateFlow(balancePrefs.selectedCurrency)
|
||||
val selectedCurrency: StateFlow<String?> = _selectedCurrency
|
||||
|
||||
fun setSelectedCurrency(currency: String?) {
|
||||
if (currency == _selectedCurrency.value) return
|
||||
_selectedCurrency.value = currency
|
||||
if (currency != null) {
|
||||
balancePrefs.edit { putString("fiat_currency", currency) }
|
||||
} else {
|
||||
// Remove the key so next cold start also starts with null
|
||||
balancePrefs.edit { remove("fiat_currency") }
|
||||
}
|
||||
// Invalidate rate cache regardless
|
||||
cachedRate = null
|
||||
rateLastFetchedAt = 0L
|
||||
cachedRateCurrency = ""
|
||||
balancePrefs.setSelectedCurrency(currency)
|
||||
fiatCache.invalidateRate() // ← replaces the three raw-var resets
|
||||
Log.d(TAG, "FIAT [CURRENCY ] changed to ${currency ?: "none"} — cache invalidated")
|
||||
if (currency != null) {
|
||||
viewModelScope.launch { fetchAndApplyFiatRate() }
|
||||
} else {
|
||||
// Clear fiat from balance state immediately
|
||||
val current = _balanceState.value as? BalanceState.Success ?: return
|
||||
_balanceState.value = current.copy(fiatAmount = null, fiatCurrency = null)
|
||||
}
|
||||
@@ -123,16 +97,11 @@ class WalletViewModel(
|
||||
|
||||
private val _receiveState = MutableStateFlow<ReceiveState>(ReceiveState.Idle)
|
||||
val receiveState: StateFlow<ReceiveState> = _receiveState
|
||||
|
||||
private val _sendState = MutableStateFlow<SendState>(SendState.Idle)
|
||||
val sendState: StateFlow<SendState> = _sendState
|
||||
|
||||
private val _navigateToReceive = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
val navigateToReceive: SharedFlow<Unit> = _navigateToReceive
|
||||
|
||||
private val _lightningAddress = MutableStateFlow<String?>(
|
||||
balancePrefs.getString("lightning_address", null)
|
||||
)
|
||||
private val _lightningAddress = MutableStateFlow<String?>(balancePrefs.lightningAddress)
|
||||
val lightningAddress: StateFlow<String?> = _lightningAddress
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
@@ -146,8 +115,8 @@ class WalletViewModel(
|
||||
// ── Balance loading ───────────────────────────────────────────────────────
|
||||
|
||||
private fun loadBalanceWithCache() {
|
||||
val savedSats = balancePrefs.getLong(WalletConstants.BALANCE_PREFS_KEY, -1L)
|
||||
val savedAt = balancePrefs.getLong(WalletConstants.BALANCE_PREFS_SAVED_AT, 0L)
|
||||
val savedSats = balancePrefs.savedBalanceSats
|
||||
val savedAt = balancePrefs.balanceSavedAt
|
||||
val ageMs = System.currentTimeMillis() - savedAt
|
||||
|
||||
if (savedSats >= 0 && ageMs < WalletConstants.BALANCE_CACHE_TTL_MS) {
|
||||
@@ -197,7 +166,7 @@ class WalletViewModel(
|
||||
.onFailure { e ->
|
||||
Log.w(TAG, "BALANCE [NET ERROR ] ${e.message}")
|
||||
val staleSats = (_balanceState.value as? BalanceState.Success)?.sats
|
||||
?: balancePrefs.getLong(WalletConstants.BALANCE_PREFS_KEY, -1L).takeIf { it >= 0 }
|
||||
?: balancePrefs.savedBalanceSats.takeIf { it >= 0 }
|
||||
if (staleSats != null) {
|
||||
Log.w(TAG, "BALANCE [NET ERROR ] Falling back to stale value: $staleSats sats")
|
||||
} else {
|
||||
@@ -210,91 +179,38 @@ class WalletViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun persistBalance(sats: Long) {
|
||||
balancePrefs.edit {
|
||||
putLong(WalletConstants.BALANCE_PREFS_KEY, sats)
|
||||
.putLong(WalletConstants.BALANCE_PREFS_SAVED_AT, System.currentTimeMillis())
|
||||
}
|
||||
balancePrefs.persistBalance(sats)
|
||||
Log.d(TAG, "BALANCE [PERSIST ] $sats sats written to SharedPreferences")
|
||||
}
|
||||
|
||||
// ── Fiat rate fetch + apply ───────────────────────────────────────────────
|
||||
private suspend fun fetchAndApplyFiatRate() {
|
||||
val currency = _selectedCurrency.value ?: return
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
// ── 1. Serve from in-memory cache if still fresh ──────────────────────
|
||||
val inMemoryFresh = cachedRate != null
|
||||
&& cachedRateCurrency == currency
|
||||
&& (now - rateLastFetchedAt) < RATE_TTL_MS
|
||||
|
||||
val rate: Double = if (inMemoryFresh) {
|
||||
Log.d(TAG, "FIAT [RATE CACHE ] $currency — in-memory hit (age ${(now - rateLastFetchedAt) / 1000}s)")
|
||||
cachedRate!!
|
||||
} else {
|
||||
// ── 2. Fetch from network ─────────────────────────────────────────
|
||||
runCatching { repo.getFiatRate(currency) }
|
||||
.getOrElse { e ->
|
||||
Log.w(TAG, "FIAT [RATE ERROR ] $currency — ${e.message}")
|
||||
// ── 3. Fall back to last persisted rate ───────────────────
|
||||
val persisted = balancePrefs.getFloat("fiat_rate_$currency", 0f).toDouble()
|
||||
if (persisted > 0.0) {
|
||||
Log.w(TAG, "FIAT [RATE STALE ] $currency — using persisted rate $persisted")
|
||||
persisted
|
||||
} else {
|
||||
val rate: Double = fiatCache.getRate(currency, RATE_TTL_MS)
|
||||
?: run {
|
||||
// Cache miss — fetch from network
|
||||
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 // nothing to show; leave fiatAmount = null
|
||||
}
|
||||
}
|
||||
.also { fresh ->
|
||||
// Update in-memory cache
|
||||
cachedRate = fresh
|
||||
rateLastFetchedAt = now
|
||||
cachedRateCurrency = currency
|
||||
// Persist for next cold start
|
||||
balancePrefs.edit {
|
||||
putFloat("fiat_rate_$currency", fresh.toFloat())
|
||||
.putLong("fiat_rate_fetched_at_$currency", now)
|
||||
}
|
||||
Log.d(TAG, "FIAT [RATE FRESH ] 1 $currency = $fresh sats — cached + persisted")
|
||||
}
|
||||
}
|
||||
// ── 4. Apply to current balance state ─────────────────────────────────
|
||||
.also { fresh -> fiatCache.putRate(currency, fresh) }
|
||||
}
|
||||
|
||||
val current = _balanceState.value as? BalanceState.Success ?: return
|
||||
val fiat = satsToFiat(current.sats, rate)
|
||||
Log.d(TAG, "FIAT [APPLY ] ${current.sats} sats ÷ $rate sats/$currency = $fiat $currency")
|
||||
_balanceState.value = current.copy(
|
||||
fiatAmount = fiat,
|
||||
fiatCurrency = currency
|
||||
)
|
||||
_balanceState.value = current.copy(fiatAmount = fiat, fiatCurrency = currency)
|
||||
}
|
||||
|
||||
|
||||
// ── Currency list ────────────────────────────────────────────────────────
|
||||
suspend fun getSupportedCurrencies(): List<String> {
|
||||
// 1. In-memory
|
||||
cachedCurrencies?.let {
|
||||
Log.d(TAG, "FIAT [CURRENCIES] in-memory hit (${it.size} currencies)")
|
||||
return it
|
||||
}
|
||||
// 2. SharedPreferences
|
||||
val json = balancePrefs.getString("fiat_currencies_json", null)
|
||||
if (json != null) {
|
||||
return try {
|
||||
val fromPrefs = json
|
||||
.removeSurrounding("[", "]")
|
||||
.split(",")
|
||||
.map { it.trim().removeSurrounding("\"") }
|
||||
.filter { it.isNotBlank() }
|
||||
Log.d(TAG, "FIAT [CURRENCIES] prefs hit (${fromPrefs.size} currencies)")
|
||||
cachedCurrencies = fromPrefs
|
||||
fromPrefs
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "FIAT [CURRENCIES] prefs parse failed — fetching from network")
|
||||
fetchAndCacheCurrencies()
|
||||
}
|
||||
}
|
||||
// 3. Network
|
||||
fiatCache.getCurrencies()?.let { return it }
|
||||
// Cache miss (memory + disk) — fetch from network
|
||||
return fetchAndCacheCurrencies()
|
||||
}
|
||||
|
||||
@@ -304,15 +220,7 @@ class WalletViewModel(
|
||||
Log.w(TAG, "FIAT [CURRENCIES] network fetch failed — ${e.message}")
|
||||
emptyList()
|
||||
}
|
||||
.also { list ->
|
||||
if (list.isNotEmpty()) {
|
||||
cachedCurrencies = list
|
||||
// Persist as a simple JSON array string
|
||||
val json = "[${list.joinToString(",") { "\"$it\"" }}]"
|
||||
balancePrefs.edit { putString("fiat_currencies_json", json) }
|
||||
Log.d(TAG, "FIAT [CURRENCIES] fetched + cached ${list.size} currencies")
|
||||
}
|
||||
}
|
||||
.also { list -> fiatCache.putCurrencies(list) }
|
||||
}
|
||||
|
||||
// ── WebSocket event collection ────────────────────────────────────────────
|
||||
@@ -361,7 +269,7 @@ class WalletViewModel(
|
||||
if (address != null) {
|
||||
Log.d(TAG, "LNADDR [FETCH OK ] $address")
|
||||
_lightningAddress.value = address
|
||||
balancePrefs.edit { putString("lightning_address", address) }
|
||||
balancePrefs.setLightningAddress(address)
|
||||
} else {
|
||||
Log.d(TAG, "LNADDR [FETCH OK ] no pay link / username found")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user