Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-25 19:51:03 +05:00
parent ed25735b31
commit 5a5cfc0fac
16 changed files with 232 additions and 47 deletions

View file

@ -11,16 +11,20 @@ import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.value.ObserveLifecycleMode
import com.arkivanov.decompose.value.subscribe
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
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.common.CommonSendRoute
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.ui.SendContent
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.subcomponents.destination.SendDestinationComponent
@ -40,9 +44,11 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: NFTSendComponent.Params,
private val nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory,
private val analyticsEventHandler: AnalyticsEventHandler,
) : NFTSendComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<CommonSendRoute>()
private val analyticsCategoryName = CommonSendAnalyticEvents.NFT_SEND_CATEGORY
private val innerRouter = InnerRouter<CommonSendRoute>(
stackNavigation = stackNavigation,
@ -79,15 +85,21 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
componentScope.launch {
when (val activeComponent = stack.active.instance) {
is NFTSendConfirmComponent -> if (currentRouteFlow.value.isEditMode) {
// analyticsEventHandler.send(SendAnalyticEvents.ConfirmationScreenOpened)
analyticsEventHandler.send(
CommonSendAnalyticEvents.ConfirmationScreenOpened(categoryName = analyticsCategoryName),
)
activeComponent.updateState(model.uiState.value)
}
is SendDestinationComponent -> {
// analyticsEventHandler.send(SendAnalyticEvents.AddressScreenOpened)
analyticsEventHandler.send(
CommonSendAnalyticEvents.AddressScreenOpened(categoryName = analyticsCategoryName),
)
activeComponent.updateState(model.uiState.value.destinationUM)
}
is SendFeeComponent -> {
// analyticsEventHandler.send(SendAnalyticEvents.FeeScreenOpened)
analyticsEventHandler.send(
CommonSendAnalyticEvents.FeeScreenOpened(categoryName = analyticsCategoryName),
)
}
}
currentRouteFlow.emit(stack.active.configuration)
@ -121,7 +133,8 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
state = model.uiState.value.destinationUM,
currentRoute = currentRouteFlow.filterIsInstance<CommonSendRoute.Destination>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = "", // todo
title = resourceReference(R.string.nft_send),
analyticsCategoryName = analyticsCategoryName,
userWalletId = params.userWalletId,
cryptoCurrency = model.cryptoCurrency,
callback = model,
@ -145,7 +158,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
params = SendFeeComponentParams.FeeParams(
state = model.uiState.value.feeUM,
currentRoute = currentRouteFlow.filterIsInstance<CommonSendRoute.Fee>(),
analyticsCategoryName = "", // todo
analyticsCategoryName = analyticsCategoryName,
userWallet = model.userWallet,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus,
@ -167,7 +180,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
nftDetailsBlockComponentFactory = nftDetailsBlockComponentFactory,
params = NFTSendConfirmComponent.Params(
state = model.uiState.value,
analyticsCategoryName = "", // todo
analyticsCategoryName = analyticsCategoryName,
userWallet = model.userWallet,
nftAsset = params.nftAsset,
nftCollectionName = params.nftCollectionName,

View file

@ -0,0 +1,28 @@
package com.tangem.features.send.v2.sendnft.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
/**
* Send screen analytics
*/
internal sealed class NFTSendAnalyticEvents(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = CommonSendAnalyticEvents.NFT_SEND_CATEGORY, event = event, params = params) {
/** Transaction send screen opened */
data class TransactionScreenOpened(
val token: String,
val feeType: AnalyticsParam.FeeType,
) : NFTSendAnalyticEvents(
event = "NFT Sent Screen Opened",
params = mapOf(
TOKEN_PARAM to token,
FEE_TYPE to feeType.value,
),
)
}

View file

@ -0,0 +1,53 @@
package com.tangem.features.send.v2.sendnft.analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import javax.inject.Inject
@ModelScoped
internal class NFTSendAnalyticHelper @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) {
fun nftSendSuccessAnalytics(cryptoCurrency: CryptoCurrency, nftSendUM: NFTSendUM) {
val destinationUM = nftSendUM.destinationUM as? DestinationUM.Content
val feeUM = nftSendUM.feeUM as? FeeUM.Content
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content ?: return
val feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM)
analyticsEventHandler.send(
NFTSendAnalyticEvents.TransactionScreenOpened(
token = cryptoCurrency.symbol,
feeType = feeType,
),
)
analyticsEventHandler.send(
Basic.TransactionSent(
sentFrom = AnalyticsParam.TxSentFrom.NFT(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = feeType,
),
memoType = getSendTransactionMemoType(destinationUM?.memoTextField),
),
)
}
private fun getSendTransactionMemoType(
recipientMemo: DestinationTextFieldUM.RecipientMemo?,
): Basic.TransactionSent.MemoType {
val memo = recipientMemo?.value
return when {
memo?.isBlank() == true -> Basic.TransactionSent.MemoType.Empty
memo?.isNotBlank() == true -> Basic.TransactionSent.MemoType.Full
else -> Basic.TransactionSent.MemoType.Null
}
}
}

View file

@ -14,6 +14,11 @@ import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
import com.tangem.domain.settings.NeverShowTapHelpUseCase
import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase
@ -21,12 +26,14 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.SendBalanceUpdater
import com.tangem.features.send.v2.common.SendConfirmAlertFactory
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.NavigationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.features.send.v2.sendnft.analytics.NFTSendAnalyticHelper
import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent
import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmInitialStateTransformer
import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmSendingStateTransformer
@ -40,6 +47,7 @@ import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger
import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.stripZeroPlainString
import com.tangem.utils.transformer.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -47,7 +55,7 @@ import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
@ModelScoped
internal class NFTSendConfirmModel @Inject constructor(
paramsContainer: ParamsContainer,
@ -59,17 +67,23 @@ internal class NFTSendConfirmModel @Inject constructor(
private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val notificationsUpdateTrigger: NotificationsUpdateTrigger,
private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger,
private val sendFeeCheckReloadListener: SendFeeCheckReloadListener,
private val alertFactory: SendConfirmAlertFactory,
private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
private val analyticsEventHandler: AnalyticsEventHandler,
private val nftSendAnalyticHelper: NFTSendAnalyticHelper,
sendBalanceUpdaterFactory: SendBalanceUpdater.Factory,
) : Model(), NFTSendConfirmClickIntents {
private val params: NFTSendConfirmComponent.Params = paramsContainer.require()
private val analyticsCategoryName = params.analyticsCategoryName
private val userWallet = params.userWallet
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
private val cryptoCurrency = cryptoCurrencyStatus.currency
@ -129,7 +143,12 @@ internal class NFTSendConfirmModel @Inject constructor(
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
// analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = analyticsCategoryName,
source = SendScreenSource.Address,
),
)
router.push(CommonSendRoute.Destination(isEditMode = true))
}
}
@ -141,7 +160,12 @@ internal class NFTSendConfirmModel @Inject constructor(
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
// analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = analyticsCategoryName,
source = SendScreenSource.Fee,
),
)
router.push(CommonSendRoute.Fee)
}
}
@ -159,18 +183,38 @@ internal class NFTSendConfirmModel @Inject constructor(
override fun onExploreClick() {
val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return
// analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked)
analyticsEventHandler.send(
CommonSendAnalyticEvents.ExploreButtonClicked(analyticsCategoryName),
)
urlOpener.openUrl(confirmUM.txUrl)
}
override fun onShareClick() {
val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return
// analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked)
analyticsEventHandler.send(
CommonSendAnalyticEvents.ShareButtonClicked(analyticsCategoryName),
)
shareManager.shareText(confirmUM.txUrl)
}
override fun onFailedTxEmailClick(errorMessage: String) {
// TODO()
saveBlockchainErrorUseCase(
error = BlockchainErrorInfo(
errorMessage = errorMessage,
blockchainId = cryptoCurrency.network.id.value,
derivationPath = cryptoCurrency.network.derivationPath.value,
destinationAddress = confirmData.enteredDestination.orEmpty(),
tokenSymbol = null,
amount = params.nftAsset.amount.toString(),
fee = confirmData.fee?.amount?.value?.stripZeroPlainString(),
),
)
val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return
modelScope.launch {
sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo))
}
}
private fun initialState() {
@ -229,9 +273,9 @@ internal class NFTSendConfirmModel @Inject constructor(
ifLeft = { error ->
Timber.e(error)
_uiState.update(NFTSendConfirmSendingStateTransformer(isSending = false))
// alertFactory.getGenericErrorState {
// onFailedTxEmailClick(error.localizedMessage.orEmpty())
// }
alertFactory.getGenericErrorState {
onFailedTxEmailClick(error.localizedMessage.orEmpty())
}
},
ifRight = { txData ->
sendTransaction(txData)
@ -252,17 +296,22 @@ internal class NFTSendConfirmModel @Inject constructor(
result.fold(
ifLeft = { error ->
Timber.e(error.toString())
// alertFactory.getSendTransactionErrorState(
// error = error,
// popBack = appRouter::pop,
// onFailedTxEmailClick = ::onFailedTxEmailClick,
// )
// analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol))
alertFactory.getSendTransactionErrorState(
error = error,
popBack = appRouter::pop,
onFailedTxEmailClick = ::onFailedTxEmailClick,
)
analyticsEventHandler.send(
CommonSendAnalyticEvents.TransactionError(
categoryName = analyticsCategoryName,
token = cryptoCurrency.symbol,
),
)
},
ifRight = {
updateTransactionStatus(txData)
sendBalanceUpdater.scheduleUpdates()
// sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value)
nftSendAnalyticHelper.nftSendSuccessAnalytics(cryptoCurrency, uiState.value)
},
)
}
@ -307,6 +356,7 @@ internal class NFTSendConfirmModel @Inject constructor(
feeUM = uiState.value.feeUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
analyticsCategoryName = analyticsCategoryName,
).transform(uiState.value.confirmUM),
)
}
@ -331,7 +381,8 @@ internal class NFTSendConfirmModel @Inject constructor(
backIconRes = R.drawable.ic_close_24,
backIconClick = {
analyticsEventHandler.send(
SendAnalyticEvents.CloseButtonClicked(
CommonSendAnalyticEvents.CloseButtonClicked(
categoryName = analyticsCategoryName,
source = SendScreenSource.Confirm,
isFromSummary = true,
isValid = confirmUM.isPrimaryButtonEnabled,

View file

@ -5,8 +5,8 @@ import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
@ -18,6 +18,7 @@ internal class NFTSendConfirmationNotificationsTransformer(
private val feeUM: FeeUM,
private val analyticsEventHandler: AnalyticsEventHandler,
private val cryptoCurrency: CryptoCurrency,
private val analyticsCategoryName: String,
) : Transformer<ConfirmUM> {
override fun transform(prevState: ConfirmUM): ConfirmUM {
val state = prevState as? ConfirmUM.Content ?: return prevState
@ -39,7 +40,10 @@ internal class NFTSendConfirmationNotificationsTransformer(
if (feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue) {
add(NotificationUM.Warning.FeeTooLow)
analyticsEventHandler.send(
SendAnalyticEvents.NoticeTransactionDelays(cryptoCurrency.symbol),
CommonSendAnalyticEvents.NoticeTransactionDelays(
categoryName = analyticsCategoryName,
token = cryptoCurrency.symbol,
),
)
}
}

View file

@ -13,6 +13,11 @@ import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
@ -25,6 +30,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.send.v2.api.NFTSendComponent
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.common.ui.state.NavigationUM
import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent
@ -58,6 +64,10 @@ internal class NFTSendModel @Inject constructor(
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val alertFactory: SendConfirmAlertFactory,
) : Model(), SendNFTComponentCallback {
val params: NFTSendComponent.Params = paramsContainer.require()
@ -145,13 +155,33 @@ internal class NFTSendModel @Inject constructor(
)
},
ifLeft = {
// sendConfirmAlertFactory.getGenericErrorState(::onFailedTxEmailClick)
alertFactory.getGenericErrorState(::onFailedTxEmailClick)
return@launch
},
)
}
}
private fun onFailedTxEmailClick(errorMessage: String? = null) {
saveBlockchainErrorUseCase(
error = BlockchainErrorInfo(
errorMessage = errorMessage.orEmpty(),
blockchainId = cryptoCurrency.network.id.value,
derivationPath = cryptoCurrency.network.derivationPath.value,
destinationAddress = "",
tokenSymbol = null,
amount = "",
fee = "",
),
)
val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return
modelScope.launch {
sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo))
}
}
private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean) {
getCurrencyStatusUpdatesUseCase(
userWalletId = userWalletId,
@ -171,9 +201,9 @@ internal class NFTSendModel @Inject constructor(
}
},
ifLeft = {
// sendConfirmAlertFactory.getGenericErrorState {
// onFailedTxEmailClick(it.toString())
// }
alertFactory.getGenericErrorState {
onFailedTxEmailClick(it.toString())
}
},
)
}.launchIn(modelScope)