refactor: extract PaymentPoller
This commit is contained in:
@@ -17,10 +17,10 @@ import com.bitcointxoko.gudariwallet.service.NodeAliasService
|
||||
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.nfc.NfcViewModel
|
||||
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel
|
||||
import com.bitcointxoko.gudariwallet.ui.send.LnurlPayer
|
||||
import com.bitcointxoko.gudariwallet.ui.send.LnurlScanner
|
||||
import com.bitcointxoko.gudariwallet.ui.send.PaymentPoller
|
||||
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
||||
|
||||
class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
|
||||
@@ -33,6 +33,7 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
||||
val lnurlCache = LnurlCacheRepository(app)
|
||||
val lnurlScanner = LnurlScanner(repo = repo, lnurlCache = lnurlCache)
|
||||
val lnurlPayer = LnurlPayer(repo = repo, scanner = lnurlScanner, cache = lnurlCache)
|
||||
val poller = PaymentPoller(repo)
|
||||
val paymentCache = PaymentCacheRepository(app)
|
||||
val fiatDataStore = FiatDataStore(app)
|
||||
val balancePrefs = BalancePrefsStore(app)
|
||||
@@ -49,7 +50,8 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
||||
receiveVm.handleWithdrawResponse(raw, lnurl)
|
||||
},
|
||||
scanner = lnurlScanner,
|
||||
payer = lnurlPayer
|
||||
payer = lnurlPayer,
|
||||
poller = poller,
|
||||
)
|
||||
|
||||
val clipboardVm = ClipboardViewModel(
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.bitcointxoko.gudariwallet.ui.send
|
||||
|
||||
import android.util.Log
|
||||
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
||||
class PaymentPoller(private val repo: WalletRepository) {
|
||||
|
||||
sealed class PollResult {
|
||||
data class Settled(val feeSats: Long?) : PollResult()
|
||||
object Failed : PollResult()
|
||||
object TimedOut : PollResult()
|
||||
}
|
||||
|
||||
suspend fun poll(
|
||||
paymentHash : String,
|
||||
intervalMs : Long = 5_000L,
|
||||
maxAttempts : Int = 120,
|
||||
): PollResult {
|
||||
repeat(maxAttempts) { attempt ->
|
||||
delay(intervalMs)
|
||||
|
||||
val detail = runCatching { repo.getPaymentDetail(paymentHash) }.getOrNull()
|
||||
|
||||
when {
|
||||
detail?.paid == true -> {
|
||||
Log.d(TAG, "HOLD INVOICE [POLL $attempt] settled — $paymentHash")
|
||||
return PollResult.Settled(detail.details?.feeSat)
|
||||
}
|
||||
detail?.details?.status == "pending" || detail?.details?.pending == true -> {
|
||||
Log.d(TAG, "HOLD INVOICE [POLL $attempt] still pending — $paymentHash")
|
||||
// continue
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "HOLD INVOICE [POLL $attempt] payment failed — $paymentHash")
|
||||
return PollResult.Failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "HOLD INVOICE [POLL TIMEOUT] $paymentHash")
|
||||
return PollResult.TimedOut
|
||||
}
|
||||
|
||||
companion object { private const val TAG = "PaymentPoller" }
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.filled.Nfc
|
||||
import androidx.compose.material.icons.outlined.HourglassTop
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -34,8 +33,6 @@ import com.bitcointxoko.gudariwallet.ui.receive.AmountDisplay
|
||||
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
||||
import com.bitcointxoko.gudariwallet.util.feePpm
|
||||
import com.bitcointxoko.gudariwallet.util.fiatLabel
|
||||
import com.bitcointxoko.gudariwallet.util.formatFiat
|
||||
import com.bitcointxoko.gudariwallet.util.satsToFiat
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ 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
|
||||
@@ -56,6 +55,7 @@ class SendViewModel(
|
||||
private val onWithdrawScanned: (raw: LnurlScanResponse, lnurl: String) -> Unit,
|
||||
private val scanner : LnurlScanner,
|
||||
private val payer : LnurlPayer,
|
||||
private val poller : PaymentPoller,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _sendState = MutableStateFlow<SendState>(SendState.Idle)
|
||||
@@ -121,87 +121,54 @@ class SendViewModel(
|
||||
alias : String?,
|
||||
strings : SendStrings,
|
||||
) {
|
||||
val detail = runCatching {
|
||||
repo.getPaymentDetail(paymentHash)
|
||||
}.getOrNull()
|
||||
val detail = runCatching { repo.getPaymentDetail(paymentHash) }.getOrNull()
|
||||
|
||||
when {
|
||||
// ── Settled ──────────────────────────────────────────────────────────
|
||||
// ── Settled immediately
|
||||
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()
|
||||
emitPaymentSent(paymentHash, amountSats, detail.details?.feeSat, bolt11, memo, pubkey, alias)
|
||||
}
|
||||
|
||||
// ── Hold invoice — still pending ──────────────────────────────────
|
||||
// ── Hold invoice — poll
|
||||
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)
|
||||
when (val result = poller.poll(paymentHash)) {
|
||||
is PaymentPoller.PollResult.Settled ->
|
||||
emitPaymentSent(paymentHash, amountSats, result.feeSats, bolt11, memo, pubkey, alias)
|
||||
|
||||
is PaymentPoller.PollResult.Failed ->
|
||||
_sendState.value = SendState.Error(strings.paymentFailed)
|
||||
|
||||
is PaymentPoller.PollResult.TimedOut ->
|
||||
_sendState.value = SendState.Error(strings.paymentFailed)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Failed or unknown ─────────────────────────────────────────────
|
||||
// ── Failed or unknown
|
||||
else -> {
|
||||
_sendState.value = SendState.Error(strings.paymentFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pollUntilSettled(
|
||||
private suspend fun emitPaymentSent(
|
||||
paymentHash : String,
|
||||
amountSats : Long,
|
||||
feeSats : 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")
|
||||
_sendState.value = SendState.PaymentSent(
|
||||
amountSats = amountSats,
|
||||
feeSats = feeSats,
|
||||
paymentHash = paymentHash,
|
||||
pendingRecord = buildPendingRecord(paymentHash, amountSats, feeSats, bolt11, memo, pubkey, alias)
|
||||
)
|
||||
balanceVm.refreshBalance()
|
||||
}
|
||||
|
||||
|
||||
fun payLnurl(
|
||||
rawRes : LnurlScanResponse,
|
||||
lnurl : String,
|
||||
|
||||
Reference in New Issue
Block a user