refactor: WalletViewModel extract ReceiveViewModel
This commit is contained in:
@@ -14,7 +14,6 @@ import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
|||||||
import com.bitcointxoko.gudariwallet.util.SendInputType
|
import com.bitcointxoko.gudariwallet.util.SendInputType
|
||||||
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||||
import com.bitcointxoko.gudariwallet.util.parseApiError
|
import com.bitcointxoko.gudariwallet.util.parseApiError
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -25,6 +24,7 @@ import com.bitcointxoko.gudariwallet.api.RouteHintHop
|
|||||||
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
||||||
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
||||||
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
|
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel
|
||||||
import com.bitcointxoko.gudariwallet.util.BiometricHelper
|
import com.bitcointxoko.gudariwallet.util.BiometricHelper
|
||||||
|
|
||||||
private const val TAG = "WalletViewModel"
|
private const val TAG = "WalletViewModel"
|
||||||
@@ -36,7 +36,8 @@ class WalletViewModel(
|
|||||||
val paymentCache: PaymentCacheRepository,
|
val paymentCache: PaymentCacheRepository,
|
||||||
private val balancePrefs: BalancePrefsStore,
|
private val balancePrefs: BalancePrefsStore,
|
||||||
val balanceVm : BalanceViewModel,
|
val balanceVm : BalanceViewModel,
|
||||||
val fiatVm : FiatViewModel
|
val fiatVm : FiatViewModel,
|
||||||
|
val receiveVm : ReceiveViewModel,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// ── Balance state ─────────────────────────────────────────────────────────
|
// ── Balance state ─────────────────────────────────────────────────────────
|
||||||
@@ -49,15 +50,17 @@ class WalletViewModel(
|
|||||||
suspend fun getSupportedCurrencies() = fiatVm.getSupportedCurrencies()
|
suspend fun getSupportedCurrencies() = fiatVm.getSupportedCurrencies()
|
||||||
val fiatRate: Double? get() = fiatVm.fiatRate
|
val fiatRate: Double? get() = fiatVm.fiatRate
|
||||||
val fiatCurrency: String? get() = fiatVm.fiatCurrency
|
val fiatCurrency: String? get() = fiatVm.fiatCurrency
|
||||||
|
val receiveState: StateFlow<ReceiveState> get() = receiveVm.receiveState
|
||||||
|
val navigateToReceive: SharedFlow<Unit> get() = receiveVm.navigateToReceive
|
||||||
|
fun createInvoice(amountSats: Long, memo: String) = receiveVm.createInvoice(amountSats, memo)
|
||||||
|
fun resetReceiveState() = receiveVm.resetReceiveState()
|
||||||
|
fun executeWithdraw(k1: String, callback: String, amountSats: Long, memo: String) =
|
||||||
|
receiveVm.executeWithdraw(k1, callback, amountSats, memo)
|
||||||
|
|
||||||
|
|
||||||
// ── Other states ──────────────────────────────────────────────────────────
|
// ── Other states ──────────────────────────────────────────────────────────
|
||||||
private val _receiveState = MutableStateFlow<ReceiveState>(ReceiveState.Idle)
|
|
||||||
val receiveState: StateFlow<ReceiveState> = _receiveState
|
|
||||||
private val _sendState = MutableStateFlow<SendState>(SendState.Idle)
|
private val _sendState = MutableStateFlow<SendState>(SendState.Idle)
|
||||||
val sendState: StateFlow<SendState> = _sendState
|
val sendState: StateFlow<SendState> = _sendState
|
||||||
private val _navigateToReceive = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
|
||||||
val navigateToReceive: SharedFlow<Unit> = _navigateToReceive
|
|
||||||
private val _lightningAddress = MutableStateFlow<String?>(balancePrefs.lightningAddress)
|
private val _lightningAddress = MutableStateFlow<String?>(balancePrefs.lightningAddress)
|
||||||
val lightningAddress: StateFlow<String?> = _lightningAddress
|
val lightningAddress: StateFlow<String?> = _lightningAddress
|
||||||
|
|
||||||
@@ -72,18 +75,11 @@ class WalletViewModel(
|
|||||||
private fun observePaymentEvents() {
|
private fun observePaymentEvents() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
balanceVm.incomingPayment.collect { event ->
|
balanceVm.incomingPayment.collect { event ->
|
||||||
val rs = _receiveState.value
|
receiveVm.onIncomingPayment(event)
|
||||||
if (rs is ReceiveState.InvoiceReady) {
|
|
||||||
Log.d(TAG, "BALANCE [WS RECEIVE] Marking invoice ${rs.paymentHash.take(8)}… as received")
|
|
||||||
_receiveState.value = ReceiveState.PaymentReceived(
|
|
||||||
amountSats = event.amountSats,
|
|
||||||
memo = event.memo,
|
|
||||||
checkingId = rs.paymentHash
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchLightningAddress() {
|
private fun fetchLightningAddress() {
|
||||||
// If we already have a persisted address, show it immediately and
|
// If we already have a persisted address, show it immediately and
|
||||||
// still refresh in the background (address could have changed).
|
// still refresh in the background (address could have changed).
|
||||||
@@ -105,38 +101,6 @@ class WalletViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Receive ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
fun createInvoice(amountSats: Long, memo: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_receiveState.value = ReceiveState.AwaitingInvoice
|
|
||||||
runCatching { repo.createInvoice(amountSats, memo) }
|
|
||||||
.onSuccess { invoice ->
|
|
||||||
val fallbackExpiry = System.currentTimeMillis() / 1000L + 600L
|
|
||||||
val expiresAt = invoice.expiry?.let { isoString ->
|
|
||||||
runCatching {
|
|
||||||
java.time.LocalDateTime
|
|
||||||
.parse(isoString, java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
|
||||||
.toEpochSecond(java.time.ZoneOffset.UTC)
|
|
||||||
}.getOrNull()
|
|
||||||
} ?: fallbackExpiry
|
|
||||||
_receiveState.value = ReceiveState.InvoiceReady(
|
|
||||||
bolt11 = invoice.bolt11,
|
|
||||||
paymentHash = invoice.paymentHash,
|
|
||||||
amountSats = amountSats,
|
|
||||||
memo = memo,
|
|
||||||
expiresAt = expiresAt
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.onFailure { e ->
|
|
||||||
Log.e(TAG, "RECEIVE [CREATE INVOICE ERROR] ${e.message}")
|
|
||||||
_receiveState.value = ReceiveState.Error(parseApiError(e, "Failed to create invoice"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetReceiveState() { _receiveState.value = ReceiveState.Idle }
|
|
||||||
|
|
||||||
// ── Send ──────────────────────────────────────────────────────────────────
|
// ── Send ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fun scan(rawInput: String) {
|
fun scan(rawInput: String) {
|
||||||
@@ -160,69 +124,6 @@ class WalletViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun handleWithdrawResponse(raw: LnurlScanResponse, lnurl: String) {
|
|
||||||
val minSats = (raw.minWithdrawable ?: 0L) / WalletConstants.MSAT_PER_SAT
|
|
||||||
val maxSats = (raw.maxWithdrawable ?: 0L) / WalletConstants.MSAT_PER_SAT
|
|
||||||
_receiveState.value = ReceiveState.LnurlWithdrawReady(
|
|
||||||
k1 = raw.k1 ?: "",
|
|
||||||
callback = raw.callback ?: "",
|
|
||||||
defaultDescription = raw.defaultDescription ?: "",
|
|
||||||
minSats = minSats,
|
|
||||||
maxSats = maxSats,
|
|
||||||
domain = LnurlMetadataParser.extractDomain(raw.callback),
|
|
||||||
lnurl = lnurl
|
|
||||||
)
|
|
||||||
_sendState.value = SendState.Idle // clear scanning spinner
|
|
||||||
_navigateToReceive.tryEmit(Unit)
|
|
||||||
Log.d(TAG, "LNURL-WITHDRAW [NAV EVENT] min=$minSats max=$maxSats sats — navigating to Receive")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun executeWithdraw(
|
|
||||||
k1 : String,
|
|
||||||
callback : String,
|
|
||||||
amountSats: Long,
|
|
||||||
memo : String
|
|
||||||
) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_receiveState.value = ReceiveState.AwaitingInvoice
|
|
||||||
|
|
||||||
val invoiceResult = runCatching { repo.createInvoice(amountSats, memo) }
|
|
||||||
if (invoiceResult.isFailure) {
|
|
||||||
val e = invoiceResult.exceptionOrNull() ?: Exception("Failed to create invoice")
|
|
||||||
Log.e(TAG, "LNURL-WITHDRAW [INVOICE ERROR] ${e.message}")
|
|
||||||
_receiveState.value = ReceiveState.Error(parseApiError(e, "Failed to create invoice"))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
val invoice = invoiceResult.getOrThrow()
|
|
||||||
Log.d(TAG, "LNURL-WITHDRAW [INVOICE] ${invoice.paymentHash} — submitting to callback")
|
|
||||||
|
|
||||||
runCatching { repo.executeWithdraw(callback, k1, invoice.bolt11) }
|
|
||||||
.onSuccess { response ->
|
|
||||||
if (response.status == "OK") {
|
|
||||||
Log.d(TAG, "LNURL-WITHDRAW [CALLBACK OK] waiting for push payment")
|
|
||||||
_receiveState.value = ReceiveState.InvoiceReady(
|
|
||||||
bolt11 = invoice.bolt11,
|
|
||||||
paymentHash = invoice.paymentHash,
|
|
||||||
amountSats = amountSats,
|
|
||||||
memo = memo,
|
|
||||||
expiresAt = System.currentTimeMillis() / 1000L + 600L
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val reason = response.reason
|
|
||||||
?.ifBlank { null }
|
|
||||||
?: "Withdraw service rejected the invoice"
|
|
||||||
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK ERR] $reason")
|
|
||||||
_receiveState.value = ReceiveState.Error(reason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onFailure { e ->
|
|
||||||
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK FAIL] ${e.message}")
|
|
||||||
_receiveState.value = ReceiveState.Error(parseApiError(e, "Withdraw failed"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun payBolt11(bolt11: String) {
|
fun payBolt11(bolt11: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val amountSats = (_sendState.value as? SendState.Bolt11Decoded)?.amountSats ?: 0L
|
val amountSats = (_sendState.value as? SendState.Bolt11Decoded)?.amountSats ?: 0L
|
||||||
@@ -469,7 +370,7 @@ class WalletViewModel(
|
|||||||
if (trimmed == lastOfferedClipboard) return
|
if (trimmed == lastOfferedClipboard) return
|
||||||
|
|
||||||
// ── Guard: ignore our own invoice ─────────────────────────────────────
|
// ── Guard: ignore our own invoice ─────────────────────────────────────
|
||||||
val ownInvoice = (_receiveState.value as? ReceiveState.InvoiceReady)?.bolt11
|
val ownInvoice = (receiveState.value as? ReceiveState.InvoiceReady)?.bolt11
|
||||||
if (ownInvoice != null && trimmed.equals(ownInvoice, ignoreCase = true)) {
|
if (ownInvoice != null && trimmed.equals(ownInvoice, ignoreCase = true)) {
|
||||||
Log.d("Clipboard", "Skipping own invoice")
|
Log.d("Clipboard", "Skipping own invoice")
|
||||||
lastOfferedClipboard = trimmed // still mark as seen so focus re-checks don't re-trigger
|
lastOfferedClipboard = trimmed // still mark as seen so focus re-checks don't re-trigger
|
||||||
@@ -664,7 +565,10 @@ class WalletViewModel(
|
|||||||
.onSuccess { raw ->
|
.onSuccess { raw ->
|
||||||
Log.d(TAG, "LUD17 [RESPONSE] tag=${raw.tag}")
|
Log.d(TAG, "LUD17 [RESPONSE] tag=${raw.tag}")
|
||||||
when (raw.tag) {
|
when (raw.tag) {
|
||||||
"withdrawRequest" -> handleWithdrawResponse(raw, httpsUrl)
|
"withdrawRequest" -> {
|
||||||
|
_sendState.value = SendState.Idle
|
||||||
|
receiveVm.handleWithdrawResponse(raw, httpsUrl)
|
||||||
|
}
|
||||||
"payRequest" -> {
|
"payRequest" -> {
|
||||||
lnurlCache.put(httpsUrl, raw)
|
lnurlCache.put(httpsUrl, raw)
|
||||||
_sendState.value = buildLnurlReadyState(raw, httpsUrl)
|
_sendState.value = buildLnurlReadyState(raw, httpsUrl)
|
||||||
@@ -729,7 +633,10 @@ class WalletViewModel(
|
|||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
Log.d(TAG, "LNURL [SCAN CACHE ] $normalized — skipping network call")
|
Log.d(TAG, "LNURL [SCAN CACHE ] $normalized — skipping network call")
|
||||||
when (cached.tag) {
|
when (cached.tag) {
|
||||||
"withdrawRequest" -> handleWithdrawResponse(cached, normalized)
|
"withdrawRequest" -> {
|
||||||
|
_sendState.value = SendState.Idle
|
||||||
|
receiveVm.handleWithdrawResponse(cached, normalized)
|
||||||
|
}
|
||||||
else -> _sendState.value = buildLnurlReadyState(cached, normalized)
|
else -> _sendState.value = buildLnurlReadyState(cached, normalized)
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
@@ -750,7 +657,10 @@ class WalletViewModel(
|
|||||||
lnurlCache.put(normalized, scanned.rawRes)
|
lnurlCache.put(normalized, scanned.rawRes)
|
||||||
when (scanned.tag) {
|
when (scanned.tag) {
|
||||||
"payRequest" -> _sendState.value = buildLnurlReadyState(scanned.rawRes, normalized)
|
"payRequest" -> _sendState.value = buildLnurlReadyState(scanned.rawRes, normalized)
|
||||||
"withdrawRequest" -> handleWithdrawResponse(scanned.rawRes, normalized)
|
"withdrawRequest" -> {
|
||||||
|
_sendState.value = SendState.Idle
|
||||||
|
receiveVm.handleWithdrawResponse(scanned.rawRes, normalized)
|
||||||
|
}
|
||||||
null -> _sendState.value = SendState.Error("Empty response from server")
|
null -> _sendState.value = SendState.Error("Empty response from server")
|
||||||
else -> _sendState.value = SendState.Error("Unsupported type: ${scanned.tag}")
|
else -> _sendState.value = SendState.Error("Unsupported type: ${scanned.tag}")
|
||||||
}
|
}
|
||||||
@@ -771,7 +681,10 @@ class WalletViewModel(
|
|||||||
lnurlCache.put(normalized, scanned.rawRes)
|
lnurlCache.put(normalized, scanned.rawRes)
|
||||||
when (scanned.tag) {
|
when (scanned.tag) {
|
||||||
"payRequest" -> _sendState.value = buildLnurlReadyState(scanned.rawRes, normalized)
|
"payRequest" -> _sendState.value = buildLnurlReadyState(scanned.rawRes, normalized)
|
||||||
"withdrawRequest" -> handleWithdrawResponse(scanned.rawRes, normalized)
|
"withdrawRequest" -> {
|
||||||
|
_sendState.value = SendState.Idle
|
||||||
|
receiveVm.handleWithdrawResponse(scanned.rawRes, normalized)
|
||||||
|
}
|
||||||
null -> _sendState.value = SendState.Error("Empty response from server")
|
null -> _sendState.value = SendState.Error("Empty response from server")
|
||||||
else -> _sendState.value = SendState.Error("Unsupported type: ${scanned.tag}")
|
else -> _sendState.value = SendState.Error("Unsupported type: ${scanned.tag}")
|
||||||
}
|
}
|
||||||
@@ -781,5 +694,4 @@ class WalletViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
|
|||||||
import com.bitcointxoko.gudariwallet.service.NodeAliasService
|
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.fiat.FiatViewModel
|
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel
|
||||||
|
|
||||||
class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
|
class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
@@ -25,7 +26,10 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
|||||||
val balancePrefs = BalancePrefsStore(app)
|
val balancePrefs = BalancePrefsStore(app)
|
||||||
val balanceVm = BalanceViewModel(repo, balancePrefs)
|
val balanceVm = BalanceViewModel(repo, balancePrefs)
|
||||||
val fiatVm = FiatViewModel(repo, balancePrefs, balanceVm)
|
val fiatVm = FiatViewModel(repo, balancePrefs, balanceVm)
|
||||||
|
val receiveVm = ReceiveViewModel(repo)
|
||||||
|
|
||||||
balanceVm.onBalanceRefreshed = { fiatVm.onBalanceRefreshed() }
|
balanceVm.onBalanceRefreshed = { fiatVm.onBalanceRefreshed() }
|
||||||
|
|
||||||
val lnurlCache = LnurlCacheRepository(app)
|
val lnurlCache = LnurlCacheRepository(app)
|
||||||
val paymentCache = PaymentCacheRepository(app)
|
val paymentCache = PaymentCacheRepository(app)
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@@ -36,7 +40,8 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
|||||||
paymentCache,
|
paymentCache,
|
||||||
balancePrefs,
|
balancePrefs,
|
||||||
balanceVm,
|
balanceVm,
|
||||||
fiatVm
|
fiatVm,
|
||||||
|
receiveVm
|
||||||
) as T
|
) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.bitcointxoko.gudariwallet.ui.receive
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.ReceiveState
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.balance.IncomingPaymentEvent
|
||||||
|
import com.bitcointxoko.gudariwallet.util.LnurlMetadataParser
|
||||||
|
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||||
|
import com.bitcointxoko.gudariwallet.util.parseApiError
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private const val TAG = "ReceiveViewModel"
|
||||||
|
|
||||||
|
class ReceiveViewModel(
|
||||||
|
private val repo: WalletRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _receiveState = MutableStateFlow<ReceiveState>(ReceiveState.Idle)
|
||||||
|
val receiveState: StateFlow<ReceiveState> = _receiveState
|
||||||
|
|
||||||
|
private val _navigateToReceive = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
|
val navigateToReceive: SharedFlow<Unit> = _navigateToReceive
|
||||||
|
|
||||||
|
// ── Called by WalletViewModel when BalanceViewModel emits an incoming payment
|
||||||
|
fun onIncomingPayment(event: IncomingPaymentEvent) {
|
||||||
|
val rs = _receiveState.value
|
||||||
|
if (rs is ReceiveState.InvoiceReady) {
|
||||||
|
Log.d(TAG, "BALANCE [WS RECEIVE] Marking invoice ${rs.paymentHash.take(8)}… as received")
|
||||||
|
_receiveState.value = ReceiveState.PaymentReceived(
|
||||||
|
amountSats = event.amountSats,
|
||||||
|
memo = event.memo,
|
||||||
|
checkingId = rs.paymentHash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Invoice creation ─────────────────────────────────────────────────────
|
||||||
|
fun createInvoice(amountSats: Long, memo: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_receiveState.value = ReceiveState.AwaitingInvoice
|
||||||
|
runCatching { repo.createInvoice(amountSats, memo) }
|
||||||
|
.onSuccess { invoice ->
|
||||||
|
val fallbackExpiry = System.currentTimeMillis() / 1000L + 600L
|
||||||
|
val expiresAt = invoice.expiry?.let { isoString ->
|
||||||
|
runCatching {
|
||||||
|
java.time.LocalDateTime
|
||||||
|
.parse(isoString, java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||||
|
.toEpochSecond(java.time.ZoneOffset.UTC)
|
||||||
|
}.getOrNull()
|
||||||
|
} ?: fallbackExpiry
|
||||||
|
_receiveState.value = ReceiveState.InvoiceReady(
|
||||||
|
bolt11 = invoice.bolt11,
|
||||||
|
paymentHash = invoice.paymentHash,
|
||||||
|
amountSats = amountSats,
|
||||||
|
memo = memo,
|
||||||
|
expiresAt = expiresAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onFailure { e ->
|
||||||
|
Log.e(TAG, "RECEIVE [CREATE INVOICE ERROR] ${e.message}")
|
||||||
|
_receiveState.value = ReceiveState.Error(parseApiError(e, "Failed to create invoice"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetReceiveState() { _receiveState.value = ReceiveState.Idle }
|
||||||
|
|
||||||
|
// ── LNURL-withdraw ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Called from WalletViewModel's scan handlers when a withdrawRequest is detected.
|
||||||
|
fun handleWithdrawResponse(raw: com.bitcointxoko.gudariwallet.api.LnurlScanResponse, lnurl: String) {
|
||||||
|
val minSats = (raw.minWithdrawable ?: 0L) / WalletConstants.MSAT_PER_SAT
|
||||||
|
val maxSats = (raw.maxWithdrawable ?: 0L) / WalletConstants.MSAT_PER_SAT
|
||||||
|
_receiveState.value = ReceiveState.LnurlWithdrawReady(
|
||||||
|
k1 = raw.k1 ?: "",
|
||||||
|
callback = raw.callback ?: "",
|
||||||
|
defaultDescription = raw.defaultDescription ?: "",
|
||||||
|
minSats = minSats,
|
||||||
|
maxSats = maxSats,
|
||||||
|
domain = LnurlMetadataParser.extractDomain(raw.callback),
|
||||||
|
lnurl = lnurl
|
||||||
|
)
|
||||||
|
_navigateToReceive.tryEmit(Unit)
|
||||||
|
Log.d(TAG, "LNURL-WITHDRAW [NAV EVENT] min=$minSats max=$maxSats sats — navigating to Receive")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun executeWithdraw(
|
||||||
|
k1 : String,
|
||||||
|
callback : String,
|
||||||
|
amountSats: Long,
|
||||||
|
memo : String
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_receiveState.value = ReceiveState.AwaitingInvoice
|
||||||
|
|
||||||
|
val invoiceResult = runCatching { repo.createInvoice(amountSats, memo) }
|
||||||
|
if (invoiceResult.isFailure) {
|
||||||
|
val e = invoiceResult.exceptionOrNull() ?: Exception("Failed to create invoice")
|
||||||
|
Log.e(TAG, "LNURL-WITHDRAW [INVOICE ERROR] ${e.message}")
|
||||||
|
_receiveState.value = ReceiveState.Error(parseApiError(e, "Failed to create invoice"))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val invoice = invoiceResult.getOrThrow()
|
||||||
|
Log.d(TAG, "LNURL-WITHDRAW [INVOICE] ${invoice.paymentHash} — submitting to callback")
|
||||||
|
|
||||||
|
runCatching { repo.executeWithdraw(callback, k1, invoice.bolt11) }
|
||||||
|
.onSuccess { response ->
|
||||||
|
if (response.status == "OK") {
|
||||||
|
Log.d(TAG, "LNURL-WITHDRAW [CALLBACK OK] waiting for push payment")
|
||||||
|
_receiveState.value = ReceiveState.InvoiceReady(
|
||||||
|
bolt11 = invoice.bolt11,
|
||||||
|
paymentHash = invoice.paymentHash,
|
||||||
|
amountSats = amountSats,
|
||||||
|
memo = memo,
|
||||||
|
expiresAt = System.currentTimeMillis() / 1000L + 600L
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val reason = response.reason?.ifBlank { null }
|
||||||
|
?: "Withdraw service rejected the invoice"
|
||||||
|
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK ERR] $reason")
|
||||||
|
_receiveState.value = ReceiveState.Error(reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { e ->
|
||||||
|
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK FAIL] ${e.message}")
|
||||||
|
_receiveState.value = ReceiveState.Error(parseApiError(e, "Withdraw failed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user