feat: migrate LightningAddressStore to DataStore

This commit is contained in:
2026-06-02 01:57:29 +02:00
parent 54e06506ae
commit fabf18760d
5 changed files with 101 additions and 21 deletions
@@ -55,13 +55,4 @@ class BalancePrefsStore(context: Context) {
else remove("fiat_currency") else remove("fiat_currency")
} }
} }
// ── Lightning address ─────────────────────────────────────────────────────
val lightningAddress: String?
get() = prefs.getString("lightning_address", null)
fun setLightningAddress(address: String) {
prefs.edit { putString("lightning_address", address) }
}
} }
@@ -0,0 +1,72 @@
package com.bitcointxoko.gudariwallet.data
import android.content.Context
import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.bitcointxoko.gudariwallet.util.WalletConstants.LNADDRESS_TTL_MS
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import java.io.IOException
private val Context.lightningAddressDataStore: DataStore<Preferences>
by preferencesDataStore(name = "lightning_address")
class LightningAddressStore(private val context: Context) {
companion object {
private val KEY_ADDRESS = stringPreferencesKey("lightning_address")
private val KEY_FETCHED_AT = longPreferencesKey("lightning_address_fetched_at")
}
// In-memory snapshot — safe for ClipboardViewModel's synchronous lambda
@Volatile private var _currentAddress: String? = null
val currentAddress: String? get() = _currentAddress
val lightningAddressFlow: Flow<String?> = context.lightningAddressDataStore.data
.catch { e ->
if (e is IOException) emit(emptyPreferences())
else throw e
}
.map { prefs ->
prefs[KEY_ADDRESS].also { address ->
_currentAddress = address
Log.d("LightningAddressStore", "LNADDR [CACHE ] read from cache: " +
if (address != null) "\"$address\"" else "(empty — nothing cached yet)") }
}
suspend fun isCacheStale(): Boolean {
return context.lightningAddressDataStore.data
.catch { e ->
if (e is IOException) emit(emptyPreferences())
else throw e
}
.map { prefs ->
val address = prefs[KEY_ADDRESS]
val fetchedAt = prefs[KEY_FETCHED_AT] ?: 0L
val age = System.currentTimeMillis() - fetchedAt
val isStale = address == null || age > LNADDRESS_TTL_MS
Log.d("LightningAddressStore", "LNADDR [TTL ] cached=${address != null} " +
"age=${age / 1000}s ttl=${LNADDRESS_TTL_MS / 1000}s " +
if (isStale) "→ cache stale, will fetch from API"
else "→ cache fresh, skipping API call")
isStale
}
.first()
}
suspend fun setLightningAddress(address: String) {
context.lightningAddressDataStore.edit { prefs ->
prefs[KEY_ADDRESS] = address
prefs[KEY_FETCHED_AT] = System.currentTimeMillis() // ← add
}
}
}
@@ -13,11 +13,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
import com.bitcointxoko.gudariwallet.data.LightningAddressStore
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
import com.bitcointxoko.gudariwallet.ui.clipboard.ClipboardViewModel import com.bitcointxoko.gudariwallet.ui.clipboard.ClipboardViewModel
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
private const val TAG = "WalletViewModel" private const val TAG = "WalletViewModel"
@@ -26,12 +29,12 @@ class WalletViewModel(
val aliasRepo : NodeAliasRepository, val aliasRepo : NodeAliasRepository,
val paymentCache: PaymentCacheRepository, val paymentCache: PaymentCacheRepository,
private val balancePrefs: BalancePrefsStore, private val balancePrefs: BalancePrefsStore,
private val lnAddressStore : LightningAddressStore,
val balanceVm : BalanceViewModel, val balanceVm : BalanceViewModel,
val fiatVm : FiatViewModel, val fiatVm : FiatViewModel,
val receiveVm : ReceiveViewModel, val receiveVm : ReceiveViewModel,
val sendVm : SendViewModel, val sendVm : SendViewModel,
val clipboardVm : ClipboardViewModel, val clipboardVm : ClipboardViewModel,
val lightningAddress: MutableStateFlow<String?>,
) : ViewModel() { ) : ViewModel() {
val balanceState: StateFlow<BalanceState> get() = balanceVm.balanceState val balanceState: StateFlow<BalanceState> get() = balanceVm.balanceState
val balanceHidden: StateFlow<Boolean> get() = fiatVm.balanceHidden val balanceHidden: StateFlow<Boolean> get() = fiatVm.balanceHidden
@@ -63,6 +66,12 @@ class WalletViewModel(
val clipboardOfferState: StateFlow<ClipboardOfferState> get() = clipboardVm.clipboardOfferState val clipboardOfferState: StateFlow<ClipboardOfferState> get() = clipboardVm.clipboardOfferState
fun checkClipboard(text: String?) = clipboardVm.checkClipboard(text) fun checkClipboard(text: String?) = clipboardVm.checkClipboard(text)
fun dismissClipboardOffer() = clipboardVm.dismissClipboardOffer() fun dismissClipboardOffer() = clipboardVm.dismissClipboardOffer()
val lightningAddress: StateFlow<String?> = lnAddressStore.lightningAddressFlow
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = null
)
init { init {
observePaymentEvents() observePaymentEvents()
@@ -80,22 +89,26 @@ class WalletViewModel(
} }
private fun fetchLightningAddress() { private fun fetchLightningAddress() {
// If we already have a persisted address, show it immediately and // lightningAddressFlow already emits the cached DataStore value immediately
// still refresh in the background (address could have changed). // via SharingStarted.Eagerly — this coroutine only does the network refresh.
viewModelScope.launch { viewModelScope.launch {
if (!lnAddressStore.isCacheStale()) {
Log.d(TAG, "LNADDR [TTL ] cache is fresh — skipping network fetch")
return@launch
}
runCatching { repo.getLightningAddress() } runCatching { repo.getLightningAddress() }
.onSuccess { address -> .onSuccess { address ->
if (address != null) { if (address != null) {
Log.d(TAG, "LNADDR [FETCH OK ] $address") Log.d(TAG, "LNADDR [FETCH OK ] fetched from API: \"$address\" — updating cache")
lightningAddress.value = address lnAddressStore.setLightningAddress(address)
balancePrefs.setLightningAddress(address) // StateFlow updates automatically via the DataStore flow
} else { } else {
Log.d(TAG, "LNADDR [FETCH OK ] no pay link / username found") Log.d(TAG, "LNADDR [FETCH OK ] fetched from API: no pay link / username configured")
} }
} }
.onFailure { e -> .onFailure { e ->
Log.w(TAG, "LNADDR [FETCH ERR] ${e.message}keeping cached value") Log.w(TAG, "LNADDR [FETCH ERR] API call failed (${e.message})falling back to cached value")
// Keep whatever was loaded from prefs; don't clear it on network error // Nothing to do — DataStore flow already holds the last good value
} }
} }
} }
@@ -9,6 +9,7 @@ import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
import com.bitcointxoko.gudariwallet.data.FiatDataStore import com.bitcointxoko.gudariwallet.data.FiatDataStore
import com.bitcointxoko.gudariwallet.data.NodeAliasServiceRepository import com.bitcointxoko.gudariwallet.data.NodeAliasServiceRepository
import com.bitcointxoko.gudariwallet.data.LNbitsWalletRepository import com.bitcointxoko.gudariwallet.data.LNbitsWalletRepository
import com.bitcointxoko.gudariwallet.data.LightningAddressStore
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository 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
@@ -32,6 +33,7 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
val aliasRepo = NodeAliasServiceRepository(nodeAliasSvc) val aliasRepo = NodeAliasServiceRepository(nodeAliasSvc)
val repo = LNbitsWalletRepository(api = api, secrets = secrets) val repo = LNbitsWalletRepository(api = api, secrets = secrets)
val balancePrefs = BalancePrefsStore(app) val balancePrefs = BalancePrefsStore(app)
val lnAddressStore = LightningAddressStore(app)
val balanceVm = BalanceViewModel(repo, balancePrefs) val balanceVm = BalanceViewModel(repo, balancePrefs)
val fiatVm = FiatViewModel(repo, balancePrefs, balanceVm, fiatDataStore) val fiatVm = FiatViewModel(repo, balancePrefs, balanceVm, fiatDataStore)
val receiveVm = ReceiveViewModel(repo) val receiveVm = ReceiveViewModel(repo)
@@ -45,11 +47,11 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
} }
) )
val lightningAddress = MutableStateFlow<String?>(balancePrefs.lightningAddress) // val lightningAddress = MutableStateFlow<String?>(balancePrefs.lightningAddress)
val clipboardVm = ClipboardViewModel( val clipboardVm = ClipboardViewModel(
ownInvoice = { (receiveVm.receiveState.value as? ReceiveState.InvoiceReady)?.bolt11 }, ownInvoice = { (receiveVm.receiveState.value as? ReceiveState.InvoiceReady)?.bolt11 },
ownAddress = { lightningAddress.value } ownAddress = { lnAddressStore.currentAddress }
) )
balanceVm.onBalanceRefreshed = { fiatVm.onBalanceRefreshed() } balanceVm.onBalanceRefreshed = { fiatVm.onBalanceRefreshed() }
@@ -65,7 +67,7 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
receiveVm = receiveVm, receiveVm = receiveVm,
sendVm = sendVm, sendVm = sendVm,
clipboardVm = clipboardVm, clipboardVm = clipboardVm,
lightningAddress = lightningAddress lnAddressStore = lnAddressStore,
) as T ) as T
} }
} }
@@ -45,4 +45,6 @@ object WalletConstants {
// ── Fiat cache ───────────────────────────────────────────────────────── // ── Fiat cache ─────────────────────────────────────────────────────────
const val RATE_TTL_MS = 5 * 60 * 1_000L // 5 minutes const val RATE_TTL_MS = 5 * 60 * 1_000L // 5 minutes
const val LNADDRESS_TTL_MS = 21 * 24 * 60 * 60 * 1_000L // 21 days
} }