Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-20 15:20:53 +05:00
commit 54cd8b7cd7
17 changed files with 534 additions and 77 deletions

View file

@ -50,5 +50,9 @@
{
"name": "HOT_WALLET_ENABLED",
"version": "undefined"
},
{
"name": "NFT_SEND_REDESIGN_ENABLED",
"version": "undefined"
}
]

View file

@ -2,8 +2,9 @@ package com.tangem.features.nft.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.models.NFTAsset
interface NFTDetailsBlockComponent : ComposableContentComponent {
@ -11,6 +12,8 @@ interface NFTDetailsBlockComponent : ComposableContentComponent {
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
val nftCollectionName: String,
val title: TextReference,
val isSuccessScreen: Boolean,
)
interface Factory : ComponentFactory<Params, NFTDetailsBlockComponent>

View file

@ -22,6 +22,8 @@ class DefaultNFTDetailsBlockComponent @AssistedInject constructor(
assetName = stringReference(params.nftAsset.name.orEmpty()),
collectionName = stringReference(params.nftCollectionName),
assetImage = params.nftAsset.media?.imageUrl,
title = params.title,
isSuccessScreen = params.isSuccessScreen,
networkIconRes = getActiveIconRes(params.nftAsset.network.rawId),
)
}

View file

@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
@ -19,12 +20,15 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.common.ui.NFTLogo
import com.tangem.features.nft.impl.R
@Suppress("LongParameterList")
@Composable
internal fun NFTDetailsBlock(
title: TextReference,
assetName: TextReference,
collectionName: TextReference,
assetImage: String?,
networkIconRes: Int,
isSuccessScreen: Boolean,
) {
Column(
modifier = Modifier
@ -35,20 +39,21 @@ internal fun NFTDetailsBlock(
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Text(
text = "NFT Asset",
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
color = TangemTheme.colors.text.tertiary,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
NFTLogo(
assetImage,
networkIconRes,
background = TangemTheme.colors.background.action,
)
if (isSuccessScreen) {
NFTLogo(
assetImage,
networkIconRes,
background = TangemTheme.colors.background.action,
)
}
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
@ -63,6 +68,14 @@ internal fun NFTDetailsBlock(
color = TangemTheme.colors.text.tertiary,
)
}
if (!isSuccessScreen) {
SpacerWMax()
NFTLogo(
assetImage,
networkIconRes,
background = TangemTheme.colors.background.action,
)
}
}
}
}
@ -78,6 +91,8 @@ private fun NFTDetailsBlock_Preview() {
collectionName = stringReference("NFT Collection"),
assetImage = null,
networkIconRes = R.drawable.img_polygon_22,
title = stringReference("From My Wallet"),
isSuccessScreen = false,
)
}
}

View file

@ -3,5 +3,6 @@ package com.tangem.features.send.v2.api
interface SendFeatureToggles {
val isSendRedesignEnabled: Boolean
val isNFTSendRedesignEnabled: Boolean
val isSendWithSwapEnabled: Boolean
}

View file

@ -8,6 +8,8 @@ internal class DefaultSendFeatureToggles(
) : SendFeatureToggles {
override val isSendRedesignEnabled: Boolean
get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED")
override val isNFTSendRedesignEnabled: Boolean
get() = featureToggles.isFeatureEnabled("NFT_SEND_REDESIGN_ENABLED")
override val isSendWithSwapEnabled: Boolean
get() = featureToggles.isFeatureEnabled("SEND_VIA_SWAP_ENABLED")
}

View file

