diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 52784aaf5..466b64fdb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -133,7 +133,6 @@
-
diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt
index 8336b7ade..0161dd331 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 d9c7c91c3..317da179d 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. */
+ 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 66f2f29eb..a8abb8991 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 {
diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
index 0775139d8..b2594b183 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)
}
}
diff --git a/changelog.d/next/1054.fixed.md b/changelog.d/next/1054.fixed.md
new file mode 100644
index 000000000..0d7330c0f
--- /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.