refactor: WalletViewModel extract ReceiveViewModel

This commit is contained in:
2026-06-01 21:13:04 +02:00
parent bba41033e5
commit e6c24c761b
3 changed files with 170 additions and 117 deletions
@@ -14,7 +14,6 @@ import com.bitcointxoko.gudariwallet.util.SendInputDetector
import com.bitcointxoko.gudariwallet.util.SendInputType
import com.bitcointxoko.gudariwallet.util.WalletConstants
import com.bitcointxoko.gudariwallet.util.parseApiError
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
@@ -25,6 +24,7 @@ import com.bitcointxoko.gudariwallet.api.RouteHintHop
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel
import com.bitcointxoko.gudariwallet.util.BiometricHelper
private const val TAG = "WalletViewModel"
@@ -36,7 +36,8 @@ class WalletViewModel(
val paymentCache: PaymentCacheRepository,
private val balancePrefs: BalancePrefsStore,
val balanceVm : BalanceViewModel,
val fiatVm : FiatViewModel
val fiatVm : FiatViewModel,
val receiveVm : ReceiveViewModel,
) : ViewModel() {
// ── Balance state ─────────────────────────────────────────────────────────
@@ -49,15 +50,17 @@ class WalletViewModel(
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) = receiveVm.createInvoice(amountSats, memo)
fun resetReceiveState() = receiveVm.resetReceiveState()
fun executeWithdraw(k1: String, callback: String, amountSats: Long, memo: String) =
receiveVm.executeWithdraw(k1, callback, amountSats, memo)
// ── Other states ──────────────────────────────────────────────────────────
private val _receiveState = MutableStateFlow<ReceiveState>(ReceiveState.Idle)
val receiveState: StateFlow<ReceiveState> = _receiveState
private val _sendState = MutableStateFlow<SendState>(SendState.Idle)
val sendState: StateFlow<SendState> = _sendState
private val _navigateToReceive = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val navigateToReceive: SharedFlow<Unit> = _navigateToReceive
private val _lightningAddress = MutableStateFlow<String?>(balancePrefs.lightningAddress)
val lightningAddress: StateFlow<String?> = _lightningAddress
@@ -72,18 +75,11 @@ class WalletViewModel(
private fun observePaymentEvents() {
viewModelScope.launch {
balanceVm.incomingPayment.collect { event ->
val rs = _receiveState.value
if (rs is ReceiveState.InvoiceReady) {
Log.d(TAG, "BALANCE [WS RECEIVE] Marking invoice ${rs.paymentHash.take(8)}… as received")
_receiveState.value = ReceiveState.PaymentReceived(
amountSats = event.amountSats,
memo = event.memo,
checkingId = rs.paymentHash
)
}
receiveVm.onIncomingPayment(event)
}
}
}
private fun fetchLightningAddress() {
// If we already have a persisted address, show it immediately and
// still refresh in the background (address could have changed).
@@ -105,38 +101,6 @@ class WalletViewModel(
}
}
// ── Receive ───────────────────────────────────────────────────────────────
fun createInvoice(amountSats: Long, memo: String) {
viewModelScope.launch {
_receiveState.value = ReceiveState.AwaitingInvoice
runCatching { repo.createInvoice(amountSats, memo) }
.onSuccess { invoice ->
val fallbackExpiry = System.currentTimeMillis() / 1000L + 600L
val expiresAt = invoice.expiry?.let { isoString ->
runCatching {
java.time.LocalDateTime
.parse(isoString, java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.toEpochSecond(java.time.ZoneOffset.UTC)
}.getOrNull()
} ?: fallbackExpiry
_receiveState.value = ReceiveState.InvoiceReady(
bolt11 = invoice.bolt11,
paymentHash = invoice.paymentHash,
amountSats = amountSats,
memo = memo,
expiresAt = expiresAt
)
}
.onFailure { e ->
Log.e(TAG, "RECEIVE [CREATE INVOICE ERROR] ${e.message}")
_receiveState.value = ReceiveState.Error(parseApiError(e, "Failed to create invoice"))
}
}
}
fun resetReceiveState() { _receiveState.value = ReceiveState.Idle }
// ── Send ──────────────────────────────────────────────────────────────────
fun scan(rawInput: String) {
@@ -160,69 +124,6 @@ class WalletViewModel(
}
}
private fun handleWithdrawResponse(raw: LnurlScanResponse, lnurl: String) {
val minSats = (raw.minWithdrawable ?: 0L) / WalletConstants.MSAT_PER_SAT
val maxSats = (raw.maxWithdrawable ?: 0L) / WalletConstants.MSAT_PER_SAT
_receiveState.value = ReceiveState.LnurlWithdrawReady(
k1 = raw.k1 ?: "",
callback = raw.callback ?: "",
defaultDescription = raw.defaultDescription ?: "",
minSats = minSats,
maxSats = maxSats,
domain = LnurlMetadataParser.extractDomain(raw.callback),
lnurl = lnurl
)
_sendState.value = SendState.Idle // clear scanning spinner
_navigateToReceive.tryEmit(Unit)
Log.d(TAG, "LNURL-WITHDRAW [NAV EVENT] min=$minSats max=$maxSats sats — navigating to Receive")
}
fun executeWithdraw(
k1 : String,
callback : String,
amountSats: Long,
memo : String
) {
viewModelScope.launch {
_receiveState.value = ReceiveState.AwaitingInvoice
val invoiceResult = runCatching { repo.createInvoice(amountSats, memo) }
if (invoiceResult.isFailure) {
val e = invoiceResult.exceptionOrNull() ?: Exception("Failed to create invoice")
Log.e(TAG, "LNURL-WITHDRAW [INVOICE ERROR] ${e.message}")
_receiveState.value = ReceiveState.Error(parseApiError(e, "Failed to create invoice"))
return@launch
}
val invoice = invoiceResult.getOrThrow()
Log.d(TAG, "LNURL-WITHDRAW [INVOICE] ${invoice.paymentHash} — submitting to callback")
runCatching { repo.executeWithdraw(callback, k1, invoice.bolt11) }
.onSuccess { response ->
if (response.status == "OK") {
Log.d(TAG, "LNURL-WITHDRAW [CALLBACK OK] waiting for push payment")
_receiveState.value = ReceiveState.InvoiceReady(
bolt11 = invoice.bolt11,
paymentHash = invoice.paymentHash,
amountSats = amountSats,
memo = memo,
expiresAt = System.currentTimeMillis() / 1000L + 600L
)
} else {
val reason = response.reason
?.ifBlank { null }
?: "Withdraw service rejected the invoice"
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK ERR] $reason")
_receiveState.value = ReceiveState.Error(reason)
}
}
.onFailure { e ->
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK FAIL] ${e.message}")
_receiveState.value = ReceiveState.Error(parseApiError(e, "Withdraw failed"))
}
}
}
fun payBolt11(bolt11: String) {
viewModelScope.launch {
val amountSats = (_sendState.value as? SendState.Bolt11Decoded)?.amountSats ?: 0L
@@ -469,7 +370,7 @@ class WalletViewModel(
if (trimmed == lastOfferedClipboard) return
// ── Guard: ignore our own invoice ─────────────────────────────────────
val ownInvoice = (_receiveState.value as? ReceiveState.InvoiceReady)?.bolt11
val ownInvoice = (receiveState.value as? ReceiveState.InvoiceReady)?.bolt11
if (ownInvoice != null && trimmed.equals(ownInvoice, ignoreCase = true)) {
Log.d("Clipboard", "Skipping own invoice")
lastOfferedClipboard = trimmed // still mark as seen so focus re-checks don't re-trigger
@@ -664,7 +565,10 @@ class WalletViewModel(
.onSuccess { raw ->
Log.d(TAG, "LUD17 [RESPONSE] tag=${raw.tag}")
when (raw.tag) {
"withdrawRequest" -> handleWithdrawResponse(raw, httpsUrl)
"withdrawRequest" -> {
_sendState.value = SendState.Idle
receiveVm.handleWithdrawResponse(raw, httpsUrl)
}
"payRequest" -> {
lnurlCache.put(httpsUrl, raw)
_sendState.value = buildLnurlReadyState(raw, httpsUrl)
@@ -729,7 +633,10 @@ class WalletViewModel(
if (cached != null) {
Log.d(TAG, "LNURL [SCAN CACHE ] $normalized — skipping network call")
when (cached.tag) {
"withdrawRequest" -> handleWithdrawResponse(cached, normalized)
"withdrawRequest" -> {
_sendState.value = SendState.Idle
receiveVm.handleWithdrawResponse(cached, normalized)
}
else -> _sendState.value = buildLnurlReadyState(cached, normalized)
}
return@launch
@@ -750,7 +657,10 @@ class WalletViewModel(
lnurlCache.put(normalized, scanned.rawRes)
when (scanned.tag) {
"payRequest" -> _sendState.value = buildLnurlReadyState(scanned.rawRes, normalized)
"withdrawRequest" -> handleWithdrawResponse(scanned.rawRes, normalized)
"withdrawRequest" -> {
_sendState.value = SendState.Idle
receiveVm.handleWithdrawResponse(scanned.rawRes, normalized)
}
null -> _sendState.value = SendState.Error("Empty response from server")
else -> _sendState.value = SendState.Error("Unsupported type: ${scanned.tag}")
}
@@ -771,7 +681,10 @@ class WalletViewModel(
lnurlCache.put(normalized, scanned.rawRes)
when (scanned.tag) {
"payRequest" -> _sendState.value = buildLnurlReadyState(scanned.rawRes, normalized)
"withdrawRequest" -> handleWithdrawResponse(scanned.rawRes, normalized)
"withdrawRequest" -> {
_sendState.value = SendState.Idle
receiveVm.handleWithdrawResponse(scanned.rawRes, normalized)
}
null -> _sendState.value = SendState.Error("Empty response from server")
else -> _sendState.value = SendState.Error("Unsupported type: ${scanned.tag}")
}
@@ -781,5 +694,4 @@ class WalletViewModel(
}
}
}
}