Files
gudari/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt
T

211 lines
9.1 KiB
Kotlin

package com.bitcointxoko.gudariwallet.ui
import timber.log.Timber
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
import com.bitcointxoko.gudariwallet.data.WalletRepository
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
import com.bitcointxoko.gudariwallet.data.ContactRepository
import com.bitcointxoko.gudariwallet.data.LightningAddressStore
import com.bitcointxoko.gudariwallet.data.NwcCacheRepository
import com.bitcointxoko.gudariwallet.data.db.ContactEntity
import com.bitcointxoko.gudariwallet.data.db.PaymentAddressType
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.SendEvent
import com.bitcointxoko.gudariwallet.ui.send.SendStrings
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
private const val TAG = "WalletViewModel"
class WalletViewModel(
val repo : WalletRepository,
val aliasRepo : NodeAliasRepository,
val paymentCache: PaymentCacheRepository,
val nwcCache : NwcCacheRepository,
val contactRepo : ContactRepository,
private val balancePrefs: BalancePrefsStore,
private val lnAddressStore : LightningAddressStore,
val balanceVm : BalanceViewModel,
val fiatVm : FiatViewModel,
val receiveVm : ReceiveViewModel,
val sendVm : SendViewModel,
val clipboardVm : ClipboardViewModel
) : ViewModel() {
val balanceState: StateFlow<BalanceState> get() = balanceVm.balanceState
val balanceHidden: StateFlow<Boolean> get() = fiatVm.balanceHidden
val selectedCurrency: StateFlow<String?> get() = fiatVm.selectedCurrency
val fiatSatsPerUnit: StateFlow<Double?> get() = fiatVm.fiatSatsPerUnit
fun toggleBalanceVisibility() = fiatVm.toggleBalanceVisibility()
fun setSelectedCurrency(c: String?) = fiatVm.setSelectedCurrency(c)
suspend fun getSupportedCurrencies() = fiatVm.getSupportedCurrencies()
val fiatRate: Double? get() = fiatVm.fiatRate
val fiatCurrency: String? get() = fiatVm.fiatCurrency
val receiveState: StateFlow<ReceiveState> get() = receiveVm.receiveState
val navigateToReceive: SharedFlow<Unit> get() = receiveVm.navigateToReceive
fun createInvoice(
amountSats : Long,
memo : String,
failedToCreateInvoice : String,
) = receiveVm.createInvoice(
amountSats = amountSats,
memo = memo,
failedToCreateInvoice = failedToCreateInvoice,
)
fun resetReceiveState() = receiveVm.resetReceiveState()
fun executeWithdraw(
k1 : String,
callback : String,
amountSats : Long,
memo : String,
failedToCreateInvoice : String,
withdrawRejected : String,
withdrawFailed : String,
) = receiveVm.executeWithdraw(
k1 = k1,
callback = callback,
amountSats = amountSats,
memo = memo,
failedToCreateInvoice = failedToCreateInvoice,
withdrawRejected = withdrawRejected,
withdrawFailed = withdrawFailed,
)
val sendState: StateFlow<SendState> get() = sendVm.sendState
fun refreshBalance() = balanceVm.refreshBalance()
fun scan(rawInput: String, strings: SendStrings) =
sendVm.scan(rawInput, strings)
fun payBolt11(bolt11: String, strings: SendStrings) =
sendVm.payBolt11(bolt11, strings)
fun payLnurl(
rawRes : LnurlScanResponse,
lnurl : String,
amountMsat: Long,
comment : String?,
strings : SendStrings,
) = sendVm.payLnurl(rawRes, lnurl, amountMsat, comment, strings)
fun fetchLnurlInvoice(
rawRes : LnurlScanResponse,
lnurl : String,
domain : String,
amountMsat: Long,
comment : String?,
strings : SendStrings,
contactId : String?
) = sendVm.fetchLnurlInvoice(rawRes, lnurl, domain, amountMsat, comment, strings, contactId)
fun payLnurlInvoice(state: SendState.LnurlInvoiceReady, strings: SendStrings) =
sendVm.payLnurlInvoice(state, strings)
fun onAuthFailed(message: String, strings: SendStrings) {
sendVm.onAuthFailed(message, strings)
}
fun resetSendState() = sendVm.resetSendState()
val sendEvents: SharedFlow<SendEvent> = sendVm.events
// ── Contact tap-to-pay prefill ────────────────────────────────────────────
private val _pendingSend = MutableStateFlow<String?>(null)
val pendingSend: StateFlow<String?> get() = _pendingSend
/** Called by ContactDetailScreen before navigating to the Send tab. */
fun prefillSend(address: String) { _pendingSend.value = address }
/** Called by SendScreen once it has consumed the prefilled address. */
fun consumePendingSend() { _pendingSend.value = null }
fun handleWithdrawScanned(raw: LnurlScanResponse, lnurl: String) {
receiveVm.handleWithdrawResponse(raw, lnurl)
}
val lightningAddress: StateFlow<String?> = lnAddressStore.lightningAddressFlow
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = null
)
val clipboardOfferState: StateFlow<ClipboardOfferState> get() = clipboardVm.clipboardOfferState
fun checkClipboard(text: String?) = clipboardVm.checkClipboard(text)
fun dismissClipboardOffer() = clipboardVm.dismissClipboardOffer()
val contactForCurrentLnurl: StateFlow<ContactEntity?> =
sendVm.sendState
.map { state ->
val lnurl = (state as? SendState.LnurlReady)?.lnurl
?: return@map null
contactRepo.findByLnAddress(lnurl)
?: contactRepo.findByLnurl(lnurl)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null
)
fun saveContactFromSend(name: String, addressType: PaymentAddressType?, address: String?) {
viewModelScope.launch {
val lnAddress = address?.takeIf { addressType == PaymentAddressType.LIGHTNING_ADDRESS }
val lnUrl = address?.takeIf { addressType == PaymentAddressType.LNURL }
contactRepo.createContact(
localAlias = name,
lnAddress = lnAddress,
lnUrl = lnUrl
)
}
}
init {
observePaymentEvents()
fetchLightningAddress()
viewModelScope.launch { aliasRepo.warmUp() }
}
// ── WebSocket event collection ────────────────────────────────────────────
private fun observePaymentEvents() {
viewModelScope.launch {
balanceVm.incomingPayment.collect { event ->
receiveVm.onIncomingPayment(event)
}
}
}
private fun fetchLightningAddress() {
// lightningAddressFlow already emits the cached DataStore value immediately
// via SharingStarted.Eagerly — this coroutine only does the network refresh.
viewModelScope.launch {
if (!lnAddressStore.isCacheStale()) {
Timber.d("LNADDR [TTL ] cache is fresh — skipping network fetch")
return@launch
}
runCatching { repo.getLightningAddress() }
.onSuccess { address ->
if (address != null) {
Timber.d("LNADDR [FETCH OK ] fetched from API: \"$address\" — updating cache")
lnAddressStore.setLightningAddress(address)
// StateFlow updates automatically via the DataStore flow
} else {
Timber.d("LNADDR [FETCH OK ] fetched from API: no pay link / username configured")
}
}
.onFailure { e ->
Timber.w("LNADDR [FETCH ERR] API call failed (${e.message}) — falling back to cached value")
// Nothing to do — DataStore flow already holds the last good value
}
}
}
}