mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-10-20 18:56:25 +00:00
fix(deeplink): stop links that fail for good from replaying on every launch (#27)
* test(deeplink): pin terminal-failure replay and referendum path matching These fail on main: a link that fails for good is kept as pending and replayed on every launch, and /open/governance matches the referendum handler. * fix(deeplink): drop a pending link once it fails for good RootDeepLinkHandler cleared the persisted pending link only on success or when no handler matched. A link that reached a handler and failed with a DeepLinkHandlingException stayed in preferences and was replayed on every launch. Clear it on terminal handling failures in both the fresh and pending paths; this also recovers installs already stuck on update. Other failures keep the link pending for a retry. * fix(deeplink): match the referendum path exactly ReferendumDeepLinkHandler matched any path starting with /open/gov, so the governance banner link /open/governance went to the referendum screen and failed with ReferendumIsNotSpecified.
This commit is contained in:
1 parent
5a6eb62a1b
commit
2d5d8bec49
6 files changed
+103
-1
No files matched your search
@@ -31,6 +31,8 @@ dependencies {
|
|||||||
implementation daggerDep
|
implementation daggerDep
|
||||||
ksp daggerCompiler
|
ksp daggerCompiler
|
||||||
|
|
||||||
|
testImplementation project(':test-shared')
|
||||||
|
|
||||||
androidTestImplementation androidTestRunnerDep
|
androidTestImplementation androidTestRunnerDep
|
||||||
androidTestImplementation androidTestRulesDep
|
androidTestImplementation androidTestRulesDep
|
||||||
androidTestImplementation androidJunitDep
|
androidTestImplementation androidJunitDep
|
||||||
|
|||||||
+7
@@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_deep_linking.presentation.handling
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import io.novafoundation.nova.common.utils.onFailureInstance
|
import io.novafoundation.nova.common.utils.onFailureInstance
|
||||||
|
import io.novafoundation.nova.feature_deep_linking.presentation.handling.common.DeepLinkHandlingException
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
|
|
||||||
@@ -23,12 +24,18 @@ class RootDeepLinkHandler(
|
|||||||
|
|
||||||
return handleDeepLinkInternal(pendingDeepLink)
|
return handleDeepLinkInternal(pendingDeepLink)
|
||||||
.onSuccess { pendingDeepLinkProvider.clear() }
|
.onSuccess { pendingDeepLinkProvider.clear() }
|
||||||
|
// A pending link that reaches a handler but fails with a terminal error (malformed/unresolvable),
|
||||||
|
// or matches no handler, will never succeed on retry - clear it so it does not replay on every launch.
|
||||||
|
.onFailureInstance<DeepLinkHandlingException, Unit> { pendingDeepLinkProvider.clear() }
|
||||||
|
.onFailureInstance<HandlerNotFoundException, Unit> { pendingDeepLinkProvider.clear() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun handleDeepLink(data: Uri): Result<Unit> {
|
override suspend fun handleDeepLink(data: Uri): Result<Unit> {
|
||||||
pendingDeepLinkProvider.save(data)
|
pendingDeepLinkProvider.save(data)
|
||||||
return handleDeepLinkInternal(data)
|
return handleDeepLinkInternal(data)
|
||||||
.onSuccess { pendingDeepLinkProvider.clear() }
|
.onSuccess { pendingDeepLinkProvider.clear() }
|
||||||
|
// Same as above: a terminal handling failure must not be persisted, otherwise it bricks every launch.
|
||||||
|
.onFailureInstance<DeepLinkHandlingException, Unit> { pendingDeepLinkProvider.clear() }
|
||||||
.onFailureInstance<HandlerNotFoundException, Unit> { pendingDeepLinkProvider.clear() } // If we haven't find any handler - no need to save deep link
|
.onFailureInstance<HandlerNotFoundException, Unit> { pendingDeepLinkProvider.clear() } // If we haven't find any handler - no need to save deep link
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
package io.novafoundation.nova.feature_deep_linking.presentation.handling
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import io.novafoundation.nova.feature_deep_linking.presentation.handling.common.DeepLinkHandlingException.ReferendumHandlingException
|
||||||
|
import io.novafoundation.nova.test_shared.whenever
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.never
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
|
||||||
|
class RootDeepLinkHandlerTest {
|
||||||
|
|
||||||
|
private val link = mock(Uri::class.java)
|
||||||
|
private val pendingDeepLinkProvider = mock(PendingDeepLinkProvider::class.java)
|
||||||
|
|
||||||
|
private class FailingHandler(private val error: Throwable) : DeepLinkHandler {
|
||||||
|
override val callbackFlow: Flow<CallbackEvent> = emptyFlow()
|
||||||
|
override suspend fun matches(data: Uri) = true
|
||||||
|
override suspend fun handleDeepLink(data: Uri): Result<Unit> = Result.failure(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rootWith(error: Throwable) = RootDeepLinkHandler(pendingDeepLinkProvider, listOf(FailingHandler(error)))
|
||||||
|
|
||||||
|
// A link that reaches a handler and fails for good was kept as pending and replayed on every
|
||||||
|
// launch, showing "Referendum not found" each time the app opened.
|
||||||
|
@Test
|
||||||
|
fun `a fresh link that fails for good is not kept as pending`() = runBlocking {
|
||||||
|
rootWith(ReferendumHandlingException.ReferendumIsNotSpecified).handleDeepLink(link)
|
||||||
|
|
||||||
|
verify(pendingDeepLinkProvider).clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a pending link that fails for good is dropped`() = runBlocking {
|
||||||
|
whenever(pendingDeepLinkProvider.get()).thenReturn(link)
|
||||||
|
|
||||||
|
rootWith(ReferendumHandlingException.ReferendumIsNotSpecified).checkAndHandlePendingDeepLink()
|
||||||
|
|
||||||
|
verify(pendingDeepLinkProvider).clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a link that fails for another reason stays pending for a retry`() = runBlocking {
|
||||||
|
rootWith(IllegalStateException("not ready yet")).handleDeepLink(link)
|
||||||
|
|
||||||
|
verify(pendingDeepLinkProvider, never()).clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,4 +65,5 @@ dependencies {
|
|||||||
|
|
||||||
testImplementation jUnitDep
|
testImplementation jUnitDep
|
||||||
testImplementation mockitoDep
|
testImplementation mockitoDep
|
||||||
|
testImplementation project(':test-shared')
|
||||||
}
|
}
|
||||||
+3
-1
@@ -31,7 +31,9 @@ class ReferendumDeepLinkHandler(
|
|||||||
override suspend fun matches(data: Uri): Boolean {
|
override suspend fun matches(data: Uri): Boolean {
|
||||||
val path = data.path ?: return false
|
val path = data.path ?: return false
|
||||||
|
|
||||||
return path.startsWith(ReferendumDetailsDeepLinkConfigurator.PREFIX)
|
// Exact match, not startsWith: otherwise "/open/governance" (and any "/open/gov*") would wrongly
|
||||||
|
// match the referendum prefix "/open/gov" and fail with ReferendumIsNotSpecified.
|
||||||
|
return path == ReferendumDetailsDeepLinkConfigurator.PREFIX
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun handleDeepLink(data: Uri) = runCatching {
|
override suspend fun handleDeepLink(data: Uri) = runCatching {
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package io.novafoundation.nova.feature_governance_impl.presentation.referenda.details.deeplink
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||||
|
import io.novafoundation.nova.feature_governance_api.data.MutableGovernanceState
|
||||||
|
import io.novafoundation.nova.feature_governance_impl.presentation.GovernanceRouter
|
||||||
|
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||||
|
import io.novafoundation.nova.test_shared.whenever
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
|
||||||
|
class ReferendumDeepLinkHandlerTest {
|
||||||
|
|
||||||
|
private val handler = ReferendumDeepLinkHandler(
|
||||||
|
router = mock(GovernanceRouter::class.java),
|
||||||
|
chainRegistry = mock(ChainRegistry::class.java),
|
||||||
|
mutableGovernanceState = mock(MutableGovernanceState::class.java),
|
||||||
|
automaticInteractionGate = mock(AutomaticInteractionGate::class.java)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun linkWithPath(path: String): Uri = mock(Uri::class.java).also {
|
||||||
|
whenever(it.path).thenReturn(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `matches a referendum link`() = runBlocking {
|
||||||
|
assertTrue(handler.matches(linkWithPath("/open/gov")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// "/open/governance" starts with the referendum prefix "/open/gov"; matching it sent the
|
||||||
|
// governance banner link to the referendum screen, which failed without a referendum id.
|
||||||
|
@Test
|
||||||
|
fun `does not match a link that only starts with the referendum path`() = runBlocking {
|
||||||
|
assertFalse(handler.matches(linkWithPath("/open/governance")))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user