feat: migrate FiatRateCache to DataStore
This commit is contained in:
@@ -58,6 +58,7 @@ dependencies {
|
|||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
implementation(libs.retrofit2.retrofit)
|
implementation(libs.retrofit2.retrofit)
|
||||||
implementation(libs.converter.gson)
|
implementation(libs.converter.gson)
|
||||||
@@ -71,6 +72,9 @@ dependencies {
|
|||||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
|
|
||||||
|
// DataStore
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
|
||||||
// Room
|
// Room
|
||||||
implementation(libs.room.runtime)
|
implementation(libs.room.runtime)
|
||||||
implementation(libs.room.ktx)
|
implementation(libs.room.ktx)
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
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.floatPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.longPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
private const val TAG = "FiatDataStore"
|
||||||
|
private const val DATASTORE_NAME = "fiat_prefs"
|
||||||
|
|
||||||
|
// Top-level extension — one instance per process
|
||||||
|
private val Context.fiatDataStore: DataStore<Preferences> by preferencesDataStore(
|
||||||
|
name = DATASTORE_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
class FiatDataStore(context: Context) {
|
||||||
|
|
||||||
|
private val dataStore = context.applicationContext.fiatDataStore
|
||||||
|
private val gson = Gson()
|
||||||
|
|
||||||
|
// ── Key factories ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun rateKey(currency: String) = floatPreferencesKey("fiat_rate_$currency")
|
||||||
|
private fun rateFetchedAtKey(currency: String) = longPreferencesKey("fiat_rate_fetched_at_$currency")
|
||||||
|
private val currenciesKey = stringPreferencesKey("fiat_currencies_json")
|
||||||
|
|
||||||
|
// ── Rate ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last persisted rate for [currency], or null if none exists.
|
||||||
|
* Used as a cold-start fallback while a fresh network fetch is in flight.
|
||||||
|
*/
|
||||||
|
suspend fun getPersistedRate(currency: String): Double? {
|
||||||
|
val prefs = dataStore.data
|
||||||
|
.catch { e ->
|
||||||
|
if (e is IOException) {
|
||||||
|
Log.w(TAG, "FIAT [RATE STALE ] read error — ${e.message}")
|
||||||
|
emit(emptyPreferences())
|
||||||
|
} else throw e
|
||||||
|
}
|
||||||
|
.first()
|
||||||
|
val value = prefs[rateKey(currency)]?.toDouble()
|
||||||
|
if (value != null && value > 0.0) {
|
||||||
|
Log.d(TAG, "FIAT [RATE STALE ] $currency — using persisted rate $value")
|
||||||
|
}
|
||||||
|
return value?.takeIf { it > 0.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a freshly fetched [rate] for [currency].
|
||||||
|
* Always called alongside the in-memory update in [FiatRateCache.putRate].
|
||||||
|
*/
|
||||||
|
suspend fun persistRate(currency: String, rate: Double) {
|
||||||
|
runCatching {
|
||||||
|
dataStore.edit { prefs ->
|
||||||
|
prefs[rateKey(currency)] = rate.toFloat()
|
||||||
|
prefs[rateFetchedAtKey(currency)] = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}.onFailure { e ->
|
||||||
|
Log.w(TAG, "FIAT [RATE FRESH ] persist failed for $currency — ${e.message}")
|
||||||
|
}
|
||||||
|
Log.d(TAG, "FIAT [RATE FRESH ] $currency = $rate — persisted to DataStore")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Currency list ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the persisted currency list, or null if none has been stored yet.
|
||||||
|
*/
|
||||||
|
suspend fun getPersistedCurrencies(): List<String>? {
|
||||||
|
val prefs = dataStore.data
|
||||||
|
.catch { e ->
|
||||||
|
if (e is IOException) {
|
||||||
|
Log.w(TAG, "FIAT [CURRENCIES] read error — ${e.message}")
|
||||||
|
emit(emptyPreferences())
|
||||||
|
} else throw e
|
||||||
|
}
|
||||||
|
.first()
|
||||||
|
val json = prefs[currenciesKey] ?: return null
|
||||||
|
return runCatching {
|
||||||
|
val type = object : TypeToken<List<String>>() {}.type
|
||||||
|
val list: List<String> = gson.fromJson(json, type)
|
||||||
|
Log.d(TAG, "FIAT [CURRENCIES] DataStore hit (${list.size} currencies)")
|
||||||
|
list
|
||||||
|
}.getOrElse { e ->
|
||||||
|
Log.w(TAG, "FIAT [CURRENCIES] parse failed — ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists [currencies] to DataStore.
|
||||||
|
*/
|
||||||
|
suspend fun persistCurrencies(currencies: List<String>) {
|
||||||
|
if (currencies.isEmpty()) return
|
||||||
|
runCatching {
|
||||||
|
dataStore.edit { prefs ->
|
||||||
|
prefs[currenciesKey] = gson.toJson(currencies)
|
||||||
|
}
|
||||||
|
}.onFailure { e ->
|
||||||
|
Log.w(TAG, "FIAT [CURRENCIES] persist failed — ${e.message}")
|
||||||
|
}
|
||||||
|
Log.d(TAG, "FIAT [CURRENCIES] persisted ${currencies.size} currencies to DataStore")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,19 @@
|
|||||||
package com.bitcointxoko.gudariwallet.data
|
package com.bitcointxoko.gudariwallet.data
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.edit
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
|
|
||||||
private const val TAG = "FiatRateCache"
|
private const val TAG = "FiatRateCache"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Two-tier (in-memory + SharedPreferences) cache for fiat exchange rates
|
* Two-tier (in-memory + DataStore) cache for fiat exchange rates
|
||||||
* and the supported-currency list.
|
* and the supported-currency list.
|
||||||
*
|
*
|
||||||
* Receives the already-open [prefs] instance from [BalancePrefsStore] so that
|
* Disk persistence is delegated to [FiatDataStore]; this class owns
|
||||||
* all wallet prefs stay in a single file.
|
* only the in-memory (L1) layer and the cache-coordination logic.
|
||||||
*/
|
*/
|
||||||
class FiatRateCache(store: BalancePrefsStore) {
|
class FiatRateCache(private val fiatDataStore: FiatDataStore) {
|
||||||
private val prefs = store.prefs
|
|
||||||
|
|
||||||
// ── Rate ─────────────────────────────────────────────────────────────────
|
// ── Rate ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private var cachedRate : Double? = null
|
private var cachedRate : Double? = null
|
||||||
private var rateLastFetchedAt : Long = 0L
|
private var rateLastFetchedAt : Long = 0L
|
||||||
@@ -26,9 +21,10 @@ class FiatRateCache(store: BalancePrefsStore) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a fresh in-memory rate for [currency] if it is younger than
|
* Returns a fresh in-memory rate for [currency] if it is younger than
|
||||||
* [ttlMs], then falls back to the last persisted float, then returns null.
|
* [ttlMs], then falls back to the last persisted value via DataStore,
|
||||||
|
* then returns null.
|
||||||
*/
|
*/
|
||||||
fun getRate(currency: String, ttlMs: Long): Double? {
|
suspend fun getRate(currency: String, ttlMs: Long): Double? {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val age = now - rateLastFetchedAt
|
val age = now - rateLastFetchedAt
|
||||||
|
|
||||||
@@ -37,9 +33,9 @@ class FiatRateCache(store: BalancePrefsStore) {
|
|||||||
return cachedRate
|
return cachedRate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cold-start fallback: last persisted rate
|
// Cold-start fallback: last persisted rate from DataStore
|
||||||
val persisted = prefs.getFloat("fiat_rate_$currency", 0f).toDouble()
|
val persisted = fiatDataStore.getPersistedRate(currency)
|
||||||
if (persisted > 0.0) {
|
if (persisted != null) {
|
||||||
Log.d(TAG, "FIAT [RATE STALE ] $currency — using persisted rate $persisted")
|
Log.d(TAG, "FIAT [RATE STALE ] $currency — using persisted rate $persisted")
|
||||||
return persisted
|
return persisted
|
||||||
}
|
}
|
||||||
@@ -50,15 +46,12 @@ class FiatRateCache(store: BalancePrefsStore) {
|
|||||||
/**
|
/**
|
||||||
* Stores a freshly fetched [rate] for [currency] in memory and on disk.
|
* Stores a freshly fetched [rate] for [currency] in memory and on disk.
|
||||||
*/
|
*/
|
||||||
fun putRate(currency: String, rate: Double) {
|
suspend fun putRate(currency: String, rate: Double) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
cachedRate = rate
|
cachedRate = rate
|
||||||
rateLastFetchedAt = now
|
rateLastFetchedAt = now
|
||||||
cachedRateCurrency = currency
|
cachedRateCurrency = currency
|
||||||
prefs.edit {
|
fiatDataStore.persistRate(currency, rate)
|
||||||
putFloat("fiat_rate_$currency", rate.toFloat())
|
|
||||||
putLong("fiat_rate_fetched_at_$currency", now)
|
|
||||||
}
|
|
||||||
Log.d(TAG, "FIAT [RATE FRESH ] 1 $currency = $rate sats — cached + persisted")
|
Log.d(TAG, "FIAT [RATE FRESH ] 1 $currency = $rate sats — cached + persisted")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,38 +65,32 @@ class FiatRateCache(store: BalancePrefsStore) {
|
|||||||
// ── Currency list ─────────────────────────────────────────────────────────
|
// ── Currency list ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private var cachedCurrencies: List<String>? = null
|
private var cachedCurrencies: List<String>? = null
|
||||||
private val gson = Gson()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cached currency list (memory → disk → null).
|
* Returns the cached currency list (memory → DataStore → null).
|
||||||
* Callers are responsible for fetching from the network on a null result.
|
* Callers are responsible for fetching from the network on a null result.
|
||||||
*/
|
*/
|
||||||
fun getCurrencies(): List<String>? {
|
suspend fun getCurrencies(): List<String>? {
|
||||||
cachedCurrencies?.let {
|
cachedCurrencies?.let {
|
||||||
Log.d(TAG, "FIAT [CURRENCIES] in-memory hit (${it.size} currencies)")
|
Log.d(TAG, "FIAT [CURRENCIES] in-memory hit (${it.size} currencies)")
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
val json = prefs.getString("fiat_currencies_json", null) ?: return null
|
val persisted = fiatDataStore.getPersistedCurrencies()
|
||||||
return runCatching {
|
if (persisted != null) {
|
||||||
val type = object : TypeToken<List<String>>() {}.type
|
cachedCurrencies = persisted
|
||||||
val list: List<String> = gson.fromJson(json, type)
|
|
||||||
Log.d(TAG, "FIAT [CURRENCIES] prefs hit (${list.size} currencies)")
|
|
||||||
list.also { cachedCurrencies = it }
|
|
||||||
}.getOrElse { e ->
|
|
||||||
Log.w(TAG, "FIAT [CURRENCIES] prefs parse failed — ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
return persisted
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stores [currencies] in memory and on disk. */
|
/** Stores [currencies] in memory and on disk. */
|
||||||
fun putCurrencies(currencies: List<String>) {
|
suspend fun putCurrencies(currencies: List<String>) {
|
||||||
if (currencies.isEmpty()) return
|
if (currencies.isEmpty()) return
|
||||||
cachedCurrencies = currencies
|
cachedCurrencies = currencies
|
||||||
prefs.edit { putString("fiat_currencies_json", gson.toJson(currencies)) }
|
fiatDataStore.persistCurrencies(currencies)
|
||||||
Log.d(TAG, "FIAT [CURRENCIES] cached ${currencies.size} currencies")
|
Log.d(TAG, "FIAT [CURRENCIES] cached ${currencies.size} currencies")
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentRate: Double?
|
val currentRate: Double?
|
||||||
get() = cachedRate.takeIf { cachedRateCurrency.isNotBlank() }
|
get() = cachedRate.takeIf { cachedRateCurrency.isNotBlank() }
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,9 @@ import com.bitcointxoko.gudariwallet.api.LNbitsClient
|
|||||||
import com.bitcointxoko.gudariwallet.api.NodeAliasClient
|
import com.bitcointxoko.gudariwallet.api.NodeAliasClient
|
||||||
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
||||||
import com.bitcointxoko.gudariwallet.data.LNbitsNodeAliasRepository
|
import com.bitcointxoko.gudariwallet.data.LNbitsNodeAliasRepository
|
||||||
|
import com.bitcointxoko.gudariwallet.data.FiatDataStore
|
||||||
import com.bitcointxoko.gudariwallet.data.LNbitsWalletRepository
|
import com.bitcointxoko.gudariwallet.data.LNbitsWalletRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
|
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
|
|
||||||
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
|
||||||
@@ -30,10 +30,11 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
|||||||
val paymentCache = PaymentCacheRepository(app)
|
val paymentCache = PaymentCacheRepository(app)
|
||||||
|
|
||||||
val aliasRepo = LNbitsNodeAliasRepository(nodeAliasSvc)
|
val aliasRepo = LNbitsNodeAliasRepository(nodeAliasSvc)
|
||||||
|
val fiatDataStore = FiatDataStore(app)
|
||||||
val repo = LNbitsWalletRepository(api = api, secrets = secrets)
|
val repo = LNbitsWalletRepository(api = api, secrets = secrets)
|
||||||
val balancePrefs = BalancePrefsStore(app)
|
val balancePrefs = BalancePrefsStore(app)
|
||||||
val balanceVm = BalanceViewModel(repo, balancePrefs)
|
val balanceVm = BalanceViewModel(repo, balancePrefs)
|
||||||
val fiatVm = FiatViewModel(repo, balancePrefs, balanceVm)
|
val fiatVm = FiatViewModel(repo, balancePrefs, balanceVm, fiatDataStore)
|
||||||
val receiveVm = ReceiveViewModel(repo)
|
val receiveVm = ReceiveViewModel(repo)
|
||||||
val sendVm = SendViewModel(
|
val sendVm = SendViewModel(
|
||||||
repo = repo,
|
repo = repo,
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import android.util.Log
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
||||||
|
import com.bitcointxoko.gudariwallet.data.FiatDataStore
|
||||||
import com.bitcointxoko.gudariwallet.data.FiatRateCache
|
import com.bitcointxoko.gudariwallet.data.FiatRateCache
|
||||||
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||||
import com.bitcointxoko.gudariwallet.ui.BalanceState
|
import com.bitcointxoko.gudariwallet.ui.BalanceState
|
||||||
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
||||||
|
import com.bitcointxoko.gudariwallet.util.WalletConstants.RATE_TTL_MS
|
||||||
import com.bitcointxoko.gudariwallet.util.satsToFiat
|
import com.bitcointxoko.gudariwallet.util.satsToFiat
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -17,15 +19,15 @@ import kotlinx.coroutines.flow.stateIn
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private const val TAG = "FiatViewModel"
|
private const val TAG = "FiatViewModel"
|
||||||
private const val RATE_TTL_MS = 5 * 60 * 1_000L // 5 minutes
|
|
||||||
|
|
||||||
class FiatViewModel(
|
class FiatViewModel(
|
||||||
private val repo : WalletRepository,
|
private val repo : WalletRepository,
|
||||||
private val balancePrefs: BalancePrefsStore,
|
private val balancePrefs: BalancePrefsStore,
|
||||||
private val balanceVm : BalanceViewModel,
|
private val balanceVm : BalanceViewModel,
|
||||||
|
fiatDataStore : FiatDataStore,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val fiatCache = FiatRateCache(balancePrefs)
|
private val fiatCache = FiatRateCache(fiatDataStore)
|
||||||
|
|
||||||
val fiatRate: Double?
|
val fiatRate: Double?
|
||||||
get() = fiatCache.currentRate
|
get() = fiatCache.currentRate
|
||||||
@@ -33,7 +35,7 @@ class FiatViewModel(
|
|||||||
val fiatCurrency: String?
|
val fiatCurrency: String?
|
||||||
get() = _selectedCurrency.value
|
get() = _selectedCurrency.value
|
||||||
|
|
||||||
// ── Balance hidden ───────────────────────────────────────────────────────
|
// ── Balance hidden ────────────────────────────────────────────────────────
|
||||||
private val _balanceHidden = MutableStateFlow(balancePrefs.isBalanceHidden)
|
private val _balanceHidden = MutableStateFlow(balancePrefs.isBalanceHidden)
|
||||||
val balanceHidden: StateFlow<Boolean> = _balanceHidden
|
val balanceHidden: StateFlow<Boolean> = _balanceHidden
|
||||||
|
|
||||||
@@ -43,8 +45,9 @@ class FiatViewModel(
|
|||||||
balancePrefs.setBalanceHidden(next)
|
balancePrefs.setBalanceHidden(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Selected currency ────────────────────────────────────────────────────
|
// ── Selected currency ─────────────────────────────────────────────────────
|
||||||
private val _selectedCurrency = MutableStateFlow(balancePrefs.selectedCurrency)
|
private val _selectedCurrency =
|
||||||
|
MutableStateFlow(balancePrefs.selectedCurrency)
|
||||||
val selectedCurrency: StateFlow<String?> = _selectedCurrency
|
val selectedCurrency: StateFlow<String?> = _selectedCurrency
|
||||||
|
|
||||||
fun setSelectedCurrency(currency: String?) {
|
fun setSelectedCurrency(currency: String?) {
|
||||||
@@ -60,8 +63,7 @@ class FiatViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sats-per-fiat-unit derived flow ──────────────────────────────────────
|
// ── Sats-per-fiat-unit derived flow ───────────────────────────────────────
|
||||||
// Used by UnitWheelPicker to convert between sats and fiat amounts.
|
|
||||||
val fiatSatsPerUnit: StateFlow<Double?> = balanceVm.balanceState
|
val fiatSatsPerUnit: StateFlow<Double?> = balanceVm.balanceState
|
||||||
.mapNotNull { state ->
|
.mapNotNull { state ->
|
||||||
(state as? BalanceState.Success)?.let { s ->
|
(state as? BalanceState.Success)?.let { s ->
|
||||||
@@ -76,13 +78,12 @@ class FiatViewModel(
|
|||||||
initialValue = null
|
initialValue = null
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Called by BalanceViewModel after every successful balance refresh ────
|
// ── Called by BalanceViewModel after every successful balance refresh ─────
|
||||||
// Keeps fiat display in sync whenever the balance changes.
|
|
||||||
fun onBalanceRefreshed() {
|
fun onBalanceRefreshed() {
|
||||||
viewModelScope.launch { fetchAndApplyFiatRate() }
|
viewModelScope.launch { fetchAndApplyFiatRate() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Rate fetch + apply ───────────────────────────────────────────────────
|
// ── Rate fetch + apply ────────────────────────────────────────────────────
|
||||||
private suspend fun fetchAndApplyFiatRate() {
|
private suspend fun fetchAndApplyFiatRate() {
|
||||||
val currency = _selectedCurrency.value ?: return
|
val currency = _selectedCurrency.value ?: return
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ class FiatViewModel(
|
|||||||
balanceVm.applyFiat(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 }
|
||||||
return fetchAndCacheCurrencies()
|
return fetchAndCacheCurrencies()
|
||||||
|
|||||||
@@ -42,4 +42,7 @@ object WalletConstants {
|
|||||||
const val BALANCE_PREFS_KEY = "last_balance_sats"
|
const val BALANCE_PREFS_KEY = "last_balance_sats"
|
||||||
const val BALANCE_PREFS_SAVED_AT = "last_balance_saved_at"
|
const val BALANCE_PREFS_SAVED_AT = "last_balance_saved_at"
|
||||||
const val BALANCE_CACHE_TTL_MS = 3_600_000L // 1 hour
|
const val BALANCE_CACHE_TTL_MS = 3_600_000L // 1 hour
|
||||||
|
|
||||||
|
// ── Fiat cache ─────────────────────────────────────────────────────────
|
||||||
|
const val RATE_TTL_MS = 5 * 60 * 1_000L // 5 minutes
|
||||||
}
|
}
|
||||||
@@ -21,8 +21,7 @@ navigationCompose = "2.8.9"
|
|||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
ksp = "2.2.10-2.0.2"
|
ksp = "2.2.10-2.0.2"
|
||||||
room = "2.7.1"
|
room = "2.7.1"
|
||||||
|
datastore = "1.2.1"
|
||||||
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
|
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
|
||||||
@@ -57,7 +56,7 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
|
|||||||
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||||
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||||
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||||
|
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user