diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/send/SendViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/send/SendViewModel.kt index ae9bd56..fc327f1 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/send/SendViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/send/SendViewModel.kt @@ -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, - 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 } } } }