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")
}
}
// ── 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 androidx.fragment.app.FragmentActivity
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
import com.bitcointxoko.gudariwallet.data.LightningAddressStore
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
import com.bitcointxoko.gudariwallet.ui.clipboard.ClipboardViewModel
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
private const val TAG = "WalletViewModel"
@@ -26,12 +29,12 @@ class WalletViewModel(
val aliasRepo : NodeAliasRepository,
val paymentCache: PaymentCacheRepository,
private val balancePrefs: BalancePrefsStore,
private val lnAddressStore : LightningAddressStore,
val balanceVm : BalanceViewModel,
val fiatVm : FiatViewModel,
val receiveVm : ReceiveViewModel,
val sendVm : SendViewModel,
val clipboardVm : ClipboardViewModel,
val lightningAddress: MutableStateFlow<String?>,
) : ViewModel() {
val balanceState: StateFlow<BalanceState> get() = balanceVm.balanceState
val balanceHidden: StateFlow<Boolean> get() = fiatVm.balanceHidden
@@ -63,6 +66,12 @@ class WalletViewModel(
val clipboardOfferState: StateFlow<ClipboardOfferState> get() = clipboardVm.clipboardOfferState
fun checkClipboard(text: String?) = clipboardVm.checkClipboard(text)
fun dismissClipboardOffer() = clipboardVm.dismissClipboardOffer()
val lightningAddress: StateFlow<String?> = lnAddressStore.lightningAddressFlow
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = null
)
init {
observePaymentEvents()
@@ -80,22 +89,26 @@ class WalletViewModel(
}
private fun fetchLightningAddress() {
// If we already have a persisted address, show it immediately and
// still refresh in the background (address could have changed).
// lightningAddressFlow already emits the cached DataStore value immediately
// via SharingStarted.Eagerly — this coroutine only does the network refresh.
viewModelScope.launch {
if (!lnAddressStore.isCacheStale()) {
Log.d(TAG, "LNADDR [TTL ] cache is fresh — skipping network fetch")
return@launch
}
runCatching { repo.getLightningAddress() }
.onSuccess { address ->
if (address != null) {
Log.d(TAG, "LNADDR [FETCH OK ] $address")
lightningAddress.value = address
balancePrefs.setLightningAddress(address)
Log.d(TAG, "LNADDR [FETCH OK ] fetched from API: \"$address\" — updating cache")
lnAddressStore.setLightningAddress(address)
// StateFlow updates automatically via the DataStore flow
} 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 ->
Log.w(TAG, "LNADDR [FETCH ERR] ${e.message}keeping cached value")
// Keep whatever was loaded from prefs; don't clear it on network error
Log.w(TAG, "LNADDR [FETCH ERR] API call failed (${e.message})falling back to cached value")
// 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.NodeAliasServiceRepository
import com.bitcointxoko.gudariwallet.data.LNbitsWalletRepository
import com.bitcointxoko.gudariwallet.data.LightningAddressStore
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
@@ -32,6 +33,7 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
val aliasRepo = NodeAliasServiceRepository(nodeAliasSvc)
val repo = LNbitsWalletRepository(api = api, secrets = secrets)
val balancePrefs = BalancePrefsStore(app)
val lnAddressStore = LightningAddressStore(app)
val balanceVm = BalanceViewModel(repo, balancePrefs)
val fiatVm = FiatViewModel(repo, balancePrefs, balanceVm, fiatDataStore)
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(
ownInvoice = { (receiveVm.receiveState.value as? ReceiveState.InvoiceReady)?.bolt11 },
ownAddress = { lightningAddress.value }
ownAddress = { lnAddressStore.currentAddress }
)
balanceVm.onBalanceRefreshed = { fiatVm.onBalanceRefreshed() }
@@ -65,7 +67,7 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
receiveVm = receiveVm,
sendVm = sendVm,
clipboardVm = clipboardVm,
lightningAddress = lightningAddress
lnAddressStore = lnAddressStore,
) as T
}
}
@@ -45,4 +45,6 @@ object WalletConstants {
// ── Fiat cache ─────────────────────────────────────────────────────────
const val RATE_TTL_MS = 5 * 60 * 1_000L // 5 minutes
const val LNADDRESS_TTL_MS = 21 * 24 * 60 * 60 * 1_000L // 21 days
}