Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-13 10:02:33 +02:00
parent 6fdbff6fa8
commit 2091e5eecd
3 changed files with 118 additions and 0 deletions

View file

@ -0,0 +1,75 @@
package com.tangem.common.routing.deeplink
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.DeepLinkRoute
import com.tangem.common.routing.DeepLinkScheme
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.onramp.model.OnrampSource
import java.net.URI
/**
* Classification of a marketing-banner deeplink used to decide how a banner tap is routed.
*
* Only the `tangem://` scheme with a swap/buy host triggers contextual in-app routing; everything
* else (external `https://` T&S links, unknown hosts, malformed input) is [EXTERNAL] and handed off
* to the generic deeplink launcher. This mirrors iOS `DefaultIncomingLinkParser`, where
* `https://tangem.com/...` links always resolve to an external link, not an in-app destination.
*/
enum class MarketingDeeplink {
/** `tangem://swap` — open swap for the current token. */
SWAP,
/** `tangem://buy` — open onramp for the current token. */
BUY,
/** External or unrecognized link — route through the generic deeplink launcher. */
EXTERNAL,
}
// TODO: [temporary] Banner taps are intercepted in-host and mapped to an AppRoute directly because the
// shared tangem://swap and tangem://buy deeplinks open context-less screens (generic swap / BuyCrypto
// hub) with no current-token prefill. Replace with targeted swap/onramp deeplinks and drop this
// interception: [REDACTED_JIRA]
/**
* Resolves a marketing-banner [link] into a [MarketingDeeplink]. Never throws: malformed input
* degrades to [MarketingDeeplink.EXTERNAL].
*/
fun resolveMarketingDeeplink(link: String): MarketingDeeplink {
val uri = runCatching { URI(link) }.getOrNull() ?: return MarketingDeeplink.EXTERNAL
if (!uri.scheme.equals(DeepLinkScheme.Tangem.scheme, ignoreCase = true)) {
return MarketingDeeplink.EXTERNAL
}
return when (uri.host) {
DeepLinkRoute.Swap.host -> MarketingDeeplink.SWAP
DeepLinkRoute.Buy.host -> MarketingDeeplink.BUY
else -> MarketingDeeplink.EXTERNAL
}
}
/**
* Builds the contextual in-app route for a marketing-banner deeplink on a token-scoped screen (staking,
* yield, swap, onramp): swap for the current token, or onramp to buy it. Returns `null` for
* [MarketingDeeplink.EXTERNAL] so the caller falls back to the generic deeplink launcher.
*/
fun MarketingDeeplink.toContextualRoute(
userWalletId: UserWalletId,
currency: CryptoCurrency,
screenSource: AnalyticsParam.ScreensSources,
onrampSource: OnrampSource = OnrampSource.MARKETING_BANNER,
): AppRoute? = when (this) {
MarketingDeeplink.SWAP -> AppRoute.Swap(
userWalletId = userWalletId,
fromCryptoCurrency = currency,
screenSource = screenSource.value,
)
MarketingDeeplink.BUY -> AppRoute.Onramp(
userWalletId = userWalletId,
currency = currency,
source = onrampSource,
)
MarketingDeeplink.EXTERNAL -> null
}

View file

@ -0,0 +1,42 @@
package com.tangem.common.routing.deeplink
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class MarketingDeeplinkTest {
@ParameterizedTest
@MethodSource("provideTestModels")
fun resolveMarketingDeeplink(model: ResolveModel) {
// Act
val actual = resolveMarketingDeeplink(model.link)
// Assert
assertThat(actual).isEqualTo(model.expected)
}
internal data class ResolveModel(val link: String, val expected: MarketingDeeplink)
private fun provideTestModels() = listOf(
// tangem:// swap/buy -> contextual
ResolveModel(link = "tangem://swap", expected = MarketingDeeplink.SWAP),
ResolveModel(link = "tangem://buy", expected = MarketingDeeplink.BUY),
ResolveModel(link = "tangem://swap?foo=bar", expected = MarketingDeeplink.SWAP),
ResolveModel(link = "tangem://buy/extra", expected = MarketingDeeplink.BUY),
ResolveModel(link = "TANGEM://swap", expected = MarketingDeeplink.SWAP),
// tangem:// other hosts -> external
ResolveModel(link = "tangem://token", expected = MarketingDeeplink.EXTERNAL),
ResolveModel(link = "tangem://promo", expected = MarketingDeeplink.EXTERNAL),
// https T&S links -> external (iOS treats these as .link too)
ResolveModel(link = "https://tangem.com/swap", expected = MarketingDeeplink.EXTERNAL),
ResolveModel(link = "https://tangem.com/buy", expected = MarketingDeeplink.EXTERNAL),
ResolveModel(link = "https://tangem.com/promo/summer", expected = MarketingDeeplink.EXTERNAL),
// other schemes / garbage -> external
ResolveModel(link = "wc://connect", expected = MarketingDeeplink.EXTERNAL),
ResolveModel(link = "not a uri", expected = MarketingDeeplink.EXTERNAL),
ResolveModel(link = "", expected = MarketingDeeplink.EXTERNAL),
)
}