Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-23 18:02:14 +05:00
parent 4032360f13
commit 4dc6dffd67
16 changed files with 381 additions and 50 deletions

View file

@ -1,5 +1,6 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)

View file

@ -15,15 +15,16 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsModel
import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -35,6 +36,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory,
txHistoryComponentFactory: TxHistoryComponent.Factory,
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
private val yieldSupplyWarningComponentFactory: YieldSupplyDepositedWarningComponent.Factory,
yieldSupplyComponentFactory: YieldSupplyComponent.Factory,
) : TokenDetailsComponent, AppComponentContext by appComponentContext {
@ -50,7 +52,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = TokenReceiveConfig.serializer(),
serializer = TokenDetailsBottomSheetConfig.serializer(),
handleBackButton = false,
childFactory = ::bottomSheetChild,
)
@ -98,15 +100,26 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
}
private fun bottomSheetChild(
config: TokenReceiveConfig,
route: TokenDetailsBottomSheetConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create(
context = childByContext(componentContext),
params = TokenReceiveComponent.Params(
config = config,
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
): ComposableBottomSheetComponent = when (route) {
is TokenDetailsBottomSheetConfig.Receive -> tokenReceiveComponentFactory.create(
context = childByContext(componentContext),
params = TokenReceiveComponent.Params(
config = route.tokenReceiveConfig,
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
is TokenDetailsBottomSheetConfig.YieldSupplyWarning -> yieldSupplyWarningComponentFactory.create(
context = childByContext(componentContext),
params = YieldSupplyDepositedWarningComponent.Params(
cryptoCurrency = route.cryptoCurrency,
onDismiss = model.bottomSheetNavigation::dismiss,
modelCallback = model,
tokenAction = route.tokenAction,
),
)
}
@AssistedFactory
interface Factory : TokenDetailsComponent.Factory {

View file

@ -5,6 +5,7 @@ import arrow.core.getOrElse
import arrow.core.merge
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
@ -58,6 +59,7 @@ import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.analytics.*
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.DetailsScreenOpened.TokenBalance
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.domain.transaction.error.AssociateAssetError
import com.tangem.domain.transaction.error.IncompleteTransactionError
import com.tangem.domain.transaction.error.OpenTrustlineError
@ -72,6 +74,7 @@ import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionListen
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsCurrencyStatusAnalyticsSender
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender
import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
@ -81,6 +84,7 @@ import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
@ -139,7 +143,10 @@ internal class TokenDetailsModel @Inject constructor(
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
private val getEnsNameUseCase: GetEnsNameUseCase,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
) : Model(), TokenDetailsClickIntents {
private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase,
private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase,
private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase,
) : Model(), TokenDetailsClickIntents, YieldSupplyDepositedWarningComponent.ModelCallback {
private val params = paramsContainer.require<TokenDetailsComponent.Params>()
private val userWalletId: UserWalletId = params.userWalletId
@ -162,7 +169,7 @@ internal class TokenDetailsModel @Inject constructor(
/** Transaction id to check for status */
private val waitForFirstExpressStatusEmmit = MutableStateFlow(false)
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
val bottomSheetNavigation: SlotNavigation<TokenDetailsBottomSheetConfig> = SlotNavigation()
private val stateFactory = TokenDetailsStateFactory(
currentStateProvider = Provider { uiState.value },
@ -527,8 +534,18 @@ internal class TokenDetailsModel @Inject constructor(
if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) {
return
}
sendCurrency()
modelScope.launch {
if (needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)) {
bottomSheetNavigation.activate(
configuration = TokenDetailsBottomSheetConfig.YieldSupplyWarning(
cryptoCurrency = cryptoCurrency,
tokenAction = TokenAction.Send,
),
)
} else {
sendCurrency()
}
}
}
private fun sendCurrency() {
@ -563,26 +580,15 @@ internal class TokenDetailsModel @Inject constructor(
}
modelScope.launch {
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
if (needShowYieldSupplyWarning()) {
bottomSheetNavigation.activate(
configuration = configureReceiveAddresses(addresses = networkAddress),
configuration = TokenDetailsBottomSheetConfig.YieldSupplyWarning(
cryptoCurrency = cryptoCurrency,
tokenAction = TokenAction.Receive,
),
)
} else {
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol))
internalUiState.value = stateFactory.getStateWithReceiveBottomSheet(
currency = cryptoCurrency,
networkAddress = networkAddress,
onCopyClick = {
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol))
clipboardManager.setText(text = it, isSensitive = true)
},
onShareClick = {
analyticsEventsHandler.send(
TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol),
)
shareManager.shareText(text = it)
},
)
navigateToReceive()
}
}
}
@ -656,13 +662,24 @@ internal class TokenDetailsModel @Inject constructor(
return
}
appRouter.push(
AppRoute.Swap(
currencyFrom = cryptoCurrency,
userWalletId = userWalletId,
screenSource = AnalyticsParam.ScreensSources.Token.value,
),
)
modelScope.launch {
if (needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)) {
bottomSheetNavigation.activate(
configuration = TokenDetailsBottomSheetConfig.YieldSupplyWarning(
cryptoCurrency = cryptoCurrency,
tokenAction = TokenAction.Swap,
),
)
} else {
appRouter.push(
AppRoute.Swap(
currencyFrom = cryptoCurrency,
userWalletId = userWalletId,
screenSource = AnalyticsParam.ScreensSources.Token.value,
),
)
}
}
}
override fun onDismissDialog() {
@ -1040,7 +1057,7 @@ internal class TokenDetailsModel @Inject constructor(
.launchIn(modelScope)
}
private suspend fun configureReceiveAddresses(addresses: NetworkAddress): TokenReceiveConfig {
private suspend fun configureReceiveAddresses(addresses: NetworkAddress): TokenDetailsBottomSheetConfig {
val ensName = getEnsNameUseCase.invoke(
userWalletId = userWalletId,
network = cryptoCurrency.network,
@ -1069,12 +1086,14 @@ internal class TokenDetailsModel @Inject constructor(
}
}
return TokenReceiveConfig(
shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(),
cryptoCurrency = cryptoCurrency,
userWalletId = userWalletId,
showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
receiveAddress = receiveAddresses,
return TokenDetailsBottomSheetConfig.Receive(
TokenReceiveConfig(
shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(),
cryptoCurrency = cryptoCurrency,
userWalletId = userWalletId,
showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
receiveAddress = receiveAddresses,
),
)
}
@ -1115,6 +1134,59 @@ internal class TokenDetailsModel @Inject constructor(
isBalanceLoadedEventSent = true
}
private suspend fun needShowYieldSupplyWarning(): Boolean {
return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled &&
needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)
}
override fun onYieldSupplyWarningAcknowledged(tokenAction: TokenAction) {
bottomSheetNavigation.dismiss()
modelScope.launch {
saveViewedYieldSupplyWarningUseCase(cryptoCurrency.name)
if (tokenAction == TokenAction.Receive) {
saveViewedTokenReceiveWarningUseCase(cryptoCurrency.name)
}
when (tokenAction) {
TokenAction.Receive -> navigateToReceive()
TokenAction.Send -> sendCurrency()
TokenAction.Swap -> appRouter.push(
AppRoute.Swap(
currencyFrom = cryptoCurrency,
userWalletId = userWalletId,
screenSource = AnalyticsParam.ScreensSources.Token.value,
),
)
}
}
}
private fun navigateToReceive() {
val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
modelScope.launch {
bottomSheetNavigation.activate(
configuration = configureReceiveAddresses(addresses = networkAddress),
)
}
} else {
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol))
internalUiState.value = stateFactory.getStateWithReceiveBottomSheet(
currency = cryptoCurrency,
networkAddress = networkAddress,
onCopyClick = {
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol))
clipboardManager.setText(text = it, isSensitive = true)
},
onShareClick = {
analyticsEventsHandler.send(
TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol),
)
shareManager.shareText(text = it)
},
)
}
}
private companion object {
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
}

