From 2091e5eecdbc67f6c2c9b2d59a55491907a8c442 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Jul 2026 10:02:33 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../routing/deeplink/MarketingDeeplink.kt | 75 +++++++++++++++++++ .../routing/deeplink/MarketingDeeplinkTest.kt | 42 +++++++++++ .../domain/onramp/model/OnrampSource.kt | 1 + 3 files changed, 118 insertions(+) create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/MarketingDeeplink.kt create mode 100644 common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/MarketingDeeplinkTest.kt diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/MarketingDeeplink.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/MarketingDeeplink.kt new file mode 100644 index 0000000000..6436422a27 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/MarketingDeeplink.kt @@ -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 +} \ No newline at end of file diff --git a/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/MarketingDeeplinkTest.kt b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/MarketingDeeplinkTest.kt new file mode 100644 index 0000000000..1035d3123f --- /dev/null +++ b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/MarketingDeeplinkTest.kt @@ -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), + ) +} \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampSource.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampSource.kt index 86b72907ad..3e74b2884e 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampSource.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampSource.kt @@ -6,4 +6,5 @@ enum class OnrampSource(val analyticsName: String) { TOKEN_LONG_TAP("Long Tap"), TOKEN_DETAILS("Token"), MARKETS("Markets"), + MARKETING_BANNER("Marketing Banner"), } \ No newline at end of file