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.