refactor: WalletViewModel extract ClipboardViewModel
This commit is contained in:
@@ -4,12 +4,9 @@ import android.util.Log
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
|
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
|
||||||
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
|
|
||||||
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
|
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||||
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
|
||||||
import com.bitcointxoko.gudariwallet.util.SendInputType
|
|
||||||
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
|
||||||
@@ -17,6 +14,7 @@ import kotlinx.coroutines.launch
|
|||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
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.clipboard.ClipboardViewModel
|
||||||
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.ui.receive.ReceiveViewModel
|
||||||
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
||||||
@@ -26,16 +24,15 @@ private const val TAG = "WalletViewModel"
|
|||||||
class WalletViewModel(
|
class WalletViewModel(
|
||||||
val repo : WalletRepository,
|
val repo : WalletRepository,
|
||||||
val aliasRepo : NodeAliasRepository,
|
val aliasRepo : NodeAliasRepository,
|
||||||
val lnurlCache: LnurlCacheRepository,
|
|
||||||
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,
|
val receiveVm : ReceiveViewModel,
|
||||||
val sendVm : SendViewModel,
|
val sendVm : SendViewModel,
|
||||||
|
val clipboardVm : ClipboardViewModel,
|
||||||
|
val lightningAddress: MutableStateFlow<String?>,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// ── Balance state ─────────────────────────────────────────────────────────
|
|
||||||
val balanceState: StateFlow<BalanceState> get() = balanceVm.balanceState
|
val balanceState: StateFlow<BalanceState> get() = balanceVm.balanceState
|
||||||
val balanceHidden: StateFlow<Boolean> get() = fiatVm.balanceHidden
|
val balanceHidden: StateFlow<Boolean> get() = fiatVm.balanceHidden
|
||||||
val selectedCurrency: StateFlow<String?> get() = fiatVm.selectedCurrency
|
val selectedCurrency: StateFlow<String?> get() = fiatVm.selectedCurrency
|
||||||
@@ -52,6 +49,7 @@ class WalletViewModel(
|
|||||||
fun executeWithdraw(k1: String, callback: String, amountSats: Long, memo: String) =
|
fun executeWithdraw(k1: String, callback: String, amountSats: Long, memo: String) =
|
||||||
receiveVm.executeWithdraw(k1, callback, amountSats, memo)
|
receiveVm.executeWithdraw(k1, callback, amountSats, memo)
|
||||||
val sendState: StateFlow<SendState> get() = sendVm.sendState
|
val sendState: StateFlow<SendState> get() = sendVm.sendState
|
||||||
|
fun refreshBalance() = balanceVm.refreshBalance()
|
||||||
fun scan(rawInput: String) = sendVm.scan(rawInput)
|
fun scan(rawInput: String) = sendVm.scan(rawInput)
|
||||||
fun payBolt11(bolt11: String) = sendVm.payBolt11(bolt11)
|
fun payBolt11(bolt11: String) = sendVm.payBolt11(bolt11)
|
||||||
fun payLnurl(rawRes: LnurlScanResponse, lnurl: String, amountMsat: Long, comment: String?) =
|
fun payLnurl(rawRes: LnurlScanResponse, lnurl: String, amountMsat: Long, comment: String?) =
|
||||||
@@ -62,12 +60,9 @@ class WalletViewModel(
|
|||||||
fun requestPayment(activity: FragmentActivity, subtitle: String, pay: () -> Unit) =
|
fun requestPayment(activity: FragmentActivity, subtitle: String, pay: () -> Unit) =
|
||||||
sendVm.requestPayment(activity, subtitle, pay)
|
sendVm.requestPayment(activity, subtitle, pay)
|
||||||
fun resetSendState() = sendVm.resetSendState()
|
fun resetSendState() = sendVm.resetSendState()
|
||||||
|
val clipboardOfferState: StateFlow<ClipboardOfferState> get() = clipboardVm.clipboardOfferState
|
||||||
// ── Other states ──────────────────────────────────────────────────────────
|
fun checkClipboard(text: String?) = clipboardVm.checkClipboard(text)
|
||||||
private val _lightningAddress = MutableStateFlow<String?>(balancePrefs.lightningAddress)
|
fun dismissClipboardOffer() = clipboardVm.dismissClipboardOffer()
|
||||||
val lightningAddress: StateFlow<String?> = _lightningAddress
|
|
||||||
|
|
||||||
// ── Init ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
observePaymentEvents()
|
observePaymentEvents()
|
||||||
@@ -91,7 +86,7 @@ class WalletViewModel(
|
|||||||
.onSuccess { address ->
|
.onSuccess { address ->
|
||||||
if (address != null) {
|
if (address != null) {
|
||||||
Log.d(TAG, "LNADDR [FETCH OK ] $address")
|
Log.d(TAG, "LNADDR [FETCH OK ] $address")
|
||||||
_lightningAddress.value = address
|
lightningAddress.value = address
|
||||||
balancePrefs.setLightningAddress(address)
|
balancePrefs.setLightningAddress(address)
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "LNADDR [FETCH OK ] no pay link / username found")
|
Log.d(TAG, "LNADDR [FETCH OK ] no pay link / username found")
|
||||||
@@ -103,47 +98,4 @@ class WalletViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Clipboard offer ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private var lastOfferedClipboard: String? = null
|
|
||||||
|
|
||||||
private val _clipboardOfferState = MutableStateFlow<ClipboardOfferState>(ClipboardOfferState.None)
|
|
||||||
val clipboardOfferState: StateFlow<ClipboardOfferState> = _clipboardOfferState
|
|
||||||
|
|
||||||
fun checkClipboard(text: String?) {
|
|
||||||
Log.d("Clipboard", "checkClipboard called with: $text")
|
|
||||||
val trimmed = text?.trim().takeIf { !it.isNullOrBlank() } ?: return
|
|
||||||
|
|
||||||
// ── Deduplicate: don't re-offer the same string twice ─────────────────
|
|
||||||
if (trimmed == lastOfferedClipboard) return
|
|
||||||
|
|
||||||
// ── Guard: ignore our own invoice ─────────────────────────────────────
|
|
||||||
val ownInvoice = (receiveState.value as? ReceiveState.InvoiceReady)?.bolt11
|
|
||||||
if (ownInvoice != null && trimmed.equals(ownInvoice, ignoreCase = true)) {
|
|
||||||
Log.d("Clipboard", "Skipping own invoice")
|
|
||||||
lastOfferedClipboard = trimmed // still mark as seen so focus re-checks don't re-trigger
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Guard: ignore our own lightning address ────────────────────────────
|
|
||||||
val ownAddress = _lightningAddress.value
|
|
||||||
if (ownAddress != null && trimmed.equals(ownAddress, ignoreCase = true)) {
|
|
||||||
Log.d("Clipboard", "Skipping own lightning address")
|
|
||||||
lastOfferedClipboard = trimmed
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val type = SendInputDetector.detect(trimmed)
|
|
||||||
if (type == SendInputType.Unknown) return
|
|
||||||
|
|
||||||
lastOfferedClipboard = trimmed
|
|
||||||
_clipboardOfferState.value = ClipboardOfferState.Detected(raw = trimmed, inputType = type)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dismissClipboardOffer() {
|
|
||||||
_clipboardOfferState.value = ClipboardOfferState.None
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshBalance() = balanceVm.refreshBalance()
|
|
||||||
}
|
}
|
||||||
@@ -13,9 +13,11 @@ import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
|||||||
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
|
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.clipboard.ClipboardViewModel
|
||||||
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.ui.receive.ReceiveViewModel
|
||||||
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
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 {
|
||||||
@@ -41,6 +43,14 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
|||||||
receiveVm.handleWithdrawResponse(raw, lnurl)
|
receiveVm.handleWithdrawResponse(raw, lnurl)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val lightningAddress = MutableStateFlow<String?>(balancePrefs.lightningAddress)
|
||||||
|
|
||||||
|
val clipboardVm = ClipboardViewModel(
|
||||||
|
ownInvoice = { (receiveVm.receiveState.value as? ReceiveState.InvoiceReady)?.bolt11 },
|
||||||
|
ownAddress = { lightningAddress.value }
|
||||||
|
)
|
||||||
|
|
||||||
balanceVm.onBalanceRefreshed = { fiatVm.onBalanceRefreshed() }
|
balanceVm.onBalanceRefreshed = { fiatVm.onBalanceRefreshed() }
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@@ -53,7 +63,9 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
|||||||
balanceVm = balanceVm,
|
balanceVm = balanceVm,
|
||||||
fiatVm = fiatVm,
|
fiatVm = fiatVm,
|
||||||
receiveVm = receiveVm,
|
receiveVm = receiveVm,
|
||||||
sendVm = sendVm
|
sendVm = sendVm,
|
||||||
|
clipboardVm = clipboardVm,
|
||||||
|
lightningAddress = lightningAddress
|
||||||
) as T
|
) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.bitcointxoko.gudariwallet.ui.clipboard
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.ClipboardOfferState
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.ReceiveState
|
||||||
|
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
||||||
|
import com.bitcointxoko.gudariwallet.util.SendInputType
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow as SF
|
||||||
|
|
||||||
|
private const val TAG = "ClipboardViewModel"
|
||||||
|
|
||||||
|
class ClipboardViewModel(
|
||||||
|
// Lambdas instead of direct VM references — keeps ClipboardViewModel
|
||||||
|
// independently testable with no peer ViewModel dependencies.
|
||||||
|
private val ownInvoice : () -> String?, // from ReceiveViewModel
|
||||||
|
private val ownAddress : () -> String?, // from WalletViewModel._lightningAddress
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private var lastOfferedClipboard: String? = null
|
||||||
|
|
||||||
|
private val _clipboardOfferState = MutableStateFlow<ClipboardOfferState>(ClipboardOfferState.None)
|
||||||
|
val clipboardOfferState: StateFlow<ClipboardOfferState> = _clipboardOfferState
|
||||||
|
|
||||||
|
fun checkClipboard(text: String?) {
|
||||||
|
Log.d(TAG, "checkClipboard called with: $text")
|
||||||
|
val trimmed = text?.trim().takeIf { !it.isNullOrBlank() } ?: return
|
||||||
|
|
||||||
|
// ── Deduplicate: don't re-offer the same string twice ─────────────────
|
||||||
|
if (trimmed == lastOfferedClipboard) return
|
||||||
|
|
||||||
|
// ── Guard: ignore our own invoice ──────────────────────────────────────
|
||||||
|
val invoice = ownInvoice()
|
||||||
|
if (invoice != null && trimmed.equals(invoice, ignoreCase = true)) {
|
||||||
|
Log.d(TAG, "Skipping own invoice")
|
||||||
|
lastOfferedClipboard = trimmed // mark as seen so re-focus doesn't re-trigger
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Guard: ignore our own lightning address ────────────────────────────
|
||||||
|
val address = ownAddress()
|
||||||
|
if (address != null && trimmed.equals(address, ignoreCase = true)) {
|
||||||
|
Log.d(TAG, "Skipping own lightning address")
|
||||||
|
lastOfferedClipboard = trimmed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val type = SendInputDetector.detect(trimmed)
|
||||||
|
if (type == SendInputType.Unknown) return
|
||||||
|
|
||||||
|
lastOfferedClipboard = trimmed
|
||||||
|
_clipboardOfferState.value = ClipboardOfferState.Detected(raw = trimmed, inputType = type)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissClipboardOffer() {
|
||||||
|
_clipboardOfferState.value = ClipboardOfferState.None
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user