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.balance.BalanceViewModel
|
||||||
import com.bitcointxoko.gudariwallet.ui.clipboard.ClipboardViewModel
|
import com.bitcointxoko.gudariwallet.ui.clipboard.ClipboardViewModel
|
||||||
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
|
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.receive.ReceiveViewModel
|
||||||
import com.bitcointxoko.gudariwallet.ui.send.LnurlPayer
|
import com.bitcointxoko.gudariwallet.ui.send.LnurlPayer
|
||||||
import com.bitcointxoko.gudariwallet.ui.send.LnurlScanner
|
import com.bitcointxoko.gudariwallet.ui.send.LnurlScanner
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.send.PaymentPoller
|
||||||
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
||||||
|
|
||||||
class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
|
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 lnurlCache = LnurlCacheRepository(app)
|
||||||
val lnurlScanner = LnurlScanner(repo = repo, lnurlCache = lnurlCache)
|
val lnurlScanner = LnurlScanner(repo = repo, lnurlCache = lnurlCache)
|
||||||
val lnurlPayer = LnurlPayer(repo = repo, scanner = lnurlScanner, cache = lnurlCache)
|
val lnurlPayer = LnurlPayer(repo = repo, scanner = lnurlScanner, cache = lnurlCache)
|
||||||
|
val poller = PaymentPoller(repo)
|
||||||
val paymentCache = PaymentCacheRepository(app)
|
val paymentCache = PaymentCacheRepository(app)
|
||||||
val fiatDataStore = FiatDataStore(app)
|
val fiatDataStore = FiatDataStore(app)
|
||||||
val balancePrefs = BalancePrefsStore(app)
|
val balancePrefs = BalancePrefsStore(app)
|
||||||
@@ -49,7 +50,8 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
|||||||
receiveVm.handleWithdrawResponse(raw, lnurl)
|
receiveVm.handleWithdrawResponse(raw, lnurl)
|
||||||
},
|
},
|
||||||
scanner = lnurlScanner,
|
scanner = lnurlScanner,
|
||||||
payer = lnurlPayer
|
payer = lnurlPayer,
|
||||||
|
poller = poller,
|
||||||
)
|
)
|
||||||
|
|
||||||
val clipboardVm = ClipboardViewModel(
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.ContentPaste
|
import androidx.compose.material.icons.filled.ContentPaste
|
||||||
import androidx.compose.material.icons.filled.Nfc
|
|
||||||
import androidx.compose.material.icons.outlined.HourglassTop
|
import androidx.compose.material.icons.outlined.HourglassTop
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
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.SendInputDetector
|
||||||
import com.bitcointxoko.gudariwallet.util.feePpm
|
import com.bitcointxoko.gudariwallet.util.feePpm
|
||||||
import com.bitcointxoko.gudariwallet.util.fiatLabel
|
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.delay
|
||||||
import kotlinx.coroutines.launch
|
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.WalletConstants
|
||||||
import com.bitcointxoko.gudariwallet.util.lnurlProtocolError
|
import com.bitcointxoko.gudariwallet.util.lnurlProtocolError
|
||||||
import com.bitcointxoko.gudariwallet.util.parseApiError
|
import com.bitcointxoko.gudariwallet.util.parseApiError
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -56,6 +55,7 @@ class SendViewModel(
|
|||||||
private val onWithdrawScanned: (raw: LnurlScanResponse, lnurl: String) -> Unit,
|
private val onWithdrawScanned: (raw: LnurlScanResponse, lnurl: String) -> Unit,
|
||||||
private val scanner : LnurlScanner,
|
private val scanner : LnurlScanner,
|
||||||
private val payer : LnurlPayer,
|
private val payer : LnurlPayer,
|
||||||
|
private val poller : PaymentPoller,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _sendState = MutableStateFlow<SendState>(SendState.Idle)
|
private val _sendState = MutableStateFlow<SendState>(SendState.Idle)
|
||||||
@@ -121,58 +121,44 @@ class SendViewModel(
|
|||||||
alias : String?,
|
alias : String?,
|
||||||
strings : SendStrings,
|
strings : SendStrings,
|
||||||
) {
|
) {
|
||||||
val detail = runCatching {
|
val detail = runCatching { repo.getPaymentDetail(paymentHash) }.getOrNull()
|
||||||
repo.getPaymentDetail(paymentHash)
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
when {
|
when {
|
||||||
// ── Settled ──────────────────────────────────────────────────────────
|
// ── Settled immediately
|
||||||
detail?.paid == true -> {
|
detail?.paid == true -> {
|
||||||
val feeSats = detail.details?.feeSat
|
emitPaymentSent(paymentHash, amountSats, detail.details?.feeSat, bolt11, memo, pubkey, alias)
|
||||||
_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 ──────────────────────────────────
|
// ── Hold invoice — poll
|
||||||
detail?.details?.status == "pending" || detail?.details?.pending == true -> {
|
detail?.details?.status == "pending" || detail?.details?.pending == true -> {
|
||||||
// Stay in SendState.Paying and start polling (see Part 2)
|
when (val result = poller.poll(paymentHash)) {
|
||||||
pollUntilSettled(paymentHash, amountSats, bolt11, memo, pubkey, alias, strings)
|
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 -> {
|
else -> {
|
||||||
_sendState.value = SendState.Error(strings.paymentFailed)
|
_sendState.value = SendState.Error(strings.paymentFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun pollUntilSettled(
|
private suspend fun emitPaymentSent(
|
||||||
paymentHash : String,
|
paymentHash : String,
|
||||||
amountSats : Long,
|
amountSats : Long,
|
||||||
|
feeSats : Long?,
|
||||||
bolt11 : String?,
|
bolt11 : String?,
|
||||||
memo : String?,
|
memo : String?,
|
||||||
pubkey : String?,
|
pubkey : String?,
|
||||||
alias : 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(
|
_sendState.value = SendState.PaymentSent(
|
||||||
amountSats = amountSats,
|
amountSats = amountSats,
|
||||||
feeSats = feeSats,
|
feeSats = feeSats,
|
||||||
@@ -180,27 +166,8 @@ class SendViewModel(
|
|||||||
pendingRecord = buildPendingRecord(paymentHash, amountSats, feeSats, bolt11, memo, pubkey, alias)
|
pendingRecord = buildPendingRecord(paymentHash, amountSats, feeSats, bolt11, memo, pubkey, alias)
|
||||||
)
|
)
|
||||||
balanceVm.refreshBalance()
|
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(
|
fun payLnurl(
|
||||||
rawRes : LnurlScanResponse,
|
rawRes : LnurlScanResponse,
|
||||||
|
|||||||
Reference in New Issue
Block a user