Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@

<data android:scheme="https" android:host="gemwallet.com" android:path="/join" />
<data android:scheme="https" android:host="gemwallet.com" android:pathPrefix="/join/" />
<data android:scheme="https" android:host="gemwallet.com" android:path="/tokens" />
<data android:scheme="https" android:host="gemwallet.com" android:pathPrefix="/tokens/" />
</intent-filter>
</activity>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import uniffi.gemstone.UrlAction
import uniffi.gemstone.WalletConnectLink
import uniffi.gemstone.urlAction
import javax.inject.Inject

internal sealed interface PendingNavigation {
Expand Down Expand Up @@ -37,19 +40,23 @@ class PendingNavigationCoordinator @Inject constructor(
val pendingIntent = (_pendingNavigation.value as? PendingNavigation.RawIntent)?.intent ?: return
val uri = pendingIntent.dataString

uri?.toWalletConnectLink()?.let { link ->
when (link) {
is WalletConnectLink.Pairing -> walletConnect.onPairing(link.uri)
WalletConnectLink.Request -> walletConnect.onRequest()
WalletConnectLink.Session -> Unit
when (val action = uri?.let(::urlAction)) {
is UrlAction.WalletConnect -> {
when (val link = action.link) {
is WalletConnectLink.Connect -> walletConnect.onPairing(link.uri)
WalletConnectLink.Request -> walletConnect.onRequest()
is WalletConnectLink.Session -> Unit
}
replace(pendingIntent, replacement = null)
return
}
replace(pendingIntent, replacement = null)
return
}

uri?.toWebDeepLinkRoute()?.let { route ->
replace(pendingIntent, PendingNavigation.Route(route))
return
is UrlAction.Deeplink -> {
action.deeplink.toRoute()?.let { route ->
replace(pendingIntent, PendingNavigation.Route(route))
return
}
}
null -> Unit
}

if (!pendingIntent.hasNotificationPayload()) {
Expand Down

This file was deleted.

This file was deleted.

44 changes: 6 additions & 38 deletions android/app/src/main/kotlin/com/gemwallet/android/WebDeepLinks.kt
Original file line number Diff line number Diff line change
@@ -1,47 +1,15 @@
package com.gemwallet.android

import androidx.navigation3.runtime.NavKey
import com.gemwallet.android.features.asset_select.presents.navigation.AssetsSearchRoute
import com.gemwallet.android.ext.toAssetId
import com.gemwallet.android.ui.navigation.routes.AssetRoute
import com.gemwallet.android.ui.navigation.routes.ReferralRoute
import com.wallet.core.primitives.AssetId
import com.wallet.core.primitives.Chain
import java.net.URI
import uniffi.gemstone.Deeplink

internal fun String.toWebDeepLinkRoute(): NavKey? {
val uri = runCatching { URI(this) }.getOrNull() ?: return null
if (!WebDeepLinkScheme.equals(uri.scheme, ignoreCase = true)) return null
if (!WebDeepLinkHost.equals(uri.host, ignoreCase = true)) return null

val segments = uri.pathSegments()
return when (segments.firstOrNull()) {
WebDeepLinkPathJoin -> ReferralRoute(
code = segments.elementAtOrNull(1)
?: uri.rawQuery
.decodeUriQueryParameters()[WebDeepLinkCodeQuery]
?.takeIf(String::isNotBlank),
)
WebDeepLinkPathTokens -> segments.toTokenRoute()
internal fun Deeplink.toRoute(): NavKey? {
return when (this) {
is Deeplink.Asset -> assetId.toAssetId()?.let { AssetRoute(it) }
is Deeplink.Rewards -> ReferralRoute(code = code?.takeIf(String::isNotBlank))
else -> null
}
}

private fun List<String>.toTokenRoute(): NavKey? {
if (size == 1) return AssetsSearchRoute
if (size !in 2..3) return null
val chain = Chain.entries.firstOrNull { it.string == this[1] } ?: return null
return AssetRoute(AssetId(chain = chain, tokenId = elementAtOrNull(2)))
}

private fun URI.pathSegments(): List<String> {
return rawPath
?.split("/")
?.mapNotNull { it.takeIf(String::isNotBlank)?.decodeUriComponent() }
.orEmpty()
}

private const val WebDeepLinkScheme = "https"
private const val WebDeepLinkHost = "gemwallet.com"
private const val WebDeepLinkPathJoin = "join"
private const val WebDeepLinkPathTokens = "tokens"
private const val WebDeepLinkCodeQuery = "code"
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,30 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import uniffi.gemstone.Deeplink
import uniffi.gemstone.UrlAction
import uniffi.gemstone.WalletConnectLink
import uniffi.gemstone.urlAction

class PendingNavigationCoordinatorTest {

private val notificationNavigation = mockk<NotificationNavigation>(relaxed = true)
private val coordinator = PendingNavigationCoordinator(notificationNavigation)

@Before
fun setUp() = mockkStatic("uniffi.gemstone.GemstoneKt")

@After
fun tearDown() = unmockkStatic("uniffi.gemstone.GemstoneKt")

@Test
fun resolve_withoutPendingIntent_isNoOp() = runTest {
coordinator.resolve(NoOpWalletConnect)
Expand All @@ -27,18 +41,22 @@ class PendingNavigationCoordinatorTest {
@Test
fun resolve_walletConnectPairing_invokesPairingHandlerAndClears() = runTest {
val handler = RecordingWalletConnect()
coordinator.setPendingIntentForTest(intent(uri = "wc:abc@2?relay-protocol=irn"))
val uri = "wc:abc@2?relay-protocol=irn"
every { urlAction(uri) } returns UrlAction.WalletConnect(WalletConnectLink.Connect(uri))
coordinator.setPendingIntentForTest(intent(uri = uri))

coordinator.resolve(handler)

assertEquals(listOf("pairing:wc:abc@2?relay-protocol=irn"), handler.events)
assertEquals(listOf("pairing:$uri"), handler.events)
assertNull("intent must be cleared after handing off to wallet connect", coordinator.pendingNavigation.value)
}

@Test
fun resolve_walletConnectRequest_invokesRequestHandlerAndClears() = runTest {
val handler = RecordingWalletConnect()
coordinator.setPendingIntentForTest(intent(uri = "gem://wc?requestId=42"))
val uri = "gem://wc?requestId=42"
every { urlAction(uri) } returns UrlAction.WalletConnect(WalletConnectLink.Request)
coordinator.setPendingIntentForTest(intent(uri = uri))

coordinator.resolve(handler)

Expand All @@ -48,7 +66,9 @@ class PendingNavigationCoordinatorTest {

@Test
fun resolve_webDeepLink_storesRoute() = runTest {
coordinator.setPendingIntentForTest(intent(uri = "https://gemwallet.com/join/gemcoder"))
val uri = "https://gemwallet.com/join/gemcoder"
every { urlAction(uri) } returns UrlAction.Deeplink(Deeplink.Rewards(code = "gemcoder"))
coordinator.setPendingIntentForTest(intent(uri = uri))

coordinator.resolve(NoOpWalletConnect)

Expand All @@ -58,7 +78,9 @@ class PendingNavigationCoordinatorTest {

@Test
fun resolve_unknownIntentWithoutNotificationPayload_clears() = runTest {
coordinator.setPendingIntentForTest(intent(uri = "https://example.com/unknown"))
val uri = "https://example.com/unknown"
every { urlAction(uri) } returns null
coordinator.setPendingIntentForTest(intent(uri = uri))

coordinator.resolve(NoOpWalletConnect)

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,49 +1,27 @@
package com.gemwallet.android

import com.gemwallet.android.features.asset_select.presents.navigation.AssetsSearchRoute
import com.gemwallet.android.testkit.mockAssetId
import com.gemwallet.android.ui.navigation.routes.AssetRoute
import com.gemwallet.android.ui.navigation.routes.ReferralRoute
import com.wallet.core.primitives.Chain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import uniffi.gemstone.Deeplink

class WebDeepLinksTest {

@Test
fun webDeepLinkRoute_acceptsJoinLinks() {
assertEquals(
ReferralRoute(code = "gemcoder"),
"https://gemwallet.com/join/gemcoder".toWebDeepLinkRoute(),
)
assertEquals(
ReferralRoute(code = "gemcoder"),
"https://gemwallet.com/join?code=gemcoder".toWebDeepLinkRoute(),
)
assertEquals(ReferralRoute(), "https://gemwallet.com/join".toWebDeepLinkRoute())
fun toRoute_mapsSupportedDeeplinks() {
val tokenId = "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"
assertEquals(AssetRoute(mockAssetId(Chain.Bitcoin)), Deeplink.Asset(assetId = "bitcoin").toRoute())
assertEquals(AssetRoute(mockAssetId(Chain.Solana, tokenId)), Deeplink.Asset(assetId = "solana_$tokenId").toRoute())
assertEquals(ReferralRoute(code = "gemcoder"), Deeplink.Rewards(code = "gemcoder").toRoute())
assertEquals(ReferralRoute(), Deeplink.Rewards(code = null).toRoute())
}

@Test
fun webDeepLinkRoute_acceptsTokenLinks() {
assertEquals(AssetsSearchRoute, "https://gemwallet.com/tokens".toWebDeepLinkRoute())
assertEquals(
AssetRoute(mockAssetId(Chain.Bitcoin)),
"https://gemwallet.com/tokens/bitcoin".toWebDeepLinkRoute(),
)
assertEquals(
AssetRoute(mockAssetId(Chain.Solana, tokenId = "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN")),
"https://gemwallet.com/tokens/solana/JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN".toWebDeepLinkRoute(),
)
}

@Test
fun webDeepLinkRoute_rejectsUnsupportedLinks() {
assertNull("https://gemwallet.com/swap/bitcoin".toWebDeepLinkRoute())
assertNull("https://gemwallet.com/en/tokens/bitcoin".toWebDeepLinkRoute())
assertNull("https://example.com/tokens/bitcoin".toWebDeepLinkRoute())
assertNull("gem://tokens/bitcoin".toWebDeepLinkRoute())
assertNull("https://gemwallet.com/tokens/notachain".toWebDeepLinkRoute())
assertNull("https://gemwallet.com/tokens/bitcoin/too/many".toWebDeepLinkRoute())
fun toRoute_rejectsUnsupportedLinks() {
assertNull(Deeplink.Perpetuals.toRoute())
}
}
Loading