Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-11 17:09:54 +05:00
parent ab27ff4bea
commit 2f7c994a61
15 changed files with 256 additions and 137 deletions

View file

@ -34,6 +34,7 @@
<application
android:name="com.tangem.tap.TangemHiltApplication"
android:allowBackup="false"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="false"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
@ -43,7 +44,6 @@
android:resizeableActivity="@bool/resizeable_activity"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:enableOnBackInvokedCallback="true"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:allowBackup, android:fullBackupContent, android:label">
@ -208,6 +208,39 @@
android:host="token_chart"
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="buy"
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="sell"
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="swap"
android:scheme="tangem" />
</intent-filter>
</activity>
<!-- Disable android.startup completely. Used for Worker according doc -->

View file

@ -4,12 +4,12 @@ import android.net.Uri
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.DeepLinkRoute
import com.tangem.common.routing.DeepLinkScheme
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler
import com.tangem.features.onramp.deeplink.*
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
@ -21,6 +21,7 @@ import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.transformLatest
import timber.log.Timber
@ -29,9 +30,10 @@ import javax.inject.Inject
@Suppress("LongParameterList")
@ActivityScoped
internal class DeepLinkFactory @Inject constructor(
private val cardSdkProvider: CardSdkProvider,
private val onrampDeepLink: OnrampDeepLinkHandler.Factory,
private val sellDeepLink: SellDeepLinkHandler.Factory,
private val buyDeepLink: BuyDeepLinkHandler.Factory,
private val sellRedirectDeepLink: SellRedirectDeepLinkHandler.Factory,
private val buyRedirectDeepLink: BuyRedirectDeepLinkHandler.Factory,
private val referralDeepLink: ReferralDeepLinkHandler.Factory,
private val walletConnectDeepLink: WalletConnectDeepLinkHandler.Factory,
private val walletDeepLink: WalletDeepLinkHandler.Factory,
@ -39,6 +41,9 @@ internal class DeepLinkFactory @Inject constructor(
private val stakingDeepLink: StakingDeepLinkHandler.Factory,
private val marketsDeepLink: MarketsDeepLinkHandler.Factory,
private val marketsTokenDetailDeepLink: MarketsTokenDetailDeepLinkHandler.Factory,
private val buyDeepLink: BuyDeepLinkHandler.Factory,
private val sellDeepLink: SellDeepLinkHandler.Factory,
private val swapDeepLink: SwapDeepLinkHandler.Factory,
) {
private val permittedAppRoute = MutableStateFlow(false)
@ -55,15 +60,19 @@ internal class DeepLinkFactory @Inject constructor(
|- Received URI: $deeplinkUri
""".trimIndent(),
)
permittedAppRoute
.transformLatest<Boolean, Unit> { isPermitted ->
if (isPermitted) {
lastDeepLink?.let {
launchDeepLink(it, coroutineScope, isFromOnNewIntent)
}
lastDeepLink = null
combine(
permittedAppRoute,
cardSdkProvider.sdk.uiVisibility(),
) { isRoutePermitted, isCardSdkVisible ->
isRoutePermitted to isCardSdkVisible
}.transformLatest<Pair<Boolean, Boolean>, Unit> { (isRoutePermitted, isCardSdkVisible) ->
if (isRoutePermitted && !isCardSdkVisible) {
lastDeepLink?.let {
launchDeepLink(it, coroutineScope, isFromOnNewIntent)
}
lastDeepLink = null
}
}
.launchIn(coroutineScope)
.saveIn(deepLinkHandlerJobHolder)
}
@ -103,8 +112,8 @@ internal class DeepLinkFactory @Inject constructor(
val queryParams = getQueryParams(deeplinkUri)
when (deeplinkUri.host) {
DeepLinkRoute.Onramp.host -> onrampDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.Sell.host -> sellDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.Buy.host -> buyDeepLink.create(coroutineScope)
DeepLinkRoute.SellRedirect.host -> sellRedirectDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.BuyRedirect.host -> buyRedirectDeepLink.create(coroutineScope)
DeepLinkRoute.Referral.host -> referralDeepLink.create()
DeepLinkRoute.Wallet.host -> walletDeepLink.create()
DeepLinkRoute.TokenDetails.host -> tokenDetailsDeepLink.create(
@ -115,6 +124,9 @@ internal class DeepLinkFactory @Inject constructor(
DeepLinkRoute.Staking.host -> stakingDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.Markets.host -> marketsDeepLink.create()
DeepLinkRoute.MarketTokenDetail.host -> marketsTokenDetailDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.Buy.host -> buyDeepLink.create()
DeepLinkRoute.Sell.host -> sellDeepLink.create()
DeepLinkRoute.Swap.host -> swapDeepLink.create()
else -> {
Timber.i(
"""

View file

@ -2,12 +2,12 @@ package com.tangem.tap.routing.utils
import android.net.Uri
import com.tangem.common.routing.AppRoute
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler
import com.tangem.features.onramp.deeplink.*
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
@ -18,6 +18,7 @@ import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Before
@ -30,10 +31,10 @@ class DeepLinkFactoryTest {
private val onrampDeepLinkFactory = mockk<OnrampDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk()
}
private val sellDeepLinkFactory = mockk<SellDeepLinkHandler.Factory>(relaxed = true) {
private val sellRedirectDeepLinkFactory = mockk<SellRedirectDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk()
}
private val buyDeepLinkFactory = mockk<BuyDeepLinkHandler.Factory>(relaxed = true) {
private val buyRedirectDeepLinkFactory = mockk<BuyRedirectDeepLinkHandler.Factory>(relaxed = true) {
every { create(any()) } returns mockk()
}
private val referralDeepLinkFactory = mockk<ReferralDeepLinkHandler.Factory>(relaxed = true) {
@ -57,7 +58,19 @@ class DeepLinkFactoryTest {
private val marketsTokenDetailDeepLinkFactory = mockk<MarketsTokenDetailDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk()
}
private val sellDeepLinkFactory = mockk<SellDeepLinkHandler.Factory>(relaxed = true) {
every { create() } returns mockk()
}
private val buyDeepLinkFactory = mockk<BuyDeepLinkHandler.Factory>(relaxed = true) {
every { create() } returns mockk()
}
private val swapDeepLinkFactory = mockk<SwapDeepLinkHandler.Factory>(relaxed = true) {
every { create() } returns mockk()
}
private val cardSdkProvider = mockk<CardSdkProvider>(relaxed = true) {
every { sdk.uiVisibility() } returns MutableStateFlow(false)
}
private val mockedUri = mockk<Uri>(relaxed = true)
private val isFromOnNewIntent: Boolean = false
@ -65,16 +78,20 @@ class DeepLinkFactoryTest {
private lateinit var testScope: TestScope
private val deepLinkFactory = DeepLinkFactory(
onrampDeepLinkFactory,
sellDeepLinkFactory,
buyDeepLinkFactory,
referralDeepLinkFactory,
walletConnectDeepLinkFactory,
walletDeepLinkFactory,
tokenDetailsDeepLinkFactory,
stakingDeepLinkFactory,
marketsDeepLinkFactory,
marketsTokenDetailDeepLinkFactory,
cardSdkProvider = cardSdkProvider,
onrampDeepLink = onrampDeepLinkFactory,
sellRedirectDeepLink = sellRedirectDeepLinkFactory,
buyRedirectDeepLink = buyRedirectDeepLinkFactory,
referralDeepLink = referralDeepLinkFactory,
walletConnectDeepLink = walletConnectDeepLinkFactory,
walletDeepLink = walletDeepLinkFactory,
tokenDetailsDeepLink = tokenDetailsDeepLinkFactory,
stakingDeepLink = stakingDeepLinkFactory,
marketsDeepLink = marketsDeepLinkFactory,
marketsTokenDetailDeepLink = marketsTokenDetailDeepLinkFactory,
buyDeepLink = buyDeepLinkFactory,
sellDeepLink = sellDeepLinkFactory,
swapDeepLink = swapDeepLinkFactory,
)
@OptIn(ExperimentalCoroutinesApi::class)
@ -140,6 +157,24 @@ class DeepLinkFactoryTest {
verify(inverse = true) { onrampDeepLinkFactory.create(any(), any()) }
}
@Test
fun `handleDeeplink does not launch when card scan visible`() = runTest {
every { mockedUri.scheme } returns "tangem"
every { mockedUri.host } returns "onramp"
every { mockedUri.query } returns "param=value"
every { mockedUri.queryParameterNames } returns setOf("param")
every { mockedUri.getQueryParameter("param") } returns "value"
every { cardSdkProvider.sdk.uiVisibility() } returns MutableStateFlow(true)
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
advanceUntilIdle()
// Verify no handler was called
verify(inverse = true) { onrampDeepLinkFactory.create(any(), any()) }
}
@Test
fun `launchDeepLink handles tangem scheme correctly`() = runTest {
every { mockedUri.scheme } returns "tangem"
@ -184,8 +219,8 @@ class DeepLinkFactoryTest {
verify(inverse = true) {
onrampDeepLinkFactory.create(any(), any())
sellDeepLinkFactory.create(any(), any())
buyDeepLinkFactory.create(any())
sellRedirectDeepLinkFactory.create(any(), any())
buyRedirectDeepLinkFactory.create(any())
referralDeepLinkFactory.create()
walletConnectDeepLinkFactory.create(any())
walletDeepLinkFactory.create()
@ -208,11 +243,11 @@ class DeepLinkFactoryTest {
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) }
// Test Sell
// Test Sell Redirect
every { mockedUri.host } returns "redirect_sell"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { sellDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) }
verify { sellRedirectDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) }
// Test Token Details
every { mockedUri.host } returns "token"
@ -242,11 +277,11 @@ class DeepLinkFactoryTest {
every { mockedUri.queryParameterNames } returns emptySet()
every { mockedUri.getQueryParameter(any()) } returns ""
// Test Buy
// Test Buy Redirect
every { mockedUri.host } returns "redirect"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { buyDeepLinkFactory.create(eq(testScope)) }
verify { buyRedirectDeepLinkFactory.create(eq(testScope)) }
// Test Referral
every { mockedUri.host } returns "referral"
@ -265,6 +300,24 @@ class DeepLinkFactoryTest {
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { marketsDeepLinkFactory.create() }
// Test Sell
every { mockedUri.host } returns "sell"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { sellDeepLinkFactory.create() }
// Test Swap
every { mockedUri.host } returns "swap"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { sellDeepLinkFactory.create() }
// Test Buy
every { mockedUri.host } returns "buy"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { buyDeepLinkFactory.create() }
}
@Test
@ -279,12 +332,15 @@ class DeepLinkFactoryTest {
verify(inverse = true) {
onrampDeepLinkFactory.create(any(), any())
sellDeepLinkFactory.create(any(), any())
buyDeepLinkFactory.create(any())
sellRedirectDeepLinkFactory.create(any(), any())
buyRedirectDeepLinkFactory.create(any())
referralDeepLinkFactory.create()
walletConnectDeepLinkFactory.create(any())
walletDeepLinkFactory.create()
tokenDetailsDeepLinkFactory.create(any(), any(), any())
buyDeepLinkFactory.create()
sellDeepLinkFactory.create()
swapDeepLinkFactory.create()
}
}

View file

@ -1,11 +1,12 @@
package com.tangem.core.deeplink
object DeeplinkConst {
@Deprecated("Used only for push notifications mapping, use `WALLET_ID_KEY` only")
const val PAYLOAD_WALLET_ID_KEY = "user_wallet_id"
const val TANGEM_SCHEME = "tangem"
const val WALLET_ID_KEY = "walletId"
const val WALLET_ID_KEY = "user_wallet_id"
const val NETWORK_ID_KEY = "network_id"
const val TYPE_KEY = "type"
const val TOKEN_ID_KEY = "token_id"
const val DERIVATION_PATH_KEY = "derivation_path"
const val TRANSACTION_ID_KEY = "transaction_id"
const val NAME_KEY = "name"
}

View file

@ -3,9 +3,11 @@ package com.tangem.core.deeplink.converter
import com.tangem.common.routing.DeepLinkRoute
import com.tangem.common.routing.DeepLinkScheme
import com.tangem.core.deeplink.DEEPLINK_KEY
import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
import com.tangem.core.deeplink.DeeplinkConst.NAME_KEY
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.PAYLOAD_WALLET_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.utils.converter.Converter
@ -25,21 +27,29 @@ object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {
val type = payload[TYPE_KEY] ?: return null
val networkId = payload[NETWORK_ID_KEY] ?: return null
val tokenId = payload[TOKEN_ID_KEY] ?: return null
val walletId = payload[PAYLOAD_WALLET_ID_KEY] ?: return null
val walletId = payload[WALLET_ID_KEY] ?: return null
val derivationPath = payload[DERIVATION_PATH_KEY] ?: return null
val transactionId = payload[TRANSACTION_ID_KEY]
val name = payload[NAME_KEY]
return DeepLinkBuilder().setScheme(DeepLinkScheme.Tangem.scheme)
.setAction(DeepLinkRoute.TokenDetails.host)
.addQueryParam(NETWORK_ID_KEY, networkId)
.addQueryParam(TOKEN_ID_KEY, tokenId)
.addQueryParam(TYPE_KEY, type)
.addQueryParam(WALLET_ID_KEY, walletId)
.build()
return DeepLinkBuilder().setScheme(DeepLinkScheme.Tangem.scheme).apply {
setAction(DeepLinkRoute.TokenDetails.host)
addQueryParam(NETWORK_ID_KEY, networkId)
addQueryParam(TOKEN_ID_KEY, tokenId)
addQueryParam(TYPE_KEY, type)
addQueryParam(WALLET_ID_KEY, walletId)
addQueryParam(DERIVATION_PATH_KEY, derivationPath)
transactionId?.let { addQueryParam(TRANSACTION_ID_KEY, it) }
name?.let { addQueryParam(NAME_KEY, it) }
}.build()
}
private fun isTangemPushNotificationPayload(payload: Map<String, String>): Boolean {
return payload.containsKey(TYPE_KEY) &&
payload.containsKey(NETWORK_ID_KEY) &&
payload.containsKey(TOKEN_ID_KEY) &&
payload.containsKey(PAYLOAD_WALLET_ID_KEY)
payload.containsKey(WALLET_ID_KEY) &&
payload.containsKey(DERIVATION_PATH_KEY)
}
}

View file

@ -2,10 +2,11 @@ package com.tangem.core.deeplink.converter
import com.google.common.truth.Truth.assertThat
import com.tangem.core.deeplink.DEEPLINK_KEY
import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.PAYLOAD_WALLET_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
import org.junit.Test
internal class PayloadToDeeplinkConverterTest {
@ -14,7 +15,8 @@ internal class PayloadToDeeplinkConverterTest {
fun `GIVEN payload with deeplink key WHEN convert THEN should return deeplink value`() {
// GIVEN
val payload = mapOf(
DEEPLINK_KEY to "tangem://token-details?networkId=ethereum&tokenId=0x123&type=token&walletId=wallet123",
DEEPLINK_KEY to "tangem://token-details?networkId=ethereum&tokenId=0x123&type=token&user_wallet_id=wallet123" +
"&derivation_path=m'0'0'0",
)
// WHEN
@ -22,7 +24,7 @@ internal class PayloadToDeeplinkConverterTest {
// THEN
assertThat(result).isEqualTo(
"tangem://token-details?networkId=ethereum&tokenId=0x123&type=token&walletId=wallet123",
"tangem://token-details?networkId=ethereum&tokenId=0x123&type=token&user_wallet_id=wallet123&derivation_path=m'0'0'0",
)
}
@ -33,7 +35,8 @@ internal class PayloadToDeeplinkConverterTest {
TYPE_KEY to "token",
NETWORK_ID_KEY to "ethereum",
TOKEN_ID_KEY to "0x123",
PAYLOAD_WALLET_ID_KEY to "wallet123",
WALLET_ID_KEY to "wallet123",
DERIVATION_PATH_KEY to "m'0'0'0",
)
// WHEN
@ -41,7 +44,7 @@ internal class PayloadToDeeplinkConverterTest {
// THEN
assertThat(result).isEqualTo(
"tangem://token?network_id=ethereum&token_id=0x123&type=token&walletId=wallet123",
"tangem://token?network_id=ethereum&token_id=0x123&type=token&user_wallet_id=wallet123&derivation_path=m'0'0'0",
)
}
@ -51,7 +54,8 @@ internal class PayloadToDeeplinkConverterTest {
val payload = mapOf(
NETWORK_ID_KEY to "ethereum",
TOKEN_ID_KEY to "0x123",
PAYLOAD_WALLET_ID_KEY to "wallet123",
WALLET_ID_KEY to "wallet123",
DERIVATION_PATH_KEY to "m'0'0'0",
)
// WHEN
@ -67,7 +71,8 @@ internal class PayloadToDeeplinkConverterTest {
val payload = mapOf(
TYPE_KEY to "token",
TOKEN_ID_KEY to "0x123",
PAYLOAD_WALLET_ID_KEY to "wallet123",
WALLET_ID_KEY to "wallet123",
DERIVATION_PATH_KEY to "m'0'0'0",
)
// WHEN
@ -83,7 +88,8 @@ internal class PayloadToDeeplinkConverterTest {
val payload = mapOf(
TYPE_KEY to "token",
NETWORK_ID_KEY to "ethereum",
PAYLOAD_WALLET_ID_KEY to "wallet123",
WALLET_ID_KEY to "wallet123",
DERIVATION_PATH_KEY to "m'0'0'0",
)
// WHEN

View file

@ -48,6 +48,11 @@ class MockStakingRepository : StakingRepository {
cryptoCurrency: CryptoCurrency,
): Flow<StakingAvailability> = flowOf(StakingAvailability.Unavailable)
override suspend fun getStakingAvailabilitySync(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): StakingAvailability = StakingAvailability.Unavailable
override suspend fun getActions(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,

View file

@ -15,7 +15,6 @@ dependencies {
/* Project - API */
api(projects.features.markets.api)
api(projects.features.onramp.api)
implementation(projects.core.navigation)
/* Data */
implementation(projects.data.common)
@ -38,6 +37,7 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)
implementation(projects.domain.notifications.models)
// FIXME [REDACTED_TASK_KEY]
// Remove the "Buy" and "Sell" actions from the redux middleware.
@ -73,6 +73,8 @@ dependencies {
implementation(projects.core.configToggles)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
implementation(projects.core.deepLinks)
/* Common */
implementation(projects.common.ui)

View file

@ -3,6 +3,7 @@ package com.tangem.features.markets.deeplink
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
@ -17,14 +18,18 @@ import kotlinx.coroutines.launch
import timber.log.Timber
internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject constructor(
@Assisted scope: CoroutineScope,
@Assisted queryParams: Map<String, String>,
appRouter: AppRouter,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
@Assisted private val scope: CoroutineScope,
@Assisted private val queryParams: Map<String, String>,
private val appRouter: AppRouter,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
) : MarketsTokenDetailDeepLinkHandler {
init {
handleDeepLink()
}
private fun handleDeepLink() {
val tokenId = queryParams[TOKEN_ID_KEY]
val rawTokenId = CryptoCurrency.RawID(tokenId.orEmpty())
@ -36,7 +41,7 @@ internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject construc
val tokenInfo = getTokenMarketInfoUseCase(
appCurrency = appCurrency,
tokenId = rawTokenId,
tokenSymbol = TOKEN_SYMBOL_KEY,
tokenSymbol = "", // used for analytics
).getOrElse {
Timber.e("Failed to get market token info")
return@launch
@ -71,9 +76,4 @@ internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject construc
queryParams: Map<String, String>,
): DefaultMarketsTokenDetailDeepLinkHandler
}
private companion object {
const val TOKEN_ID_KEY = "token_id"
const val TOKEN_SYMBOL_KEY = "token_symbol"
}
}

View file

@ -1,25 +1,15 @@
package com.tangem.features.onramp.deeplink
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.onramp.OnrampFeatureToggles
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
internal class DefaultBuyDeepLinkHandler @AssistedInject constructor(
@Assisted scope: CoroutineScope,
onrampFeatureToggles: OnrampFeatureToggles,
router: AppRouter,
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
analyticsEventHandler: AnalyticsEventHandler,
) : BuyDeepLinkHandler {
init {
@ -29,21 +19,13 @@ internal class DefaultBuyDeepLinkHandler @AssistedInject constructor(
Timber.e("Error on getting user wallet: $it")
},
ifRight = { userWallet ->
if (!onrampFeatureToggles.isFeatureEnabled && !userWallet.isMultiCurrency) {
scope.launch {
val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId).getOrElse {
Timber.e("Error on getting cryptoCurrency: $it")
return@launch
}
analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol))
}
}
router.push(AppRoute.BuyCrypto(userWallet.walletId))
},
)
}
@AssistedFactory
interface Factory : BuyDeepLinkHandler.Factory {
override fun create(coroutineScope: CoroutineScope): DefaultBuyDeepLinkHandler
override fun create(): DefaultBuyDeepLinkHandler
}
}

View file

@ -22,6 +22,7 @@ dependencies {
implementation(projects.core.utils)
implementation(projects.core.ui)
implementation(projects.core.decompose)
implementation(projects.core.deepLinks)
implementation(projects.libs.crypto)
implementation(projects.common.routing)
@ -41,6 +42,7 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.legacy)
implementation(projects.domain.wallets.models)
implementation(projects.domain.notifications.models)
implementation(projects.features.referral.domain)
/** Other libraries */

View file

@ -47,6 +47,7 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.decompose)
implementation(projects.core.deepLinks)
/** Domain */
@ -66,6 +67,7 @@ dependencies {
implementation(projects.domain.txhistory)
implementation(projects.domain.feedback)
implementation(projects.domain.feedback.models)
implementation(projects.domain.notifications.models)
/** Common */
implementation(projects.common.ui)

View file

@ -19,8 +19,7 @@ internal class TestPushMarketTokenClickBottomSheetTransformer(
append(DeepLinkRoute.MarketTokenDetail.host)
append("?token_id=")
append(tokenMarket.id)
append("&token_symbol=")
append(tokenMarket.symbol)
append("&type=promo")
},
),
),

View file

@ -39,56 +39,57 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
}
private fun handleDeepLink() {
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
val userWalletId = queryParams[WALLET_ID_KEY]?.let(::UserWalletId)
?: getSelectedWalletSyncUseCase().getOrNull()?.walletId
if (userWalletId == null) {
Timber.e("Error on getting user wallet")
return
}
val networkId = queryParams[NETWORK_ID_KEY]
val tokenId = queryParams[TOKEN_ID_KEY]
val type = NotificationType.getType(queryParams[TYPE_KEY])
if (type != NotificationType.Unknown) {
scope.launch {
val cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId = userWalletId).getOrElse {
Timber.e("Error on getting crypto currency list")
return@launch
}.firstOrNull {
it.network.backendId == networkId &&
it.id.rawCurrencyId?.value == tokenId
}
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
val selectedUserWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId
val walletId = queryParams[WALLET_ID_KEY]?.let(::UserWalletId) ?: selectedUserWalletId
if (cryptoCurrency == null) {
Timber.e(
"""
// If selected user wallet is different than from deeplink - ignore deeplink
// If selected user wallet is null - ignore deeplink
if (walletId != selectedUserWalletId || selectedUserWalletId == null) {
Timber.e("Error on getting user wallet")
return
}
scope.launch {
val cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId = selectedUserWalletId).getOrElse {
Timber.e("Error on getting crypto currency list")
return@launch
}.firstOrNull {
val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true)
val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
isNetwork && isCurrency
}
if (cryptoCurrency == null) {
Timber.e(
"""
Could not get crypto currency for
|- $NETWORK_ID_KEY: $networkId
|- $TOKEN_ID_KEY: $tokenId
""".trimIndent(),
)
return@launch
}
analyticsEventHandler.send(PushNotificationAnalyticEvents.NotificationOpened(type.name))
appRouter.push(
AppRoute.CurrencyDetails(
userWalletId = userWalletId,
currency = cryptoCurrency,
),
""".trimIndent(),
)
return@launch
}
if (isFromOnNewIntent) {
fetchCurrencyStatusUseCase.invoke(
userWalletId = userWalletId,
id = cryptoCurrency.id,
refresh = true,
)
}
analyticsEventHandler.send(PushNotificationAnalyticEvents.NotificationOpened(type.name))
appRouter.push(
AppRoute.CurrencyDetails(
userWalletId = selectedUserWalletId,
currency = cryptoCurrency,
),
)
if (isFromOnNewIntent) {
fetchCurrencyStatusUseCase.invoke(
userWalletId = selectedUserWalletId,
id = cryptoCurrency.id,
refresh = true,
)
}
}
}

View file

@ -1,10 +1,18 @@
package com.tangem.feature.wallet.deeplink
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultWalletDeepLinkHandler @AssistedInject constructor() : WalletDeepLinkHandler {
internal class DefaultWalletDeepLinkHandler @AssistedInject constructor(
router: AppRouter,
) : WalletDeepLinkHandler {
init {
router.popTo(AppRoute.Wallet)
}
@AssistedFactory
interface Factory : WalletDeepLinkHandler.Factory {