diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt index ba6ed06..206c5e8 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt @@ -4,12 +4,9 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.bitcointxoko.gudariwallet.api.LnurlScanResponse -import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository import com.bitcointxoko.gudariwallet.data.NodeAliasRepository import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository 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.SharedFlow import kotlinx.coroutines.flow.StateFlow @@ -17,6 +14,7 @@ import kotlinx.coroutines.launch import androidx.fragment.app.FragmentActivity import com.bitcointxoko.gudariwallet.data.BalancePrefsStore 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.receive.ReceiveViewModel import com.bitcointxoko.gudariwallet.ui.send.SendViewModel @@ -26,16 +24,15 @@ private const val TAG = "WalletViewModel" class WalletViewModel( val repo : WalletRepository, val aliasRepo : NodeAliasRepository, - val lnurlCache: LnurlCacheRepository, val paymentCache: PaymentCacheRepository, private val balancePrefs: BalancePrefsStore, val balanceVm : BalanceViewModel, val fiatVm : FiatViewModel, val receiveVm : ReceiveViewModel, val sendVm : SendViewModel, + val clipboardVm : ClipboardViewModel, + val lightningAddress: MutableStateFlow, ) : ViewModel() { - - // ── Balance state ───────────────────────────────────────────────────────── val balanceState: StateFlow get() = balanceVm.balanceState val balanceHidden: StateFlow get() = fiatVm.balanceHidden val selectedCurrency: StateFlow get() = fiatVm.selectedCurrency @@ -52,6 +49,7 @@ class WalletViewModel( fun executeWithdraw(k1: String, callback: String, amountSats: Long, memo: String) = receiveVm.executeWithdraw(k1, callback, amountSats, memo) val sendState: StateFlow get() = sendVm.sendState + fun refreshBalance() = balanceVm.refreshBalance() fun scan(rawInput: String) = sendVm.scan(rawInput) fun payBolt11(bolt11: String) = sendVm.payBolt11(bolt11) fun payLnurl(rawRes: LnurlScanResponse, lnurl: String, amountMsat: Long, comment: String?) = @@ -62,12 +60,9 @@ class WalletViewModel( fun requestPayment(activity: FragmentActivity, subtitle: String, pay: () -> Unit) = sendVm.requestPayment(activity, subtitle, pay) fun resetSendState() = sendVm.resetSendState() - - // ── Other states ────────────────────────────────────────────────────────── - private val _lightningAddress = MutableStateFlow(balancePrefs.lightningAddress) - val lightningAddress: StateFlow = _lightningAddress - - // ── Init ────────────────────────────────────────────────────────────────── + val clipboardOfferState: StateFlow get() = clipboardVm.clipboardOfferState + fun checkClipboard(text: String?) = clipboardVm.checkClipboard(text) + fun dismissClipboardOffer() = clipboardVm.dismissClipboardOffer() init { observePaymentEvents() @@ -91,7 +86,7 @@ class WalletViewModel( .onSuccess { address -> if (address != null) { Log.d(TAG, "LNADDR [FETCH OK ] $address") - _lightningAddress.value = address + lightningAddress.value = address balancePrefs.setLightningAddress(address) } else { 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.None) - val clipboardOfferState: StateFlow = _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() } \ No newline at end of file diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModelFactory.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModelFactory.kt index 03c7e55..cce4fa8 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModelFactory.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModelFactory.kt @@ -13,9 +13,11 @@ import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore 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.receive.ReceiveViewModel import com.bitcointxoko.gudariwallet.ui.send.SendViewModel +import kotlinx.coroutines.flow.MutableStateFlow class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @@ -41,6 +43,14 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F receiveVm.handleWithdrawResponse(raw, lnurl) } ) + + val lightningAddress = MutableStateFlow(balancePrefs.lightningAddress) + + val clipboardVm = ClipboardViewModel( + ownInvoice = { (receiveVm.receiveState.value as? ReceiveState.InvoiceReady)?.bolt11 }, + ownAddress = { lightningAddress.value } + ) + balanceVm.onBalanceRefreshed = { fiatVm.onBalanceRefreshed() } @Suppress("UNCHECKED_CAST") @@ -53,7 +63,9 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F balanceVm = balanceVm, fiatVm = fiatVm, receiveVm = receiveVm, - sendVm = sendVm + sendVm = sendVm, + clipboardVm = clipboardVm, + lightningAddress = lightningAddress ) as T } } \ No newline at end of file diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/clipboard/ClipboardViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/clipboard/ClipboardViewModel.kt new file mode 100644 index 0000000..3c91375 --- /dev/null +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/clipboard/ClipboardViewModel.kt @@ -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.None) + val clipboardOfferState: StateFlow = _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 + } +}