From fabf18760da85d47e53f0145ed13a0e903d93a1a Mon Sep 17 00:00:00 2001 From: rasputin Date: Tue, 2 Jun 2026 01:57:29 +0200 Subject: [PATCH] feat: migrate LightningAddressStore to DataStore --- .../gudariwallet/data/BalancePrefsStore.kt | 9 --- .../data/LightningAddressStore.kt | 72 +++++++++++++++++++ .../gudariwallet/ui/WalletViewModel.kt | 31 +++++--- .../gudariwallet/ui/WalletViewModelFactory.kt | 8 ++- .../gudariwallet/util/WalletConstants.kt | 2 + 5 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/bitcointxoko/gudariwallet/data/LightningAddressStore.kt diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/BalancePrefsStore.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/BalancePrefsStore.kt index e7c82a7..c5e8675 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/BalancePrefsStore.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/BalancePrefsStore.kt @@ -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) } - } } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/LightningAddressStore.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/LightningAddressStore.kt new file mode 100644 index 0000000..c5bb296 --- /dev/null +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/LightningAddressStore.kt @@ -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 + 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 = 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 + } + } +} + diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt index 195e7b3..6d8c2f4 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt @@ -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, ) : ViewModel() { val balanceState: StateFlow get() = balanceVm.balanceState val balanceHidden: StateFlow get() = fiatVm.balanceHidden @@ -63,6 +66,12 @@ class WalletViewModel( val clipboardOfferState: StateFlow get() = clipboardVm.clipboardOfferState fun checkClipboard(text: String?) = clipboardVm.checkClipboard(text) fun dismissClipboardOffer() = clipboardVm.dismissClipboardOffer() + val lightningAddress: StateFlow = 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 } } } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModelFactory.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModelFactory.kt index 2070e5e..f141686 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModelFactory.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModelFactory.kt @@ -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(balancePrefs.lightningAddress) +// val lightningAddress = MutableStateFlow(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 } } \ No newline at end of file diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/util/WalletConstants.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/util/WalletConstants.kt index e966902..7c47235 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/util/WalletConstants.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/util/WalletConstants.kt @@ -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 } \ No newline at end of file