refactor: clean up state in SendViewModel

This commit is contained in:
2026-06-06 17:55:19 +02:00
parent 368d03d263
commit 54f291033c
@@ -1,7 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.send
import android.util.Log
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
@@ -11,7 +10,6 @@ import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
import com.bitcointxoko.gudariwallet.data.WalletRepository
import com.bitcointxoko.gudariwallet.ui.SendState
import com.bitcointxoko.gudariwallet.util.BiometricHelper
import com.bitcointxoko.gudariwallet.util.Bip21Parser
import com.bitcointxoko.gudariwallet.util.LnurlMetadataParser
import com.bitcointxoko.gudariwallet.util.RouteHintAnalyzer
@@ -24,6 +22,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
private const val TAG = "SendViewModel"
@@ -76,7 +75,6 @@ class SendViewModel(
}
// ── Entry point ──────────────────────────────────────────────────────────
fun scan(rawInput: String, strings: SendStrings) {
val normalized = SendInputDetector.normalize(rawInput)
val type = SendInputDetector.detect(rawInput)
@@ -98,22 +96,24 @@ class SendViewModel(
}
// ── Payment ──────────────────────────────────────────────────────────────
fun payBolt11(bolt11: String, strings: SendStrings) {
viewModelScope.launch {
val decoded = _sendState.value as? SendState.Bolt11Decoded
val amountSats = decoded?.amountSats ?: 0L
if (decoded == null) {
Log.w(TAG, "SEND [PAY BOLT11] called in unexpected state: ${_sendState.value::class.simpleName}")
return@launch
}
_sendState.value = SendState.Paying
Log.d(TAG, "SEND [PAY BOLT11 START] Sending invoice to server")
runCatching { repo.payBolt11(bolt11) }
.onSuccess { response ->
handlePaymentResult(
paymentHash = response.paymentHash,
amountSats,
bolt11 = decoded?.bolt11,
memo = decoded?.memo,
pubkey = decoded?.payee,
alias = decoded?.nodeAlias,
amountSats = decoded.amountSats,
bolt11 = decoded.bolt11,
memo = decoded.memo,
pubkey = decoded.payee,
alias = decoded.nodeAlias,
strings
)
}
@@ -182,7 +182,6 @@ class SendViewModel(
_events.tryEmit(SendEvent.PaymentCompleted)
}
fun payLnurl(
rawRes : LnurlScanResponse,
lnurl : String,
@@ -192,30 +191,38 @@ class SendViewModel(
) {
viewModelScope.launch {
_sendState.value = SendState.Paying
val result = payer.pay(rawRes, lnurl, amountMsat, comment)
if (result.isSuccess) {
handlePaymentResult(
paymentHash = result.getOrThrow(),
amountSats = amountMsat / WalletConstants.MSAT_PER_SAT,
bolt11 = null,
memo = null,
pubkey = null,
alias = null,
strings = strings,
)
} else {
_sendState.value = SendState.Error(
parseApiError(
result.exceptionOrNull() ?: Exception(strings.paymentFailed),
strings.paymentFailed
)
)
}
executeLnurlPayment(rawRes, lnurl, amountMsat, comment, strings)
}
}
private suspend fun executeLnurlPayment(
rawRes : LnurlScanResponse,
lnurl : String,
amountMsat: Long,
comment : String?,
strings : SendStrings,
) {
val result = payer.pay(rawRes, lnurl, amountMsat, comment)
if (result.isSuccess) {
handlePaymentResult(
paymentHash = result.getOrThrow(),
amountSats = amountMsat / WalletConstants.MSAT_PER_SAT,
bolt11 = null,
memo = null,
pubkey = null,
alias = null,
strings = strings,
)
} else {
_sendState.value = SendState.Error(
parseApiError(
result.exceptionOrNull() ?: Exception(strings.paymentFailed),
strings.paymentFailed
)
)
}
}
fun fetchLnurlInvoice(
rawRes : LnurlScanResponse,
@@ -278,7 +285,6 @@ class SendViewModel(
resolveAliasesInBackground(
payee = decoded.payee,
routeHints = decoded.routeHints,
readState = { _sendState.value },
copyAlias = { state, alias ->
(state as? SendState.LnurlInvoiceReady)
?.takeIf { it.payee == decoded.payee }
@@ -295,9 +301,7 @@ class SendViewModel(
fun payLnurlInvoice(state: SendState.LnurlInvoiceReady, strings: SendStrings) {
viewModelScope.launch {
_sendState.value = SendState.Paying
val directResult = runCatching { repo.payBolt11(state.bolt11) }
if (directResult.isSuccess) {
val paymentHash = directResult.getOrThrow().paymentHash
handlePaymentResult(
@@ -311,17 +315,9 @@ class SendViewModel(
)
return@launch
}
Log.w(TAG, "LNURL [PAY INVOICE DIRECT FAIL] " +
"${directResult.exceptionOrNull()?.message} — falling back to payLnurl")
payLnurl(
rawRes = state.rawRes,
lnurl = state.lnurl,
amountMsat = state.amountMsat,
comment = state.comment,
strings = strings,
)
executeLnurlPayment(state.rawRes, state.lnurl, state.amountMsat, state.comment, strings)
}
}
@@ -358,10 +354,7 @@ class SendViewModel(
.onSuccess { raw ->
Log.d(TAG, "LUD17 [RESPONSE] tag=${raw.tag}")
when (raw.tag) {
"withdrawRequest" -> {
_sendState.value = SendState.Idle
_events.tryEmit(SendEvent.WithdrawScanned(raw, httpsUrl))
}
"withdrawRequest" -> {emitWithdrawScanned(raw, httpsUrl)}
"payRequest" -> {
lnurlCache.put(httpsUrl, raw)
_sendState.value = buildLnurlReadyState(raw, httpsUrl)
@@ -405,7 +398,6 @@ class SendViewModel(
resolveAliasesInBackground(
payee = decoded.payee,
routeHints = decoded.routeHints,
readState = { _sendState.value },
copyAlias = { state, alias ->
(state as? SendState.Bolt11Decoded)
?.takeIf { it.payee == decoded.payee }
@@ -429,18 +421,11 @@ class SendViewModel(
private fun handleLnurlScan(normalized: String, strings: SendStrings) {
_sendState.value = SendState.Scanning
viewModelScope.launch {
val result = scanner.scan(normalized)
if (result.isFailure) {
_sendState.value = SendState.Error(
parseApiError(
result.exceptionOrNull() ?: Exception("Scan failed"),
strings.couldNotResolveAddress
) )
val (raw, fromCache) = scanner.scan(normalized).getOrElse { e ->
_sendState.value = SendState.Error(parseApiError(e, strings.couldNotResolveAddress))
return@launch
}
val (raw, fromCache) = result.getOrThrow()
val protocolError = lnurlProtocolError(raw)
if (protocolError != null) {
Log.w(TAG, "LNURL [SCAN PROTO ERR] $protocolError")
@@ -452,7 +437,7 @@ class SendViewModel(
when (raw.tag) {
"payRequest" -> _sendState.value = buildLnurlReadyState(raw, normalized)
"withdrawRequest" -> { _sendState.value = SendState.Idle; _events.tryEmit(SendEvent.WithdrawScanned(raw, normalized)) }
"withdrawRequest" -> emitWithdrawScanned(raw, normalized)
null -> _sendState.value = SendState.Error(strings.emptyResponse)
else -> _sendState.value = SendState.Error(strings.unsupportedType(raw.tag))
}
@@ -461,6 +446,10 @@ class SendViewModel(
// ── Private helpers ──────────────────────────────────────────────────────
private fun emitWithdrawScanned(raw: LnurlScanResponse, lnurl: String) {
_events.tryEmit(SendEvent.WithdrawScanned(raw, lnurl))
}
private fun buildLnurlReadyState(raw: LnurlScanResponse, lnurl: String): SendState.LnurlReady {
return SendState.LnurlReady(
callback = raw.callback ?: "",
@@ -477,22 +466,19 @@ class SendViewModel(
private fun resolveAliasesInBackground(
payee : String?,
routeHints : List<RouteHintHop>,
readState : () -> SendState,
copyAlias : (SendState, String) -> SendState?,
copyHint : (SendState, String, String) -> SendState?
) {
payee?.let { pubkey ->
viewModelScope.launch {
val alias = aliasRepo.resolve(pubkey) ?: return@launch
val current = readState()
copyAlias(current, alias)?.let { _sendState.value = it }
_sendState.update { copyAlias(it, alias) ?: it }
}
}
routeHints.map { it.publicKey }.distinct().forEach { pubkey ->
viewModelScope.launch {
val alias = aliasRepo.resolve(pubkey) ?: return@launch
val current = readState()
copyHint(current, pubkey, alias)?.let { _sendState.value = it }
_sendState.update { copyHint(it, pubkey, alias) ?: it }
}
}
}