Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-17 13:36:49 +02:00
commit 63832bf7e7
56 changed files with 1540 additions and 270 deletions

View file

@ -5,8 +5,11 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.markets.entry.BottomSheetState
@Stable
@ -19,5 +22,13 @@ interface MarketsTokenListComponent : ComposableContentComponent {
modifier: Modifier,
)
interface Factory : ComponentFactory<Unit, MarketsTokenListComponent>
interface FactoryScreen : ComponentFactory<Unit, MarketsTokenListComponent>
interface FactoryBottomSheet {
fun create(
context: AppComponentContext,
params: Unit,
onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit)?,
): MarketsTokenListComponent
}
}

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

@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
@ -14,6 +15,9 @@ import com.arkivanov.decompose.value.Value
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.entry.MarketsEntryComponent
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child
@ -48,6 +52,7 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
componentContext = factoryContext,
router = innerRouter,
),
onTokenClick = ::marketsListTokenSelected,
)
},
)
@ -67,6 +72,23 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
)
}
@OptIn(ExperimentalDecomposeApi::class)
private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) {
innerRouter.push(
route = Child.TokenDetails(
params = MarketsTokenDetailsComponent.Params(
token = token,
appCurrency = appCurrency,
showPortfolio = true,
analyticsParams = MarketsTokenDetailsComponent.AnalyticsParams(
blockchain = null,
source = "Market",
),
),
),
)
}
private fun onChildBack() {
if (stack.value.active.configuration !is Child.TokenList) {
stackNavigation.popWhile { it != Child.TokenList }

View file

@ -3,13 +3,15 @@ package com.tangem.features.markets.entry.impl
import androidx.compose.runtime.Immutable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
import kotlinx.serialization.Serializable
import javax.inject.Inject
internal class MarketsEntryChildFactory @Inject constructor(
private val tokenListComponentFactory: MarketsTokenListComponent.Factory,
private val tokenListComponentFactory: MarketsTokenListComponent.FactoryBottomSheet,
private val tokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
) {
@ -26,7 +28,11 @@ internal class MarketsEntryChildFactory @Inject constructor(
data class TokenDetails(val params: MarketsTokenDetailsComponent.Params) : Child
}
fun createChild(child: Child, appComponentContext: AppComponentContext): Any {
fun createChild(
child: Child,
appComponentContext: AppComponentContext,
onTokenClick: (TokenMarketParams, AppCurrency) -> Unit,
): Any {
return when (child) {
is Child.TokenDetails -> {
tokenDetailsComponentFactory.create(
@ -38,6 +44,7 @@ internal class MarketsEntryChildFactory @Inject constructor(
tokenListComponentFactory.create(
context = appComponentContext,
params = Unit,
onTokenClick = onTokenClick,
)
}
}

View file

@ -46,7 +46,7 @@ internal fun EntryBottomSheetContent(
modifier = modifier,
)
}
MarketsEntryChildFactory.Child.TokenList -> {
is MarketsEntryChildFactory.Child.TokenList -> {
(it.instance as MarketsTokenListComponent).BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
@ -84,7 +84,7 @@ private fun BackgroundColorEffects(
animationSpec = tween(durationMillis = 500),
)
}
MarketsEntryChildFactory.Child.TokenList -> {
is MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.animateTo(
primary,
animationSpec = tween(durationMillis = 500),
@ -119,7 +119,7 @@ private fun BackgroundColorEffects(
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.snapTo(tertiary)
}
MarketsEntryChildFactory.Child.TokenList -> {
is MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.snapTo(primary)
}
}

View file

@ -18,6 +18,8 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.markets.toSerializableParam
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
@ -31,9 +33,10 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@Suppress("UnusedPrivateMember")
class DefaultMarketsTokenListComponent @AssistedInject constructor(
internal class DefaultMarketsTokenListComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
@Assisted onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit)? = null,
) : AppComponentContext by appComponentContext, MarketsTokenListComponent {
private val model: MarketsListModel = getOrCreateModel()
@ -41,17 +44,23 @@ class DefaultMarketsTokenListComponent @AssistedInject constructor(
init {
model.tokenSelected
.onEach { (token, appCurrency) ->
router.push(
AppRoute.MarketsTokenDetails(
token = token.toSerializableParam(),
appCurrency = appCurrency,
showPortfolio = true,
analyticsParams = AnalyticsParams(
blockchain = null,
source = "Market",
// If specific routing call back is provided, invoke it (for example inner routing in Bottom Sheet)
// Otherwise navigate using app routing
if (onTokenClick != null) {
onTokenClick(token.toSerializableParam(), appCurrency)
} else {
router.push(
AppRoute.MarketsTokenDetails(
token = token.toSerializableParam(),
appCurrency = appCurrency,
showPortfolio = true,
analyticsParams = AnalyticsParams(
blockchain = null,
source = "Market",
),
),
),
)
)
}
}
.launchIn(componentScope)
}
@ -112,7 +121,11 @@ class DefaultMarketsTokenListComponent @AssistedInject constructor(
}
@AssistedFactory
interface Factory : MarketsTokenListComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultMarketsTokenListComponent
interface FactoryBottomSheet : MarketsTokenListComponent.FactoryBottomSheet {
override fun create(
context: AppComponentContext,
params: Unit,
onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit)?,
): DefaultMarketsTokenListComponent
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.features.markets.tokenlist.impl.di
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
import com.tangem.features.markets.tokenlist.impl.DefaultMarketsTokenListComponent
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@ -14,7 +16,24 @@ internal interface ComponentModule {
@Binds
@Singleton
fun bindMarketsTokenListComponent(
factory: DefaultMarketsTokenListComponent.Factory,
): MarketsTokenListComponent.Factory
fun bindMarketsTokenListBottomSheetComponent(
factory: DefaultMarketsTokenListComponent.FactoryBottomSheet,
): MarketsTokenListComponent.FactoryBottomSheet
}
@Module
@InstallIn(SingletonComponent::class)
internal class ComponentProvideModule {
@Provides
@Singleton
fun providesMarketsTokenListScreenComponent(
factory: DefaultMarketsTokenListComponent.FactoryBottomSheet,
): MarketsTokenListComponent.FactoryScreen {
return object : MarketsTokenListComponent.FactoryScreen {
override fun create(context: AppComponentContext, params: Unit): MarketsTokenListComponent {
return factory.create(context, params, null)
}
}
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.nft.collections.model.NFTCollectionsModel
import com.tangem.features.nft.collections.ui.NFTCollections
@ -31,7 +32,7 @@ internal class NFTCollectionsComponent @AssistedInject constructor(
data class Params(
val userWalletId: UserWalletId,
val onBackClick: () -> Unit,
val onAssetClick: (asset: NFTAsset, collectionName: String) -> Unit,
val onAssetClick: (asset: NFTAsset, collection: NFTCollection) -> Unit,
val onReceiveClick: () -> Unit,
)
}

View file

@ -20,7 +20,7 @@ internal class UpdateDataStateTransformer(
private val onRetryClick: () -> Unit,
private val onExpandCollectionClick: (NFTCollection) -> Unit,
private val onRetryAssetsClick: (NFTCollection) -> Unit,
private val onAssetClick: (NFTAsset, String) -> Unit,
private val onAssetClick: (NFTAsset, NFTCollection) -> Unit,
private val initialSearchBarFactory: () -> SearchBarUM,
private val collectionIdProvider: NFTCollection.() -> String,
) : Transformer<NFTCollectionsStateUM> {
@ -118,12 +118,12 @@ internal class UpdateDataStateTransformer(
is NFTCollection.Assets.Value -> NFTCollectionAssetsListUM.Content(
items = assets
.items
.map { it.transform(name.orEmpty()) }
.map { it.transform(this) }
.toPersistentList(),
)
}
private fun NFTAsset.transform(collectionName: String): NFTCollectionAssetUM {
private fun NFTAsset.transform(collection: NFTCollection): NFTCollectionAssetUM {
return NFTCollectionAssetUM(
id = id.toString(),
name = name ?: DASH_SIGN,
@ -144,7 +144,7 @@ internal class UpdateDataStateTransformer(
)
},
onItemClick = {
onAssetClick(this, collectionName)
onAssetClick(this, collection)
},
)
}

View file

@ -79,8 +79,8 @@ internal class NFTCollectionsModel @Inject constructor(
onRetryClick = ::onRefresh,
onExpandCollectionClick = ::onExpandCollectionClick,
onRetryAssetsClick = ::onRetryAssetsClick,
onAssetClick = { asset, collectionName ->
params.onAssetClick(asset, collectionName)
onAssetClick = { asset, collection ->
params.onAssetClick(asset, collection)
},
initialSearchBarFactory = ::getInitialSearchBar,
collectionIdProvider = collectionIdProvider,

View file

@ -112,12 +112,12 @@ internal class DefaultNFTComponent @AssistedInject constructor(
),
)
},
onAssetClick = { asset, collectionName ->
onAssetClick = { asset, collection ->
innerRouter.push(
NFTRoute.Details(
userWalletId = route.userWalletId,
nftAsset = asset,
collectionName = collectionName,
collection = collection,
),
)
},
@ -144,7 +144,7 @@ internal class DefaultNFTComponent @AssistedInject constructor(
params = NFTDetailsComponent.Params(
userWalletId = route.userWalletId,
nftAsset = route.nftAsset,
nftCollectionName = route.collectionName,
nftCollection = route.collection,
onBackClick = ::onChildBack,
onAllTraitsClick = {
innerRouter.push(

View file

@ -2,6 +2,7 @@ package com.tangem.features.nft.common
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
@ -21,7 +22,7 @@ internal sealed class NFTRoute : Route {
data class Details(
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
val collectionName: String,
val collection: NFTCollection,
) : NFTRoute()
@Serializable

View file

@ -14,6 +14,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.nft.details.entity.NFTDetailsBottomSheetConfig
import com.tangem.features.nft.details.info.NFTDetailsInfoComponent
@ -65,7 +66,7 @@ internal class NFTDetailsComponent @AssistedInject constructor(
data class Params(
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
val nftCollectionName: String,
val nftCollection: NFTCollection,
val onBackClick: () -> Unit,
val onAllTraitsClick: () -> Unit,
)

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.features.nft.details.entity.NFTAssetUM
import com.tangem.features.nft.details.entity.NFTDetailsUM
@ -29,8 +30,8 @@ internal class NFTDetailsUMFactory(
private val onInfoBlockClick: (title: TextReference, text: TextReference) -> Unit,
) {
fun getInitialState(nftAsset: NFTAsset): NFTDetailsUM = NFTDetailsUM(
nftAsset = nftAsset.transform(),
fun getInitialState(nftAsset: NFTAsset, nftCollection: NFTCollection): NFTDetailsUM = NFTDetailsUM(
nftAsset = nftAsset.transform(nftCollection),
pullToRefreshConfig = PullToRefreshConfig(
isRefreshing = false,
onRefresh = { onRefresh() },
@ -42,63 +43,62 @@ internal class NFTDetailsUMFactory(
onSendClick = onSendClick,
)
private fun NFTAsset.transform(): NFTAssetUM = NFTAssetUM(
name = name.orEmpty(),
media = media?.imageUrl?.let {
NFTAssetUM.Media.Content(
url = it,
)
} ?: NFTAssetUM.Media.Empty,
topInfo = when {
!hasSalePrice() && description.isNullOrEmpty() && rarity == null -> {
NFTAssetUM.TopInfo.Empty
}
else -> {
val hasSalePrice = hasSalePrice()
val hasDescription = !description.isNullOrEmpty()
val rarity = rarity
NFTAssetUM.TopInfo.Content(
title = if (hasSalePrice) {
resourceReference(R.string.nft_details_last_sale_price)
} else {
null
},
salePrice = toSalePrice(),
description = description,
rarity = if (rarity != null) {
NFTAssetUM.Rarity.Content(
rank = rarity.rank,
label = rarity.label,
showDivider = hasSalePrice || hasDescription,
onLabelClick = {
onInfoBlockClick(
resourceReference(R.string.nft_details_rarity_label),
resourceReference(R.string.nft_details_info_rarity_label),
)
},
onRankClick = {
onInfoBlockClick(
resourceReference(R.string.nft_details_rarity_rank),
resourceReference(R.string.nft_details_info_rarity_rank),
)
},
)
} else {
NFTAssetUM.Rarity.Empty
},
private fun NFTAsset.transform(nftCollection: NFTCollection): NFTAssetUM {
val description = description?.takeIf { it.isNotEmpty() } ?: nftCollection.description
return NFTAssetUM(
name = name.orEmpty(),
media = media?.imageUrl?.let {
NFTAssetUM.Media.Content(
url = it,
)
}
},
traits = traits.take(MAX_TRAITS_COUNT).map {
NFTAssetUM.BlockItem(
title = stringReference(it.name),
value = it.value,
showInfoButton = false,
)
}.toImmutableList(),
showAllTraitsButton = traits.size > MAX_TRAITS_COUNT,
baseInfoItems = buildBaseInfoItems(),
)
} ?: NFTAssetUM.Media.Empty,
topInfo = when {
!hasSalePrice() && description.isNullOrEmpty() && rarity == null -> {
NFTAssetUM.TopInfo.Empty
}
else -> {
val hasSalePrice = hasSalePrice()
val hasDescription = !description.isNullOrEmpty()
val rarity = rarity
NFTAssetUM.TopInfo.Content(
title = resourceReference(R.string.nft_details_last_sale_price).takeIf { hasSalePrice },
salePrice = toSalePrice(),
description = description,
rarity = if (rarity != null) {
NFTAssetUM.Rarity.Content(
rank = rarity.rank,
label = rarity.label,
showDivider = hasSalePrice || hasDescription,
onLabelClick = {
onInfoBlockClick(
resourceReference(R.string.nft_details_rarity_label),
resourceReference(R.string.nft_details_info_rarity_label),
)
},
onRankClick = {
onInfoBlockClick(
resourceReference(R.string.nft_details_rarity_rank),
resourceReference(R.string.nft_details_info_rarity_rank),
)
},
)
} else {
NFTAssetUM.Rarity.Empty
},
)
}
},
traits = traits.take(MAX_TRAITS_COUNT).map {
NFTAssetUM.BlockItem(
title = stringReference(it.name),
value = it.value,
showInfoButton = false,
)
}.toImmutableList(),
showAllTraitsButton = traits.size > MAX_TRAITS_COUNT,
baseInfoItems = buildBaseInfoItems(),
)
}
private fun NFTAsset.hasSalePrice() = salePrice !is NFTSalePrice.Empty && salePrice !is NFTSalePrice.Error

View file

@ -74,7 +74,7 @@ internal class NFTDetailsModel @Inject constructor(
private val _state by lazy {
MutableStateFlow(
value = stateFactory.getInitialState(params.nftAsset),
value = stateFactory.getInitialState(params.nftAsset, params.nftCollection),
)
}
@ -187,7 +187,7 @@ internal class NFTDetailsModel @Inject constructor(
AppRoute.NFTSend(
userWalletId = params.userWalletId,
nftAsset = params.nftAsset,
nftCollectionName = params.nftCollectionName,
nftCollectionName = params.nftCollection.name.orEmpty(),
),
)
}

View file

@ -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
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.onramp.deeplink
import kotlinx.coroutines.CoroutineScope
interface BuyRedirectDeepLinkHandler {
interface Factory {
fun create(coroutineScope: CoroutineScope): BuyRedirectDeepLinkHandler
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.onramp.deeplink
interface SellDeepLinkHandler {
interface Factory {
fun create(): SellDeepLinkHandler
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.onramp.deeplink
interface SwapDeepLinkHandler {
interface Factory {
fun create(): SwapDeepLinkHandler
}
}

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

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}

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

@ -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<String, String>): SellDeepLinkHandler
fun create(coroutineScope: CoroutineScope, queryParams: Map<String, String>): SellRedirectDeepLinkHandler
}
}

View file

@ -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<String, String>,
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<String, String>,
): DefaultSellDeepLinkHandler
): DefaultSellRedirectDeepLinkHandler
}
private companion object {

View file

@ -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
}

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

@ -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<String, String>,
@ -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<String, String>,
): DefaultStakingDeepLinkHandler
}
private companion object {
const val WALLET_ID_KEY = "walletId"
const val NETWORK_ID_KEY = "network_id"
const val TOKEN_ID_KEY = "token_id"
}
}

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 {