From 692d8d10fc8b42d53d14fc9e2aecbdc40326e9ae Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 29 Jun 2026 16:46:11 -0300 Subject: [PATCH 1/4] fix: BROWSABLE category from NFC intent filter causing play console false warning --- app/src/main/AndroidManifest.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 52784aaf53..466b64fdb9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -133,7 +133,6 @@ - From 99f7d405621599b71838a115a4fc304d7e1a4864 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 14 Jul 2026 14:11:03 -0300 Subject: [PATCH 2/4] fix: don't drop ACTION_NDEF_DISCOVERED --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 6 +++++- .../viewmodels/AppViewModelSendFlowTest.kt | 21 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index d9c7c91c3c..5a1b693277 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -3,6 +3,7 @@ package to.bitkit.viewmodels import android.content.Context import android.content.Intent import android.net.Uri +import android.nfc.NfcAdapter import androidx.annotation.StringRes import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -3064,7 +3065,7 @@ class AppViewModel @Inject constructor( } fun handleDeeplinkIntent(intent: Intent) { - if (intent.action != Intent.ACTION_VIEW) return + if (intent.action !in DEEPLINK_ACTIONS) return intent.data?.let { uri -> Logger.debug("Received deeplink '${uri.toString().sanitizedDeeplinkLogValue()}'", context = TAG) processDeeplink(uri) @@ -3290,6 +3291,9 @@ class AppViewModel @Inject constructor( private const val PUBKYAUTH_SCHEME = "pubkyauth" private const val RECOVERY_MODE_DEEPLINK = "recovery-mode" private val LNURL_WITHDRAW_EXPIRY_SEC = 1.hours.inWholeSeconds.toUInt() + + /** Intent actions carrying a deeplink URI: browsers and apps send VIEW, NFC tag taps send NDEF_DISCOVERED. */ + private val DEEPLINK_ACTIONS = setOf(Intent.ACTION_VIEW, NfcAdapter.ACTION_NDEF_DISCOVERED) } } diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index 0775139d8a..b2594b1839 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -5,6 +5,7 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.net.Uri +import android.nfc.NfcAdapter import androidx.core.net.toUri import app.cash.turbine.test import com.synonym.bitkitcore.LightningInvoice @@ -497,6 +498,22 @@ class AppViewModelSendFlowTest : BaseUnitTest() { verify(lightningRepo).setRecoveryMode(true) } + @Test + fun `deeplink from NFC tag tap is processed`() = test { + sut.handleDeeplinkIntent(recoveryModeIntent(action = NfcAdapter.ACTION_NDEF_DISCOVERED)) + advanceUntilIdle() + + verify(lightningRepo).setRecoveryMode(true) + } + + @Test + fun `intent without deeplink action is ignored`() = test { + sut.handleDeeplinkIntent(recoveryModeIntent(action = Intent.ACTION_MAIN)) + advanceUntilIdle() + + verify(lightningRepo, never()).setRecoveryMode(any()) + } + @Test fun `connectBTCPay hides sheet and shows success toast`() = test { val setup = samRockSetupRequest() @@ -1400,7 +1417,7 @@ class AppViewModelSendFlowTest : BaseUnitTest() { return URLEncoder.encode(this, StandardCharsets.UTF_8.name()).replace("+", "%20") } - private fun recoveryModeIntent(): Intent { + private fun recoveryModeIntent(action: String = Intent.ACTION_VIEW): Intent { val uri = mock { on { toString() }.thenReturn("bitkit://recovery-mode") on { scheme }.thenReturn("bitkit") @@ -1408,7 +1425,7 @@ class AppViewModelSendFlowTest : BaseUnitTest() { on { pathSegments }.thenReturn(emptyList()) } return mock { - on { action }.thenReturn(Intent.ACTION_VIEW) + on { this.action }.thenReturn(action) on { data }.thenReturn(uri) } } From cbf7acfde492b6e92e0a21d980717c057b1fac2a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 14 Jul 2026 14:14:19 -0300 Subject: [PATCH 3/4] chore: add changelog fragment for nfc deeplink fix --- changelog.d/next/1054.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/next/1054.fixed.md diff --git a/changelog.d/next/1054.fixed.md b/changelog.d/next/1054.fixed.md new file mode 100644 index 0000000000..0d7330c0f5 --- /dev/null +++ b/changelog.d/next/1054.fixed.md @@ -0,0 +1 @@ +Tapping an NFC tag now opens the gift claim and other deep links instead of being ignored, and the NFC intent filter no longer declares the unused BROWSABLE category that triggered a Play Console deep link warning. From 6acefaf62773b09c33e46e3cb984e7625bcad311 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 14 Jul 2026 14:31:04 -0300 Subject: [PATCH 4/4] fix: launch key tracks NFC tag intents so they are consumed once --- app/src/main/java/to/bitkit/ui/MainActivity.kt | 2 +- .../java/to/bitkit/viewmodels/AppViewModel.kt | 2 +- .../to/bitkit/ui/MainActivityLaunchKeyTest.kt | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 8336b7aded..0161dd331e 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -297,7 +297,7 @@ class MainActivity : FragmentActivity() { internal fun Intent?.launchKey(): String? { this ?: return null return when (action) { - Intent.ACTION_VIEW -> data?.toString()?.let { + in AppViewModel.DEEPLINK_ACTIONS -> data?.toString()?.let { SamRockSetupRequest.sanitizedLaunchKey(it) ?: it } UsbManager.ACTION_USB_DEVICE_ATTACHED -> listOfNotNull(action, usbDevice()?.deviceName).joinToString(":") diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 5a1b693277..317da179db 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -3293,7 +3293,7 @@ class AppViewModel @Inject constructor( private val LNURL_WITHDRAW_EXPIRY_SEC = 1.hours.inWholeSeconds.toUInt() /** Intent actions carrying a deeplink URI: browsers and apps send VIEW, NFC tag taps send NDEF_DISCOVERED. */ - private val DEEPLINK_ACTIONS = setOf(Intent.ACTION_VIEW, NfcAdapter.ACTION_NDEF_DISCOVERED) + internal val DEEPLINK_ACTIONS = setOf(Intent.ACTION_VIEW, NfcAdapter.ACTION_NDEF_DISCOVERED) } } diff --git a/app/src/test/java/to/bitkit/ui/MainActivityLaunchKeyTest.kt b/app/src/test/java/to/bitkit/ui/MainActivityLaunchKeyTest.kt index 66f2f29ebf..a8abb89911 100644 --- a/app/src/test/java/to/bitkit/ui/MainActivityLaunchKeyTest.kt +++ b/app/src/test/java/to/bitkit/ui/MainActivityLaunchKeyTest.kt @@ -3,6 +3,7 @@ package to.bitkit.ui import android.content.Intent import android.hardware.usb.UsbManager import android.net.Uri +import android.nfc.NfcAdapter import org.junit.Test import org.mockito.kotlin.mock import java.net.URLEncoder @@ -60,6 +61,20 @@ class MainActivityLaunchKeyTest { assertNull(intent.launchKey()) } + @Test + fun `launch key tracks NFC tag intents so they are consumed once`() { + val giftUrl = "bitkit://gift-abc123-3000" + val uri = mock { + on { toString() }.thenReturn(giftUrl) + } + val intent = mock { + on { action }.thenReturn(NfcAdapter.ACTION_NDEF_DISCOVERED) + on { data }.thenReturn(uri) + } + + assertEquals(giftUrl, intent.launchKey()) + } + @Test fun `launch key tracks usb attach intents`() { val intent = mock {