View file

@ -0,0 +1,20 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.route
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.details.TokenAction
import kotlinx.serialization.Serializable
@Serializable
sealed class TokenDetailsBottomSheetConfig : Route {
@Serializable
data class Receive(val tokenReceiveConfig: TokenReceiveConfig) : TokenDetailsBottomSheetConfig()
@Serializable
data class YieldSupplyWarning(
val cryptoCurrency: CryptoCurrency,
val tokenAction: TokenAction,
) : TokenDetailsBottomSheetConfig()
}

View file

@ -4,17 +4,19 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.details.TokenAction
interface YieldSupplyDepositedWarningComponent : ComposableBottomSheetComponent {
data class Params(
val cryptoCurrency: CryptoCurrency,
val tokenAction: TokenAction,
val modelCallback: ModelCallback,
val onDismiss: () -> Unit,
)
interface ModelCallback {
fun onYieldSupplyWarningAcknowledged()
fun onYieldSupplyWarningAcknowledged(tokenAction: TokenAction)
}
interface Factory : ComponentFactory<Params, YieldSupplyDepositedWarningComponent> {

View file

@ -28,7 +28,9 @@ internal class YieldSupplyDepositedWarningModel @Inject constructor(
field = MutableStateFlow(
YieldSupplyDepositedWarningUM(
iconState = iconStateConverter.convert(params.cryptoCurrency),
onWarningAcknowledged = params.modelCallback::onYieldSupplyWarningAcknowledged,
onWarningAcknowledged = {
params.modelCallback.onYieldSupplyWarningAcknowledged(params.tokenAction)
},
network = params.cryptoCurrency.name,
),
)

View file

@ -40,7 +40,7 @@ internal fun YieldSupplyDepositedWarningContent(warningUM: YieldSupplyDepositedW
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
containerColor = TangemTheme.colors.background.primary,
containerColor = TangemTheme.colors.background.tertiary,
onBack = null,
title = {
TangemModalBottomSheetTitle(
@ -57,7 +57,7 @@ internal fun YieldSupplyDepositedWarningContent(warningUM: YieldSupplyDepositedW
.padding(horizontal = 16.dp)
.fillMaxWidth(),
text = stringResourceSafe(CoreUiR.string.balance_hidden_got_it_button),
onClick = onDismiss,
onClick = warningUM.onWarningAcknowledged,
)
},
)
@ -68,7 +68,6 @@ private fun Content(warningUM: YieldSupplyDepositedWarningUM) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.primary)
.padding(
start = 16.dp,
end = 16.dp,