From 8dc9304c06745d9911b9b73d3ec5371c8e7316d1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Jun 2025 17:08:27 +0500 Subject: [PATCH 1/3] Updated on 2026-08-14 --- .../tangem/common/routing/DeepLinkRoute.kt | 16 +++++- .../onramp/deeplink/BuyDeepLinkHandler.kt | 4 +- .../deeplink/BuyRedirectDeepLinkHandler.kt | 10 ++++ .../onramp/deeplink/SellDeepLinkHandler.kt | 8 +++ .../onramp/deeplink/SwapDeepLinkHandler.kt | 8 +++ .../DefaultBuyRedirectDeepLinkHandler.kt | 49 +++++++++++++++++++ .../deeplink/DefaultSellDeepLinkHandler.kt | 31 ++++++++++++ .../deeplink/DefaultSwapDeepLinkHandler.kt | 31 ++++++++++++ .../deeplink/di/OnrampDeeplinkModule.kt | 14 ++++++ ...dler.kt => SellRedirectDeepLinkHandler.kt} | 4 +- ... => DefaultSellRedirectDeepLinkHandler.kt} | 10 ++-- .../send/v2/deeplink/di/SendDeepLinkModule.kt | 6 +-- 12 files changed, 176 insertions(+), 15 deletions(-) create mode 100644 features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/BuyRedirectDeepLinkHandler.kt create mode 100644 features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/SellDeepLinkHandler.kt create mode 100644 features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/SwapDeepLinkHandler.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyRedirectDeepLinkHandler.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSellDeepLinkHandler.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSwapDeepLinkHandler.kt rename features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/deeplink/{SellDeepLinkHandler.kt => SellRedirectDeepLinkHandler.kt} (67%) rename features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/{DefaultSellDeepLinkHandler.kt => DefaultSellRedirectDeepLinkHandler.kt} (91%) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt index 26a1ac0ce5..b16393478e 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt @@ -8,14 +8,22 @@ sealed class DeepLinkRoute { override val host: String = "onramp" } - data object Sell : DeepLinkRoute() { + data object SellRedirect : DeepLinkRoute() { override val host: String = "redirect_sell" } - data object Buy : DeepLinkRoute() { + data object Sell : DeepLinkRoute() { + override val host: String = "sell" + } + + data object BuyRedirect : DeepLinkRoute() { override val host: String = "redirect" } + data object Buy : DeepLinkRoute() { + override val host: String = "buy" + } + data object Referral : DeepLinkRoute() { override val host: String = "referral" } @@ -39,6 +47,10 @@ sealed class DeepLinkRoute { data object MarketTokenDetail : DeepLinkRoute() { override val host: String = "token_chart" } + + data object Swap : DeepLinkRoute() { + override val host: String = "swap" + } } enum class DeepLinkScheme(val scheme: String) { diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/BuyDeepLinkHandler.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/BuyDeepLinkHandler.kt index 75c2d61f29..13e8deb82e 100644 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/BuyDeepLinkHandler.kt +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/BuyDeepLinkHandler.kt @@ -1,10 +1,8 @@ package com.tangem.features.onramp.deeplink -import kotlinx.coroutines.CoroutineScope - interface BuyDeepLinkHandler { interface Factory { - fun create(coroutineScope: CoroutineScope): BuyDeepLinkHandler + fun create(): BuyDeepLinkHandler } } \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/BuyRedirectDeepLinkHandler.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/BuyRedirectDeepLinkHandler.kt new file mode 100644 index 0000000000..da300b5cc2 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/BuyRedirectDeepLinkHandler.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.deeplink + +import kotlinx.coroutines.CoroutineScope + +interface BuyRedirectDeepLinkHandler { + + interface Factory { + fun create(coroutineScope: CoroutineScope): BuyRedirectDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/SellDeepLinkHandler.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/SellDeepLinkHandler.kt new file mode 100644 index 0000000000..086f028f40 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/SellDeepLinkHandler.kt @@ -0,0 +1,8 @@ +package com.tangem.features.onramp.deeplink + +interface SellDeepLinkHandler { + + interface Factory { + fun create(): SellDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/SwapDeepLinkHandler.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/SwapDeepLinkHandler.kt new file mode 100644 index 0000000000..dce01989cc --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/SwapDeepLinkHandler.kt @@ -0,0 +1,8 @@ +package com.tangem.features.onramp.deeplink + +interface SwapDeepLinkHandler { + + interface Factory { + fun create(): SwapDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyRedirectDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyRedirectDeepLinkHandler.kt new file mode 100644 index 0000000000..3e3f70ce4a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyRedirectDeepLinkHandler.kt @@ -0,0 +1,49 @@ +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.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 DefaultBuyRedirectDeepLinkHandler @AssistedInject constructor( + @Assisted scope: CoroutineScope, + onrampFeatureToggles: OnrampFeatureToggles, + getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, + analyticsEventHandler: AnalyticsEventHandler, +) : BuyRedirectDeepLinkHandler { + + init { + // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet + getSelectedWalletSyncUseCase().fold( + ifLeft = { + 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)) + } + } + }, + ) + } + + @AssistedFactory + interface Factory : BuyRedirectDeepLinkHandler.Factory { + override fun create(coroutineScope: CoroutineScope): DefaultBuyRedirectDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSellDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSellDeepLinkHandler.kt new file mode 100644 index 0000000000..ee38f9501a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSellDeepLinkHandler.kt @@ -0,0 +1,31 @@ +package com.tangem.features.onramp.deeplink + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import timber.log.Timber + +internal class DefaultSellDeepLinkHandler @AssistedInject constructor( + router: AppRouter, + getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, +) : SellDeepLinkHandler { + + init { + // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet + getSelectedWalletSyncUseCase().fold( + ifLeft = { + Timber.e("Error on getting user wallet: $it") + }, + ifRight = { userWallet -> + router.push(AppRoute.SellCrypto(userWallet.walletId)) + }, + ) + } + + @AssistedFactory + interface Factory : SellDeepLinkHandler.Factory { + override fun create(): DefaultSellDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSwapDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSwapDeepLinkHandler.kt new file mode 100644 index 0000000000..7fff8ef48c --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSwapDeepLinkHandler.kt @@ -0,0 +1,31 @@ +package com.tangem.features.onramp.deeplink + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import timber.log.Timber + +internal class DefaultSwapDeepLinkHandler @AssistedInject constructor( + router: AppRouter, + getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, +) : SwapDeepLinkHandler { + + init { + // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet + getSelectedWalletSyncUseCase().fold( + ifLeft = { + Timber.e("Error on getting user wallet: $it") + }, + ifRight = { userWallet -> + router.push(AppRoute.SwapCrypto(userWallet.walletId)) + }, + ) + } + + @AssistedFactory + interface Factory : SwapDeepLinkHandler.Factory { + override fun create(): DefaultSwapDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt index 418baeccc3..9b44b6bd3d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt @@ -19,7 +19,21 @@ internal interface OnrampDeeplinkModule { @Singleton fun bindOnrampDeepLinkHandlerFactory(impl: DefaultOnrampDeepLinkHandler.Factory): OnrampDeepLinkHandler.Factory + @Binds + @Singleton + fun bindBuyRedirectDeepLinkHandler( + impl: DefaultBuyRedirectDeepLinkHandler.Factory, + ): BuyRedirectDeepLinkHandler.Factory + @Binds @Singleton fun bindBuyDeepLinkHandler(impl: DefaultBuyDeepLinkHandler.Factory): BuyDeepLinkHandler.Factory + + @Binds + @Singleton + fun bindSellDeepLinkHandler(impl: DefaultSellDeepLinkHandler.Factory): SellDeepLinkHandler.Factory + + @Binds + @Singleton + fun bindSwapDeepLinkHandler(impl: DefaultSwapDeepLinkHandler.Factory): SwapDeepLinkHandler.Factory } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/deeplink/SellDeepLinkHandler.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/deeplink/SellRedirectDeepLinkHandler.kt similarity index 67% rename from features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/deeplink/SellDeepLinkHandler.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/deeplink/SellRedirectDeepLinkHandler.kt index 32d263693a..a9d945790b 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/deeplink/SellDeepLinkHandler.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/deeplink/SellRedirectDeepLinkHandler.kt @@ -2,9 +2,9 @@ package com.tangem.features.send.v2.api.deeplink import kotlinx.coroutines.CoroutineScope -interface SellDeepLinkHandler { +interface SellRedirectDeepLinkHandler { interface Factory { - fun create(coroutineScope: CoroutineScope, queryParams: Map): SellDeepLinkHandler + fun create(coroutineScope: CoroutineScope, queryParams: Map): SellRedirectDeepLinkHandler } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellDeepLinkHandler.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt similarity index 91% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellDeepLinkHandler.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt index 49aeba8204..5320f622ed 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellDeepLinkHandler.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt @@ -5,7 +5,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler +import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -14,13 +14,13 @@ import kotlinx.coroutines.launch import timber.log.Timber @Suppress("ComplexCondition") -internal class DefaultSellDeepLinkHandler @AssistedInject constructor( +internal class DefaultSellRedirectDeepLinkHandler @AssistedInject constructor( @Assisted scope: CoroutineScope, @Assisted queryParams: Map, appRouter: AppRouter, getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, -) : SellDeepLinkHandler { +) : SellRedirectDeepLinkHandler { init { val currencyId = queryParams[CURRENCY_ID_KEY] @@ -70,11 +70,11 @@ internal class DefaultSellDeepLinkHandler @AssistedInject constructor( } @AssistedFactory - interface Factory : SellDeepLinkHandler.Factory { + interface Factory : SellRedirectDeepLinkHandler.Factory { override fun create( coroutineScope: CoroutineScope, queryParams: Map, - ): DefaultSellDeepLinkHandler + ): DefaultSellRedirectDeepLinkHandler } private companion object { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/di/SendDeepLinkModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/di/SendDeepLinkModule.kt index 6cd2bca80d..4187cff72b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/di/SendDeepLinkModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/di/SendDeepLinkModule.kt @@ -1,7 +1,7 @@ package com.tangem.features.send.v2.deeplink.di -import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler -import com.tangem.features.send.v2.deeplink.DefaultSellDeepLinkHandler +import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler +import com.tangem.features.send.v2.deeplink.DefaultSellRedirectDeepLinkHandler import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -14,5 +14,5 @@ internal interface SendDeepLinkModule { @Binds @Singleton - fun bindFactory(impl: DefaultSellDeepLinkHandler.Factory): SellDeepLinkHandler.Factory + fun bindFactory(impl: DefaultSellRedirectDeepLinkHandler.Factory): SellRedirectDeepLinkHandler.Factory } \ No newline at end of file From ab27ff4bea2f05531f3abd8851f5faac77760a73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Jun 2025 17:09:20 +0500 Subject: [PATCH 2/3] Updated on 2026-08-14 --- .../data/staking/DefaultStakingRepository.kt | 41 +++++++++++++++++ .../staking/GetStakingAvailabilityUseCase.kt | 10 ++++ .../staking/repositories/StakingRepository.kt | 5 ++ .../deeplink/DefaultStakingDeepLinkHandler.kt | 46 ++++++++++++------- 4 files changed, 86 insertions(+), 16 deletions(-) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index fd08381f40..d6ce77cad7 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -246,6 +246,47 @@ internal class DefaultStakingRepository( } } + override suspend fun getStakingAvailabilitySync( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): StakingAvailability { + if (!checkFeatureToggleEnabled(cryptoCurrency.network.id)) { + return StakingAvailability.Unavailable + } + + if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) { + return StakingAvailability.Unavailable + } + + val rawCurrencyId = cryptoCurrency.id.rawCurrencyId + if (rawCurrencyId == null) { + return StakingAvailability.Unavailable + } + + val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not() + + val yields = getEnabledYieldsSync() + if (yields.isEmpty()) { + return StakingAvailability.TemporaryUnavailable + } + + val prefetchedYield = findPrefetchedYield( + yields = yields, + currencyId = rawCurrencyId, + symbol = cryptoCurrency.symbol, + ) + + return when { + prefetchedYield != null && isSupportedInMobileApp -> { + StakingAvailability.Available(prefetchedYield.id) + } + prefetchedYield == null && isSupportedInMobileApp -> { + StakingAvailability.TemporaryUnavailable + } + else -> StakingAvailability.Unavailable + } + } + private fun checkFeatureToggleEnabled(networkId: Network.ID): Boolean { return when (networkId.toBlockchain()) { Blockchain.TON -> stakingFeatureToggles.isTonStakingEnabled diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt index 5f704c96b5..b65f257b05 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt @@ -32,4 +32,14 @@ class GetStakingAvailabilityUseCase( it.right() }.catch { emit(stakingErrorResolver.resolve(it).left()) } } + + suspend fun invokeSync( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Either = Either.catch { + stakingRepository.getStakingAvailabilitySync( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ) + }.mapLeft(stakingErrorResolver::resolve) } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 7ed0ee1686..e399fcd26c 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -37,6 +37,11 @@ interface StakingRepository { fun getStakingAvailability(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow + suspend fun getStakingAvailabilitySync( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): StakingAvailability + suspend fun getActions( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt index cc791844e8..4b5a2a090c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt @@ -3,7 +3,12 @@ package com.tangem.features.staking.impl.deeplink import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY +import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY +import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY +import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetYieldUseCase +import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -15,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber +@Suppress("LongParameterList") internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( @Assisted private val scope: CoroutineScope, @Assisted private val queryParams: Map, @@ -22,6 +28,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val getYieldUseCase: GetYieldUseCase, + private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase, ) : StakingDeepLinkHandler { init { @@ -29,24 +36,28 @@ internal class DefaultStakingDeepLinkHandler @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 + val networkId = queryParams[NETWORK_ID_KEY] + val tokenId = queryParams[TOKEN_ID_KEY] - if (userWalletId == null) { + // 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 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 } - val networkId = queryParams[NETWORK_ID_KEY] - val tokenId = queryParams[TOKEN_ID_KEY] - scope.launch { - val cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId = userWalletId).getOrElse { + val cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId = selectedUserWalletId).getOrElse { Timber.e("Error on getting crypto currency list") return@launch }.firstOrNull { - it.network.backendId == networkId && it.id.rawCurrencyId?.value == tokenId + 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) { @@ -60,6 +71,15 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( return@launch } + val isStakingEnabled = getStakingAvailabilityUseCase.invokeSync( + userWalletId = selectedUserWalletId, + cryptoCurrency = cryptoCurrency, + ).getOrNull() + + if (isStakingEnabled !is StakingAvailability.Available) { + return@launch + } + val yield = getYieldUseCase.invoke( cryptoCurrencyId = cryptoCurrency.id, symbol = cryptoCurrency.symbol, @@ -70,7 +90,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( appRouter.push( AppRoute.Staking( - userWalletId = userWalletId, + userWalletId = selectedUserWalletId, cryptoCurrencyId = cryptoCurrency.id, yieldId = yield.id, ), @@ -85,10 +105,4 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( queryParams: Map, ): DefaultStakingDeepLinkHandler } - - private companion object { - const val WALLET_ID_KEY = "walletId" - const val NETWORK_ID_KEY = "network_id" - const val TOKEN_ID_KEY = "token_id" - } } \ No newline at end of file From 2f7c994a61b6bf6b7db892f15571e84d79855bff Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Jun 2025 17:09:54 +0500 Subject: [PATCH 3/3] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 35 +++++- .../tap/routing/utils/DeepLinkFactory.kt | 40 ++++--- .../tap/routing/utils/DeepLinkFactoryTest.kt | 102 ++++++++++++++---- .../com/tangem/core/deeplink/DeeplinkConst.kt | 7 +- .../converter/PayloadToDeeplinkConverter.kt | 30 ++++-- .../PayloadToDeeplinkConverterTest.kt | 22 ++-- .../repository/MockStakingRepository.kt | 5 + features/markets/impl/build.gradle.kts | 4 +- ...efaultMarketsTokenDetailDeepLinkHandler.kt | 22 ++-- .../deeplink/DefaultBuyDeepLinkHandler.kt | 28 +---- features/referral/impl/build.gradle.kts | 2 + features/staking/impl/build.gradle.kts | 2 + ...hMarketTokenClickBottomSheetTransformer.kt | 3 +- .../DefaultTokenDetailsDeepLinkHandler.kt | 81 +++++++------- .../deeplink/DefaultWalletDeepLinkHandler.kt | 10 +- 15 files changed, 256 insertions(+), 137 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 06a868e0a4..ac76057c04 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,6 +34,7 @@ @@ -208,6 +208,39 @@ android:host="token_chart" android:scheme="tangem" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index f33be6d875..02afc7ecfe 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -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 { isPermitted -> - if (isPermitted) { - lastDeepLink?.let { - launchDeepLink(it, coroutineScope, isFromOnNewIntent) - } - lastDeepLink = null + combine( + permittedAppRoute, + cardSdkProvider.sdk.uiVisibility(), + ) { isRoutePermitted, isCardSdkVisible -> + isRoutePermitted to isCardSdkVisible + }.transformLatest, 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( """ diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index 8078f58b05..018f3b76b8 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -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(relaxed = true) { every { create(any(), any()) } returns mockk() } - private val sellDeepLinkFactory = mockk(relaxed = true) { + private val sellRedirectDeepLinkFactory = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() } - private val buyDeepLinkFactory = mockk(relaxed = true) { + private val buyRedirectDeepLinkFactory = mockk(relaxed = true) { every { create(any()) } returns mockk() } private val referralDeepLinkFactory = mockk(relaxed = true) { @@ -57,7 +58,19 @@ class DeepLinkFactoryTest { private val marketsTokenDetailDeepLinkFactory = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() } + private val sellDeepLinkFactory = mockk(relaxed = true) { + every { create() } returns mockk() + } + private val buyDeepLinkFactory = mockk(relaxed = true) { + every { create() } returns mockk() + } + private val swapDeepLinkFactory = mockk(relaxed = true) { + every { create() } returns mockk() + } + private val cardSdkProvider = mockk(relaxed = true) { + every { sdk.uiVisibility() } returns MutableStateFlow(false) + } private val mockedUri = mockk(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() } } diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeeplinkConst.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeeplinkConst.kt index cf598f7b4f..e0ffec3703 100644 --- a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeeplinkConst.kt +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeeplinkConst.kt @@ -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" } \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverter.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverter.kt index 8470ffbb71..f6ce9ad32e 100644 --- a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverter.kt +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverter.kt @@ -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, 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): 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) } } \ No newline at end of file diff --git a/core/deep-links/src/test/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverterTest.kt b/core/deep-links/src/test/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverterTest.kt index 697ff01d29..f3887c86e9 100644 --- a/core/deep-links/src/test/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverterTest.kt +++ b/core/deep-links/src/test/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverterTest.kt @@ -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 diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index c53411a627..303da59cda 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -48,6 +48,11 @@ class MockStakingRepository : StakingRepository { cryptoCurrency: CryptoCurrency, ): Flow = flowOf(StakingAvailability.Unavailable) + override suspend fun getStakingAvailabilitySync( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): StakingAvailability = StakingAvailability.Unavailable + override suspend fun getActions( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 499ba281a4..2ce867d718 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -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) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt index 5007ac70ab..f9d5ca176b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt @@ -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, - appRouter: AppRouter, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, + @Assisted private val scope: CoroutineScope, + @Assisted private val queryParams: Map, + 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, ): DefaultMarketsTokenDetailDeepLinkHandler } - - private companion object { - const val TOKEN_ID_KEY = "token_id" - const val TOKEN_SYMBOL_KEY = "token_symbol" - } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt index 32029a1244..5633cf6424 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt @@ -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 } } \ No newline at end of file diff --git a/features/referral/impl/build.gradle.kts b/features/referral/impl/build.gradle.kts index 89e4f4f57c..34f1402a0b 100644 --- a/features/referral/impl/build.gradle.kts +++ b/features/referral/impl/build.gradle.kts @@ -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 */ diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 05decae6d5..ebfa0a6ffd 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -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) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenClickBottomSheetTransformer.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenClickBottomSheetTransformer.kt index 08f232befb..58fcfdbf4e 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenClickBottomSheetTransformer.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenClickBottomSheetTransformer.kt @@ -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") }, ), ), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt index 41456e71f5..379b813c06 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt @@ -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, + ) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultWalletDeepLinkHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultWalletDeepLinkHandler.kt index 508333060f..35d56c1bec 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultWalletDeepLinkHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultWalletDeepLinkHandler.kt @@ -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 {