@ -56,7 +56,7 @@ internal fun SendConfirmSuccessContent(sendUM: SendUM, destinationBlockComponent
.padding(horizontal = TangemTheme.dimens.spacing16)
.scrollable(
state = rememberScrollState(),
orientation = Orientation.Horizontal,
orientation = Orientation.Vertical,
),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {

View file

@ -18,7 +18,6 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams
@ -29,6 +28,7 @@ import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent
import com.tangem.features.send.v2.sendnft.model.NFTSendModel
import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent
import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
@ -42,7 +42,8 @@ import java.math.BigDecimal
internal class DefaultNFTSendComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: NFTSendComponent.Params,
private val nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory,
private val nftSendConfirmComponentFactory: NFTSendConfirmComponent.Factory,
private val nftSendSuccessComponentFactory: NFTSendSuccessComponent.Factory,
private val analyticsEventHandler: AnalyticsEventHandler,
) : NFTSendComponent, AppComponentContext by appComponentContext {
@ -121,6 +122,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
is CommonSendRoute.Destination -> getDestinationComponent(factoryContext)
is CommonSendRoute.Fee -> getFeeComponent(factoryContext)
CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
CommonSendRoute.ConfirmSuccess -> getSuccessComponent(factoryContext)
else -> getStubComponent()
}
@ -164,9 +166,8 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
}
}
private fun getConfirmComponent(factoryContext: AppComponentContext) = NFTSendConfirmComponent(
private fun getConfirmComponent(factoryContext: AppComponentContext) = nftSendConfirmComponentFactory.create(
appComponentContext = factoryContext,
nftDetailsBlockComponentFactory = nftDetailsBlockComponentFactory,
params = NFTSendConfirmComponent.Params(
state = model.uiState.value,
analyticsCategoryName = analyticsCategoryName,
@ -180,9 +181,34 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
currentRoute = model.currentRouteFlow.filterIsInstance<CommonSendRoute.Confirm>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
onLoadFee = model::loadFee,
onSendTransaction = { innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess) },
),
)
private fun getSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent {
val txUrl = (model.uiState.value.confirmUM as? ConfirmUM.Success)?.txUrl
if (txUrl == null) {
model.showAlertError()
return getStubComponent()
}
return nftSendSuccessComponentFactory.create(
appComponentContext = factoryContext,
params = NFTSendSuccessComponent.Params(
nftSendUMFlow = model.uiState,
analyticsCategoryName = analyticsCategoryName,
userWallet = model.userWallet,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
nftAsset = params.nftAsset,
nftCollectionName = params.nftCollectionName,
callback = model,
currentRoute = model.currentRouteFlow.filterIsInstance<CommonSendRoute.ConfirmSuccess>(),
txUrl = txUrl,
),
)
}
private fun getStubComponent() = ComposableContentComponent { }
private fun onChildBack() {

View file

@ -10,17 +10,23 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.api.SendNotificationsComponent
import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.api.params.FeeSelectorParams.FeeStateConfiguration
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel
import com.tangem.features.send.v2.sendnft.confirm.ui.NFTSendConfirmContent
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
@ -28,13 +34,17 @@ import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinat
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.*
import java.math.BigDecimal
internal class NFTSendConfirmComponent(
appComponentContext: AppComponentContext,
params: Params,
internal class NFTSendConfirmComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Params,
nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory,
feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: NFTSendConfirmModel = getOrCreateModel(params = params)
@ -74,12 +84,28 @@ internal class NFTSendConfirmComponent(
onClick = model::showEditFee,
)
private val feeSelectorBlockComponent = feeSelectorComponentFactory.create(
context = child("NFTSendConfirmFeeSelectorBlock"),
params = FeeSelectorParams.FeeSelectorBlockParams(
state = model.uiState.value.feeSelectorUM,
onLoadFee = params.onLoadFee,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeStateConfiguration = FeeStateConfiguration.None,
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
analyticsCategoryName = params.analyticsCategoryName,
),
onResult = model::onFeeResult,
)
private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create(
context = child("NFTDetailsBlock"),
params = NFTDetailsBlockComponent.Params(
userWalletId = params.userWallet.walletId,
nftAsset = params.nftAsset,
nftCollectionName = params.nftCollectionName,
isSuccessScreen = false,
title = resourceReference(R.string.send_from_wallet_name, wrappedList(params.userWallet.name)),
),
)
@ -126,6 +152,7 @@ internal class NFTSendConfirmComponent(
nftSendUM = state,
destinationBlockComponent = destinationBlockComponent,
feeBlockComponent = feeBlockComponent,
feeSelectorBlockComponent = feeSelectorBlockComponent,
nftDetailsBlockComponent = nftDetailsBlockComponent,
notificationsComponent = notificationsComponent,
notificationsUM = notificationState,
@ -145,9 +172,15 @@ internal class NFTSendConfirmComponent(
val currentRoute: Flow<CommonSendRoute.Confirm>,
val isBalanceHidingFlow: StateFlow<Boolean>,
val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
val onSendTransaction: () -> Unit,
)
interface ModelCallback {
fun onResult(nftSendUM: NFTSendUM)
}
@AssistedFactory
interface Factory {
fun create(appComponentContext: AppComponentContext, params: Params): NFTSendConfirmComponent
}
}

View file

@ -33,6 +33,7 @@ import com.tangem.features.send.v2.api.SendNotificationsComponent
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger
@ -61,6 +62,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned
@Suppress("LongParameterList", "LargeClass")
@ModelScoped
@ -89,7 +91,7 @@ internal class NFTSendConfirmModel @Inject constructor(
private val nftSendSuccessTrigger: NFTSendSuccessTrigger,
private val sendFeeReloadTrigger: SendFeeReloadTrigger,
sendBalanceUpdaterFactory: SendBalanceUpdater.Factory,
) : Model(), NFTSendConfirmClickIntents, SendNotificationsComponent.ModelCallback {
) : Model(), NFTSendConfirmClickIntents, SendNotificationsComponent.ModelCallback, FeeSelectorModelCallback {
private val params: NFTSendConfirmComponent.Params = paramsContainer.require()
@ -141,6 +143,12 @@ internal class NFTSendConfirmModel @Inject constructor(
updateConfirmNotifications()
}
override fun onFeeResult(feeSelectorUM: FeeSelectorUMRedesigned) {
sendIdleTimer = SystemClock.elapsedRealtime()
_uiState.update { it.copy(feeSelectorUM = feeSelectorUM) }
updateConfirmNotifications()
}
fun onDestinationResult(destinationUM: DestinationUM) {
_uiState.update { it.copy(destinationUM = destinationUM) }
updateConfirmNotifications()
@ -400,13 +408,23 @@ internal class NFTSendConfirmModel @Inject constructor(
).onEach { (state, _) ->
val confirmUM = state.confirmUM
val confirmUMContent = confirmUM as? ConfirmUM.Content
val isReadyToSend = confirmUMContent != null && !confirmUM.isSending
params.callback.onResult(
state.copy(
navigationUM = NavigationUM.Content(
title = resourceReference(R.string.nft_send),
subtitle = confirmUMContent?.walletName,
backIconRes = R.drawable.ic_close_24,
subtitle = if (uiState.value.isRedesignEnabled) {
null
} else {
confirmUMContent?.walletName
},
backIconRes = if (state.isRedesignEnabled) {
when (confirmUM) {
is ConfirmUM.Success -> R.drawable.ic_close_24
else -> R.drawable.ic_back_24
}
} else {
R.drawable.ic_close_24
},
backIconClick = {
analyticsEventHandler.send(
CommonSendAnalyticEvents.CloseButtonClicked(
@ -416,26 +434,13 @@ internal class NFTSendConfirmModel @Inject constructor(
isValid = confirmUM.isPrimaryButtonEnabled,
),
)
appRouter.pop()
if (state.isRedesignEnabled) {
router.pop()
} else {
appRouter.pop()
}
},
primaryButton = NavigationButton(
textReference = when (confirmUM) {
is ConfirmUM.Success -> resourceReference(R.string.common_close)
is ConfirmUM.Content -> if (confirmUM.isSending) {
resourceReference(R.string.send_sending)
} else {
resourceReference(R.string.common_send)
}
else -> resourceReference(R.string.common_send)
},
iconRes = R.drawable.ic_tangem_24,
isIconVisible = isReadyToSend,
isEnabled = confirmUM.isPrimaryButtonEnabled,
isHapticClick = isReadyToSend,
onClick = {
onNextClick(confirmUM)
},
),
primaryButton = primaryButtonUM(),
prevButton = null,
secondaryPairButtonsUM = (
NavigationButton(
@ -454,21 +459,40 @@ internal class NFTSendConfirmModel @Inject constructor(
}.launchIn(modelScope)
}
private fun onNextClick(confirmUM: ConfirmUM) {
when (confirmUM) {
is ConfirmUM.Success -> {
modelScope.launch {
nftSendSuccessTrigger.triggerSuccessNFTSend()
private fun primaryButtonUM(): NavigationButton {
val confirmUM = uiState.value.confirmUM
val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending
return NavigationButton(
textReference = when (confirmUM) {
is ConfirmUM.Success -> resourceReference(R.string.common_close)
is ConfirmUM.Content -> if (confirmUM.isSending) {
resourceReference(R.string.send_sending)
} else {
resourceReference(R.string.common_send)
}
appRouter.pop()
}
is ConfirmUM.Content -> if (confirmUM.isSending) {
return
} else {
onSendClick()
}
else -> return
}
else -> resourceReference(R.string.common_send)
},
iconRes = R.drawable.ic_tangem_24,
isIconVisible = isReadyToSend,
isEnabled = confirmUM.isPrimaryButtonEnabled,
isHapticClick = isReadyToSend,
onClick = {
when (confirmUM) {
is ConfirmUM.Success -> {
modelScope.launch {
nftSendSuccessTrigger.triggerSuccessNFTSend()
}
appRouter.pop()
}
is ConfirmUM.Content -> if (confirmUM.isSending) {
return@NavigationButton
} else {
onSendClick()
}
else -> return@NavigationButton
}
},
)
}
private companion object {

View file

@ -1,6 +1,7 @@
package com.tangem.features.send.v2.sendnft.confirm.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@ -9,6 +10,7 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.footers.SendingText
import com.tangem.common.ui.notifications.NotificationUM
@ -21,6 +23,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.tapHelp
import com.tangem.features.send.v2.impl.R
@ -40,6 +43,7 @@ internal fun NFTSendConfirmContent(
destinationBlockComponent: DefaultSendDestinationBlockComponent,
nftDetailsBlockComponent: NFTDetailsBlockComponent,
feeBlockComponent: SendFeeBlockComponent,
feeSelectorBlockComponent: FeeSelectorBlockComponent,
notificationsComponent: DefaultSendNotificationsComponent,
notificationsUM: ImmutableList<NotificationUM>,
) {
@ -54,6 +58,7 @@ internal fun NFTSendConfirmContent(
destinationBlockComponent = destinationBlockComponent,
nftDetailsBlockComponent = nftDetailsBlockComponent,
feeBlockComponent = feeBlockComponent,
feeSelectorBlockComponent = feeSelectorBlockComponent,
)
if (confirmUM != null) {
tapHelp(isDisplay = confirmUM.showTapHelp)
@ -79,31 +84,45 @@ private fun LazyListScope.blocks(
destinationBlockComponent: DefaultSendDestinationBlockComponent,
nftDetailsBlockComponent: NFTDetailsBlockComponent,
feeBlockComponent: SendFeeBlockComponent,
feeSelectorBlockComponent: FeeSelectorBlockComponent,
) {
item(key = BLOCKS_KEY) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
AnimatedVisibility(
visible = nftSendUM.confirmUM is ConfirmUM.Success,
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
) {
val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success }
TransactionDoneTitle(
title = resourceReference(R.string.sent_transaction_sent_title),
subtitle = resourceReference(
R.string.send_date_format,
wrappedList(
wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter),
wrappedConfirmUM.transactionDate.toTimeFormat(),
),
),
modifier = Modifier.padding(vertical = 12.dp),
if (nftSendUM.isRedesignEnabled) {
nftDetailsBlockComponent.Content(modifier = Modifier)
destinationBlockComponent.Content(modifier = Modifier)
feeSelectorBlockComponent.Content(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
)
} else {
TransactionDoneTitleAnimated(nftSendUM = nftSendUM)
destinationBlockComponent.Content(modifier = Modifier)
nftDetailsBlockComponent.Content(modifier = Modifier)
feeBlockComponent.Content(modifier = Modifier)
}
destinationBlockComponent.Content(modifier = Modifier)
nftDetailsBlockComponent.Content(modifier = Modifier)
feeBlockComponent.Content(modifier = Modifier)
}
}
}
@Composable
private fun TransactionDoneTitleAnimated(nftSendUM: NFTSendUM) {
AnimatedVisibility(
visible = nftSendUM.confirmUM is ConfirmUM.Success,
modifier = Modifier.padding(vertical = 12.dp),
) {
val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success }
TransactionDoneTitle(
title = resourceReference(R.string.sent_transaction_sent_title),
subtitle = resourceReference(
R.string.send_date_format,
wrappedList(
wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter),
wrappedConfirmUM.transactionDate.toTimeFormat(),
),
),
modifier = Modifier.padding(vertical = 12.dp),
)
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel
import com.tangem.features.send.v2.sendnft.model.NFTSendModel
import com.tangem.features.send.v2.sendnft.success.model.NFTSendSuccessModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -23,4 +24,9 @@ internal interface NFTSendModelModule {
@IntoMap
@ClassKey(NFTSendConfirmModel::class)
fun provideNFTSendConfirmModel(model: NFTSendConfirmModel): Model
@Binds
@IntoMap
@ClassKey(NFTSendSuccessModel::class)
fun provideNFTSendSuccessModel(model: NFTSendSuccessModel): Model
}

View file

@ -29,6 +29,8 @@ import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.common.CommonSendRoute
@ -36,6 +38,7 @@ import com.tangem.features.send.v2.common.CommonSendRoute.*
import com.tangem.features.send.v2.common.SendConfirmAlertFactory
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent
import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
@ -70,7 +73,8 @@ internal class NFTSendModel @Inject constructor(
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val alertFactory: SendConfirmAlertFactory,
) : Model(), SendNFTComponentCallback {
private val sendFeatureToggles: SendFeatureToggles,
) : Model(), SendNFTComponentCallback, NFTSendSuccessComponent.ModelCallback {
val params: NFTSendComponent.Params = paramsContainer.require()
@ -124,7 +128,7 @@ internal class NFTSendModel @Inject constructor(
} else {
when (currentRouteFlow.value) {
is Destination -> router.push(Confirm)
Confirm -> router.push(ConfirmSuccess)
Confirm -> router.replaceAll(ConfirmSuccess)
else -> onBackClick()
}
}
@ -193,6 +197,13 @@ internal class NFTSendModel @Inject constructor(
}
}
fun showAlertError() {
alertFactory.getGenericErrorState(
onFailedTxEmailClick = ::onFailedTxEmailClick,
popBack = router::pop,
)
}
private fun onFailedTxEmailClick(errorMessage: String? = null) {
saveBlockchainErrorUseCase(
error = BlockchainErrorInfo(
@ -249,7 +260,9 @@ internal class NFTSendModel @Inject constructor(
private fun initialState(): NFTSendUM = NFTSendUM(
destinationUM = DestinationUM.Empty(),
feeUM = FeeUM.Empty(),
feeSelectorUM = FeeSelectorUM.Loading,
confirmUM = ConfirmUM.Empty,
navigationUM = NavigationUM.Empty,
isRedesignEnabled = sendFeatureToggles.isNFTSendRedesignEnabled,
)
}

View file

@ -0,0 +1,96 @@
package com.tangem.features.send.v2.sendnft.success
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.sendnft.success.model.NFTSendSuccessModel
import com.tangem.features.send.v2.sendnft.success.ui.NFTSendSuccessContent
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
internal class NFTSendSuccessComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Params,
nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory,
sendDestinationBlockComponentFactory: SendDestinationBlockComponent.Factory,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: NFTSendSuccessModel = getOrCreateModel(params = params)
private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create(
context = child("NFTDetailsSuccessBlock"),
params = NFTDetailsBlockComponent.Params(
userWalletId = params.userWallet.walletId,
nftAsset = params.nftAsset,
nftCollectionName = params.nftCollectionName,
isSuccessScreen = true,
title = resourceReference(R.string.nft_asset),
),
)
private val sendDestinationBlockComponent = sendDestinationBlockComponentFactory.create(
context = child("NFTDestinationSuccessBlock"),
params = DestinationBlockParams(
state = model.uiState.value.destinationUM,
analyticsCategoryName = params.analyticsCategoryName,
userWalletId = params.userWallet.walletId,
cryptoCurrency = params.cryptoCurrencyStatus.currency,
blockClickEnableFlow = MutableStateFlow(false),
predefinedValues = PredefinedValues.Empty,
),
onResult = {},
onClick = {},
)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
NFTSendSuccessContent(
nftSendUM = state,
destinationBlockComponent = sendDestinationBlockComponent,
nftDetailsBlockComponent = nftDetailsBlockComponent,
modifier = modifier,
)
}
data class Params(
val nftSendUMFlow: StateFlow<NFTSendUM>,
val analyticsCategoryName: String,
val currentRoute: Flow<CommonSendRoute>,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val userWallet: UserWallet,
val nftAsset: NFTAsset,
val nftCollectionName: String,
val txUrl: String,
val callback: ModelCallback,
)
interface ModelCallback {
fun onResult(nftSendUM: NFTSendUM)
}
@AssistedFactory
interface Factory {
fun create(appComponentContext: AppComponentContext, params: Params): NFTSendSuccessComponent
}
}

View file

@ -0,0 +1,107 @@
package com.tangem.features.send.v2.sendnft.success.model
import androidx.compose.runtime.Stable
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
@Stable
@ModelScoped
internal class NFTSendSuccessModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appRouter: AppRouter,
private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
) : Model() {
private val params: NFTSendSuccessComponent.Params = paramsContainer.require()
val uiState = params.nftSendUMFlow
init {
configConfirmSuccessNavigation()
}
private fun configConfirmSuccessNavigation() {
combine(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).filter { it.second is CommonSendRoute.ConfirmSuccess }.onEach { (state, _) ->
params.callback.onResult(
state.copy(
navigationUM = NavigationUM.Content(
title = stringReference(""),
subtitle = null,
backIconRes = R.drawable.ic_close_24,
backIconClick = {
analyticsEventHandler.send(
CommonSendAnalyticEvents.CloseButtonClicked(
categoryName = params.analyticsCategoryName,
source = SendScreenSource.Confirm,
isFromSummary = true,
isValid = true,
),
)
appRouter.pop()
},
primaryButton = NavigationButton(
textReference = resourceReference(R.string.common_close),
iconRes = null,
isEnabled = true,
isHapticClick = false,
onClick = {
appRouter.pop()
},
),
prevButton = null,
secondaryPairButtonsUM = NavigationButton(
textReference = resourceReference(R.string.common_explore),
iconRes = R.drawable.ic_web_24,
onClick = ::onExploreClick,
) to NavigationButton(
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_share_24,
onClick = ::onShareClick,
),
),
),
)
}.launchIn(modelScope)
}
private fun onExploreClick() {
analyticsEventHandler.send(CommonSendAnalyticEvents.ExploreButtonClicked(params.analyticsCategoryName))
urlOpener.openUrl(params.txUrl)
}
private fun onShareClick() {
analyticsEventHandler.send(CommonSendAnalyticEvents.ShareButtonClicked(params.analyticsCategoryName))
shareManager.shareText(params.txUrl)
}
interface ModelCallback {
fun onResult(sendUM: SendUM)
}
}

View file

@ -0,0 +1,103 @@
package com.tangem.features.send.v2.sendnft.success.ui
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toPx
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent
import com.tangem.features.send.v2.common.ui.FeeBlock
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import kotlinx.coroutines.delay
@Composable
internal fun NFTSendSuccessContent(
nftSendUM: NFTSendUM,
destinationBlockComponent: SendDestinationBlockComponent,
nftDetailsBlockComponent: NFTDetailsBlockComponent,
modifier: Modifier = Modifier,
) {
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
delay(ANIMATION_DELAY)
visible = true
}
val height = ANIMATION_OFFSET.toPx().toInt()
AnimatedVisibility(
visible = visible,
enter = slideInVertically(
initialOffsetY = { height },
).plus(fadeIn()),
exit = slideOutVertically().plus(fadeOut()),
label = "Animate success content",
modifier = modifier,
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.tertiary),
) {
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.scrollable(
state = rememberScrollState(),
orientation = Orientation.Vertical,
),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
if (nftSendUM.confirmUM is ConfirmUM.Success) {
TransactionDoneTitle(
title = resourceReference(R.string.sent_transaction_sent_title),
subtitle = resourceReference(
R.string.send_date_format,
wrappedList(
nftSendUM.confirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter),
nftSendUM.confirmUM.transactionDate.toTimeFormat(),
),
),
modifier = Modifier.padding(vertical = 12.dp),
)
}
nftDetailsBlockComponent.Content(modifier = Modifier)
destinationBlockComponent.Content(modifier = Modifier)
FeeBlock(feeSelectorUM = nftSendUM.feeSelectorUM)
Spacer(Modifier.height(60.dp))
}
BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary)
NavigationButtonsBlockV2(
navigationUM = nftSendUM.navigationUM,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(
start = 16.dp,
end = 16.dp,
bottom = 16.dp,
),
)
}
}
}
private const val ANIMATION_DELAY = 600L
private val ANIMATION_OFFSET = (-40).dp

View file

@ -1,13 +1,16 @@
package com.tangem.features.send.v2.sendnft.ui.state
import com.tangem.common.ui.navigationButtons.NavigationUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
internal data class NFTSendUM(
val destinationUM: DestinationUM,
val feeUM: FeeUM,
val feeSelectorUM: FeeSelectorUM,
val confirmUM: ConfirmUM,
val navigationUM: NavigationUM,
val isRedesignEnabled: Boolean,
)