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 0f68405..3771576 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 @@ -20,6 +20,7 @@ import com.bitcointxoko.gudariwallet.util.SendInputType import com.bitcointxoko.gudariwallet.util.WalletConstants import com.bitcointxoko.gudariwallet.util.lnurlProtocolError import com.bitcointxoko.gudariwallet.util.parseApiError +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -87,26 +88,18 @@ class SendViewModel( val decoded = _sendState.value as? SendState.Bolt11Decoded val amountSats = decoded?.amountSats ?: 0L _sendState.value = SendState.Paying + Log.d(TAG, "SEND [PAY BOLT11 START] Sending invoice to server") runCatching { repo.payBolt11(bolt11) } .onSuccess { response -> - val feeSats = runCatching { - repo.getPaymentDetail(response.paymentHash).details?.feeSat - }.getOrNull() - _sendState.value = SendState.PaymentSent( - amountSats = amountSats, - feeSats = feeSats, - paymentHash = response.paymentHash, - pendingRecord = buildPendingRecord( - paymentHash = response.paymentHash, - amountSats = amountSats, - feeSats = feeSats, - bolt11 = decoded?.bolt11, - memo = decoded?.memo, - pubkey = decoded?.payee, - alias = decoded?.nodeAlias - ) + handlePaymentResult( + paymentHash = response.paymentHash, + amountSats, + bolt11 = decoded?.bolt11, + memo = decoded?.memo, + pubkey = decoded?.payee, + alias = decoded?.nodeAlias, + strings ) - balanceVm.refreshBalance() } .onFailure { e -> Log.e(TAG, "SEND [PAY BOLT11 ERROR] ${e.message}") @@ -117,6 +110,97 @@ class SendViewModel( } } + private suspend fun handlePaymentResult( + paymentHash : String, + amountSats : Long, + bolt11 : String?, + memo : String?, + pubkey : String?, + alias : String?, + strings : SendStrings, + ) { + val detail = runCatching { + repo.getPaymentDetail(paymentHash) + }.getOrNull() + + when { + // ── Settled ────────────────────────────────────────────────────────── + detail?.paid == true -> { + val feeSats = detail.details?.feeSat + _sendState.value = SendState.PaymentSent( + amountSats = amountSats, + feeSats = feeSats, + paymentHash = paymentHash, + pendingRecord = buildPendingRecord(paymentHash, amountSats, feeSats, bolt11, memo, pubkey, alias) + ) + balanceVm.refreshBalance() + } + + // ── Hold invoice — still pending ────────────────────────────────── + detail?.details?.status == "pending" || detail?.details?.pending == true -> { + // Stay in SendState.Paying and start polling (see Part 2) + pollUntilSettled(paymentHash, amountSats, bolt11, memo, pubkey, alias, strings) + } + + // ── Failed or unknown ───────────────────────────────────────────── + else -> { + _sendState.value = SendState.Error(strings.paymentFailed) + } + } + } + + private suspend fun pollUntilSettled( + paymentHash : String, + amountSats : Long, + bolt11 : String?, + memo : String?, + pubkey : String?, + alias : String?, + strings : SendStrings, + ) { + // SendState.Paying is already set — the UI already shows the + // hourglass/slow-payment message after 10 s, so no UI change needed here. + val pollIntervalMs = 5_000L + val maxAttempts = 120 // 10 minutes total + + repeat(maxAttempts) { attempt -> + delay(pollIntervalMs) + + val detail = runCatching { repo.getPaymentDetail(paymentHash) }.getOrNull() + + when { + detail?.paid == true -> { + val feeSats = detail.details?.feeSat + _sendState.value = SendState.PaymentSent( + amountSats = amountSats, + feeSats = feeSats, + paymentHash = paymentHash, + pendingRecord = buildPendingRecord(paymentHash, amountSats, feeSats, bolt11, memo, pubkey, alias) + ) + balanceVm.refreshBalance() + return // done + } + + // Still pending — keep polling + detail?.details?.status == "pending" || detail?.details?.pending == true -> { + Log.d(TAG, "HOLD INVOICE [POLL $attempt] still pending — $paymentHash") + } + + // Failed (status changed to "failed" or detail is gone) + else -> { + Log.w(TAG, "HOLD INVOICE [POLL $attempt] payment failed — $paymentHash") + _sendState.value = SendState.Error(strings.paymentFailed) + return + } + } + } + + // Timed out — leave in Paying so the user can dismiss manually + // (the existing 10-second slow-payment UI already handles this gracefully) + Log.w(TAG, "HOLD INVOICE [POLL TIMEOUT] $paymentHash — leaving in Paying state") + } + + fun payLnurl( rawRes : LnurlScanResponse, lnurl : String,