refactor: WalletViewModel extract BalanceViewModel
This commit is contained in:
@@ -8,7 +8,6 @@ import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
|
|||||||
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
|
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||||
import com.bitcointxoko.gudariwallet.service.WalletNotificationService
|
|
||||||
import com.bitcointxoko.gudariwallet.util.LnurlMetadataParser
|
import com.bitcointxoko.gudariwallet.util.LnurlMetadataParser
|
||||||
import com.bitcointxoko.gudariwallet.util.RouteHintAnalyzer
|
import com.bitcointxoko.gudariwallet.util.RouteHintAnalyzer
|
||||||
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
||||||
@@ -29,6 +28,7 @@ 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.data.FiatRateCache
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
||||||
import com.bitcointxoko.gudariwallet.util.BiometricHelper
|
import com.bitcointxoko.gudariwallet.util.BiometricHelper
|
||||||
|
|
||||||
private const val TAG = "WalletViewModel"
|
private const val TAG = "WalletViewModel"
|
||||||
@@ -39,12 +39,11 @@ class WalletViewModel(
|
|||||||
val lnurlCache: LnurlCacheRepository,
|
val lnurlCache: LnurlCacheRepository,
|
||||||
val paymentCache: PaymentCacheRepository,
|
val paymentCache: PaymentCacheRepository,
|
||||||
private val balancePrefs: BalancePrefsStore,
|
private val balancePrefs: BalancePrefsStore,
|
||||||
|
val balanceVm : BalanceViewModel,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// ── Balance state ─────────────────────────────────────────────────────────
|
// ── Balance state ─────────────────────────────────────────────────────────
|
||||||
private val _balanceState = MutableStateFlow<BalanceState>(BalanceState.Loading)
|
val balanceState: StateFlow<BalanceState> get() = balanceVm.balanceState
|
||||||
val balanceState: StateFlow<BalanceState> = _balanceState
|
|
||||||
|
|
||||||
// ── Balance visible state ─────────────────────────────────────────────────
|
// ── Balance visible state ─────────────────────────────────────────────────
|
||||||
private val _balanceHidden = MutableStateFlow(balancePrefs.isBalanceHidden)
|
private val _balanceHidden = MutableStateFlow(balancePrefs.isBalanceHidden)
|
||||||
val balanceHidden: StateFlow<Boolean> = _balanceHidden
|
val balanceHidden: StateFlow<Boolean> = _balanceHidden
|
||||||
@@ -59,7 +58,7 @@ class WalletViewModel(
|
|||||||
val fiatRate: Double?
|
val fiatRate: Double?
|
||||||
get() = fiatCache.currentRate
|
get() = fiatCache.currentRate
|
||||||
val fiatCurrency: String? get() = _selectedCurrency.value
|
val fiatCurrency: String? get() = _selectedCurrency.value
|
||||||
val fiatSatsPerUnit: StateFlow<Double?> = balanceState
|
val fiatSatsPerUnit: StateFlow<Double?> = balanceVm.balanceState
|
||||||
.mapNotNull { state ->
|
.mapNotNull { state ->
|
||||||
(state as? BalanceState.Success)?.let { s ->
|
(state as? BalanceState.Success)?.let { s ->
|
||||||
if (s.fiatAmount != null && s.sats > 0)
|
if (s.fiatAmount != null && s.sats > 0)
|
||||||
@@ -79,13 +78,12 @@ class WalletViewModel(
|
|||||||
if (currency == _selectedCurrency.value) return
|
if (currency == _selectedCurrency.value) return
|
||||||
_selectedCurrency.value = currency
|
_selectedCurrency.value = currency
|
||||||
balancePrefs.setSelectedCurrency(currency)
|
balancePrefs.setSelectedCurrency(currency)
|
||||||
fiatCache.invalidateRate() // ← replaces the three raw-var resets
|
fiatCache.invalidateRate()
|
||||||
Log.d(TAG, "FIAT [CURRENCY ] changed to ${currency ?: "none"} — cache invalidated")
|
Log.d(TAG, "FIAT [CURRENCY ] changed to ${currency ?: "none"} — cache invalidated")
|
||||||
if (currency != null) {
|
if (currency != null) {
|
||||||
viewModelScope.launch { fetchAndApplyFiatRate() }
|
viewModelScope.launch { fetchAndApplyFiatRate() }
|
||||||
} else {
|
} else {
|
||||||
val current = _balanceState.value as? BalanceState.Success ?: return
|
balanceVm.applyFiat(null, null) // ← fix
|
||||||
_balanceState.value = current.copy(fiatAmount = null, fiatCurrency = null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,106 +106,34 @@ class WalletViewModel(
|
|||||||
// ── Init ──────────────────────────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadBalanceWithCache()
|
|
||||||
observePaymentEvents()
|
observePaymentEvents()
|
||||||
fetchLightningAddress()
|
fetchLightningAddress()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Balance loading ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private fun loadBalanceWithCache() {
|
|
||||||
val savedSats = balancePrefs.savedBalanceSats
|
|
||||||
val savedAt = balancePrefs.balanceSavedAt
|
|
||||||
val ageMs = System.currentTimeMillis() - savedAt
|
|
||||||
|
|
||||||
if (savedSats >= 0 && ageMs < WalletConstants.BALANCE_CACHE_TTL_MS) {
|
|
||||||
Log.d(TAG, "BALANCE [CACHE HIT ] $savedSats sats (age ${ageMs / 1000}s) — showing immediately")
|
|
||||||
_balanceState.value = BalanceState.Success(
|
|
||||||
sats = savedSats,
|
|
||||||
isRefreshing = true,
|
|
||||||
lastUpdated = savedAt
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if (savedSats >= 0) {
|
|
||||||
Log.d(TAG, "BALANCE [CACHE STALE] $savedSats sats, age ${ageMs / 1000}s > TTL ${WalletConstants.BALANCE_CACHE_TTL_MS / 1000}s — cold load")
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "BALANCE [CACHE MISS ] No persisted balance — cold load")
|
|
||||||
}
|
|
||||||
_balanceState.value = BalanceState.Loading
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshBalance()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshBalance() {
|
|
||||||
val current = _balanceState.value
|
|
||||||
if (current is BalanceState.Success && !current.isRefreshing) {
|
|
||||||
Log.d(TAG, "BALANCE [REFRESH ] Manual refresh triggered (current: ${current.sats} sats)")
|
|
||||||
_balanceState.value = current.copy(isRefreshing = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
runCatching { repo.getBalance() }
|
|
||||||
.onSuccess { balance ->
|
|
||||||
val sats = balance.sats
|
|
||||||
val prev = (_balanceState.value as? BalanceState.Success)?.sats
|
|
||||||
when {
|
|
||||||
prev == null -> Log.d(TAG, "BALANCE [NETWORK ] $sats sats — cold load complete")
|
|
||||||
prev == sats -> Log.d(TAG, "BALANCE [NETWORK ] $sats sats — matches cache, no change")
|
|
||||||
else -> Log.d(TAG, "BALANCE [NETWORK ] $sats sats — was $prev sats (diff ${sats - prev})")
|
|
||||||
}
|
|
||||||
_balanceState.value = BalanceState.Success(
|
|
||||||
sats = sats,
|
|
||||||
isRefreshing = false,
|
|
||||||
lastUpdated = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
persistBalance(sats)
|
|
||||||
fetchAndApplyFiatRate()
|
|
||||||
}
|
|
||||||
.onFailure { e ->
|
|
||||||
Log.w(TAG, "BALANCE [NET ERROR ] ${e.message}")
|
|
||||||
val staleSats = (_balanceState.value as? BalanceState.Success)?.sats
|
|
||||||
?: balancePrefs.savedBalanceSats.takeIf { it >= 0 }
|
|
||||||
if (staleSats != null) {
|
|
||||||
Log.w(TAG, "BALANCE [NET ERROR ] Falling back to stale value: $staleSats sats")
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "BALANCE [NET ERROR ] No stale value available — showing error state")
|
|
||||||
}
|
|
||||||
_balanceState.value = BalanceState.Error(
|
|
||||||
message = e.message ?: "Failed to load balance",
|
|
||||||
staleSats = staleSats
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun persistBalance(sats: Long) {
|
|
||||||
balancePrefs.persistBalance(sats)
|
|
||||||
Log.d(TAG, "BALANCE [PERSIST ] $sats sats written to SharedPreferences")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Fiat rate fetch + apply ───────────────────────────────────────────────
|
// ── Fiat rate fetch + apply ───────────────────────────────────────────────
|
||||||
private suspend fun fetchAndApplyFiatRate() {
|
private suspend fun fetchAndApplyFiatRate() {
|
||||||
val currency = _selectedCurrency.value ?: return
|
val currency = _selectedCurrency.value ?: return
|
||||||
|
|
||||||
val rate: Double = fiatCache.getRate(currency, RATE_TTL_MS)
|
val rate: Double = fiatCache.getRate(currency, RATE_TTL_MS)
|
||||||
?: run {
|
?: run {
|
||||||
// Cache miss — fetch from network
|
|
||||||
runCatching { repo.getFiatRate(currency) }
|
runCatching { repo.getFiatRate(currency) }
|
||||||
.getOrElse { e ->
|
.getOrElse { e ->
|
||||||
Log.w(TAG, "FIAT [RATE ERROR ] $currency — ${e.message}")
|
Log.w(TAG, "FIAT [RATE ERROR ] $currency — ${e.message}")
|
||||||
Log.w(TAG, "FIAT [RATE MISS ] $currency — no fallback available, suppressing fiat display")
|
Log.w(TAG, "FIAT [RATE MISS ] $currency — no fallback available, suppressing fiat display")
|
||||||
return // nothing to show; leave fiatAmount = null
|
return
|
||||||
}
|
}
|
||||||
.also { fresh -> fiatCache.putRate(currency, fresh) }
|
.also { fresh -> fiatCache.putRate(currency, fresh) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val current = _balanceState.value as? BalanceState.Success ?: return
|
// ← fix: delegate to balanceVm instead of writing to _balanceState
|
||||||
val fiat = satsToFiat(current.sats, rate)
|
val currentSats = (balanceVm.balanceState.value as? BalanceState.Success)?.sats ?: return
|
||||||
Log.d(TAG, "FIAT [APPLY ] ${current.sats} sats ÷ $rate sats/$currency = $fiat $currency")
|
val fiat = satsToFiat(currentSats, rate)
|
||||||
_balanceState.value = current.copy(fiatAmount = fiat, fiatCurrency = currency)
|
Log.d(TAG, "FIAT [APPLY ] $currentSats sats ÷ $rate sats/$currency = $fiat $currency")
|
||||||
|
balanceVm.applyFiat(fiat, currency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ── Currency list ────────────────────────────────────────────────────────
|
// ── Currency list ────────────────────────────────────────────────────────
|
||||||
suspend fun getSupportedCurrencies(): List<String> {
|
suspend fun getSupportedCurrencies(): List<String> {
|
||||||
fiatCache.getCurrencies()?.let { return it }
|
fiatCache.getCurrencies()?.let { return it }
|
||||||
@@ -228,24 +154,7 @@ class WalletViewModel(
|
|||||||
|
|
||||||
private fun observePaymentEvents() {
|
private fun observePaymentEvents() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
WalletNotificationService.paymentEvents.collect { event ->
|
balanceVm.incomingPayment.collect { event ->
|
||||||
Log.d(TAG, "BALANCE [WS EVENT ] ${if (event.isOutgoing) "SENT" else "RECEIVED"} ${event.amountSats} sats")
|
|
||||||
|
|
||||||
val current = _balanceState.value
|
|
||||||
if (current is BalanceState.Success) {
|
|
||||||
val delta = if (event.isOutgoing) -event.amountSats else event.amountSats
|
|
||||||
val newSats = (current.sats + delta).coerceAtLeast(0L)
|
|
||||||
Log.d(TAG, "BALANCE [WS DELTA ] ${current.sats} ${if (delta >= 0) "+" else ""}$delta = $newSats sats (optimistic)")
|
|
||||||
_balanceState.value = current.copy(sats = newSats, isRefreshing = true)
|
|
||||||
persistBalance(newSats)
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "BALANCE [WS EVENT ] Received event but balance state is ${current::class.simpleName} — skipping delta")
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshBalance()
|
|
||||||
fetchAndApplyFiatRate()
|
|
||||||
|
|
||||||
if (!event.isOutgoing) {
|
|
||||||
val rs = _receiveState.value
|
val rs = _receiveState.value
|
||||||
if (rs is ReceiveState.InvoiceReady) {
|
if (rs is ReceiveState.InvoiceReady) {
|
||||||
Log.d(TAG, "BALANCE [WS RECEIVE] Marking invoice ${rs.paymentHash.take(8)}… as received")
|
Log.d(TAG, "BALANCE [WS RECEIVE] Marking invoice ${rs.paymentHash.take(8)}… as received")
|
||||||
@@ -258,9 +167,6 @@ class WalletViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun fetchLightningAddress() {
|
private fun fetchLightningAddress() {
|
||||||
// If we already have a persisted address, show it immediately and
|
// If we already have a persisted address, show it immediately and
|
||||||
// still refresh in the background (address could have changed).
|
// still refresh in the background (address could have changed).
|
||||||
@@ -672,7 +578,10 @@ class WalletViewModel(
|
|||||||
_clipboardOfferState.value = ClipboardOfferState.None
|
_clipboardOfferState.value = ClipboardOfferState.None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun refreshBalance() = balanceVm.refreshBalance()
|
||||||
|
|
||||||
// ── Private helpers ───────────────────────────────────────────────────────
|
// ── Private helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun buildLnurlReadyState(raw: LnurlScanResponse, lnurl: String): SendState.LnurlReady {
|
private fun buildLnurlReadyState(raw: LnurlScanResponse, lnurl: String): SendState.LnurlReady {
|
||||||
return SendState.LnurlReady(
|
return SendState.LnurlReady(
|
||||||
callback = raw.callback ?: "",
|
callback = raw.callback ?: "",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
|
|||||||
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
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
|
||||||
|
|
||||||
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 {
|
||||||
@@ -21,15 +22,17 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
|||||||
val aliasRepo = LNbitsNodeAliasRepository(nodeAliasSvc)
|
val aliasRepo = LNbitsNodeAliasRepository(nodeAliasSvc)
|
||||||
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 lnurlCache = LnurlCacheRepository(app)
|
val lnurlCache = LnurlCacheRepository(app)
|
||||||
val paymentCache = PaymentCacheRepository(app)
|
val paymentCache = PaymentCacheRepository(app)
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return WalletViewModel(
|
return WalletViewModel(
|
||||||
balancePrefs = balancePrefs,
|
repo,
|
||||||
repo = repo,
|
aliasRepo,
|
||||||
aliasRepo = aliasRepo,
|
lnurlCache,
|
||||||
lnurlCache = lnurlCache,
|
paymentCache,
|
||||||
paymentCache = paymentCache
|
balancePrefs,
|
||||||
|
balanceVm
|
||||||
) as T
|
) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package com.bitcointxoko.gudariwallet.ui.balance
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
||||||
|
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||||
|
import com.bitcointxoko.gudariwallet.service.WalletNotificationService
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.BalanceState
|
||||||
|
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private const val TAG = "BalanceViewModel"
|
||||||
|
|
||||||
|
class BalanceViewModel(
|
||||||
|
private val repo : WalletRepository,
|
||||||
|
private val balancePrefs : BalancePrefsStore,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _balanceState = MutableStateFlow<BalanceState>(BalanceState.Loading)
|
||||||
|
val balanceState: StateFlow<BalanceState> = _balanceState
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadBalanceWithCache()
|
||||||
|
observePaymentEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadBalanceWithCache() {
|
||||||
|
val savedSats = balancePrefs.savedBalanceSats
|
||||||
|
val savedAt = balancePrefs.balanceSavedAt
|
||||||
|
val ageMs = System.currentTimeMillis() - savedAt
|
||||||
|
|
||||||
|
if (savedSats >= 0 && ageMs < WalletConstants.BALANCE_CACHE_TTL_MS) {
|
||||||
|
Log.d(TAG, "BALANCE [CACHE HIT ] $savedSats sats (age ${ageMs / 1000}s) — showing immediately")
|
||||||
|
_balanceState.value = BalanceState.Success(
|
||||||
|
sats = savedSats,
|
||||||
|
isRefreshing = true,
|
||||||
|
lastUpdated = savedAt
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (savedSats >= 0) {
|
||||||
|
Log.d(TAG, "BALANCE [CACHE STALE] $savedSats sats, age ${ageMs / 1000}s > TTL ${WalletConstants.BALANCE_CACHE_TTL_MS / 1000}s — cold load")
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "BALANCE [CACHE MISS ] No persisted balance — cold load")
|
||||||
|
}
|
||||||
|
_balanceState.value = BalanceState.Loading
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshBalance()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshBalance() {
|
||||||
|
val current = _balanceState.value
|
||||||
|
if (current is BalanceState.Success && !current.isRefreshing) {
|
||||||
|
Log.d(TAG, "BALANCE [REFRESH ] Manual refresh triggered (current: ${current.sats} sats)")
|
||||||
|
_balanceState.value = current.copy(isRefreshing = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatching { repo.getBalance() }
|
||||||
|
.onSuccess { balance ->
|
||||||
|
val sats = balance.sats
|
||||||
|
val prev = (_balanceState.value as? BalanceState.Success)?.sats
|
||||||
|
when {
|
||||||
|
prev == null -> Log.d(TAG, "BALANCE [NETWORK ] $sats sats — cold load complete")
|
||||||
|
prev == sats -> Log.d(TAG, "BALANCE [NETWORK ] $sats sats — matches cache, no change")
|
||||||
|
else -> Log.d(TAG, "BALANCE [NETWORK ] $sats sats — was $prev sats (diff ${sats - prev})")
|
||||||
|
}
|
||||||
|
_balanceState.value = BalanceState.Success(
|
||||||
|
sats = sats,
|
||||||
|
isRefreshing = false,
|
||||||
|
lastUpdated = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
persistBalance(sats)
|
||||||
|
_balanceRefreshed.tryEmit(sats) // notify FiatViewModel
|
||||||
|
}
|
||||||
|
.onFailure { e ->
|
||||||
|
Log.w(TAG, "BALANCE [NET ERROR ] ${e.message}")
|
||||||
|
val staleSats = (_balanceState.value as? BalanceState.Success)?.sats
|
||||||
|
?: balancePrefs.savedBalanceSats.takeIf { it >= 0 }
|
||||||
|
if (staleSats != null) {
|
||||||
|
Log.w(TAG, "BALANCE [NET ERROR ] Falling back to stale value: $staleSats sats")
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "BALANCE [NET ERROR ] No stale value available — showing error state")
|
||||||
|
}
|
||||||
|
_balanceState.value = BalanceState.Error(
|
||||||
|
message = e.message ?: "Failed to load balance",
|
||||||
|
staleSats = staleSats
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun persistBalance(sats: Long) {
|
||||||
|
balancePrefs.persistBalance(sats)
|
||||||
|
Log.d(TAG, "BALANCE [PERSIST ] $sats sats written to SharedPreferences")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observePaymentEvents() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
WalletNotificationService.paymentEvents.collect { event ->
|
||||||
|
Log.d(TAG, "BALANCE [WS EVENT ] ${if (event.isOutgoing) "SENT" else "RECEIVED"} ${event.amountSats} sats")
|
||||||
|
|
||||||
|
val current = _balanceState.value
|
||||||
|
if (current is BalanceState.Success) {
|
||||||
|
val delta = if (event.isOutgoing) -event.amountSats else event.amountSats
|
||||||
|
val newSats = (current.sats + delta).coerceAtLeast(0L)
|
||||||
|
Log.d(TAG, "BALANCE [WS DELTA ] ${current.sats} ${if (delta >= 0) "+" else ""}$delta = $newSats sats (optimistic)")
|
||||||
|
_balanceState.value = current.copy(sats = newSats, isRefreshing = true)
|
||||||
|
persistBalance(newSats)
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "BALANCE [WS EVENT ] Received event but balance state is ${current::class.simpleName} — skipping delta")
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshBalance()
|
||||||
|
|
||||||
|
if (!event.isOutgoing) {
|
||||||
|
_incomingPayment.tryEmit(
|
||||||
|
IncomingPaymentEvent(
|
||||||
|
amountSats = event.amountSats,
|
||||||
|
memo = event.memo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyFiat(fiatAmount: Double?, fiatCurrency: String?) {
|
||||||
|
val current = _balanceState.value as? BalanceState.Success ?: return
|
||||||
|
_balanceState.value = current.copy(fiatAmount = fiatAmount, fiatCurrency = fiatCurrency)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Thin event type so ReceiveViewModel doesn't depend on the WS model directly. */
|
||||||
|
data class IncomingPaymentEvent(
|
||||||
|
val amountSats: Long,
|
||||||
|
val memo : String?
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user