Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-29 14:54:47 +05:00
parent e910d4cb3e
commit 126913bf0f
9 changed files with 115 additions and 132 deletions

View file

@ -1,9 +1,12 @@
package com.tangem.features.send.impl.presentation.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.VALIDATION
import com.tangem.core.analytics.models.AnalyticsParam.OnOffState
/**
* Send screen analytics
@ -13,24 +16,9 @@ internal sealed class SendAnalyticEvents(
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Token / Send", event = event, params = params) {
/** Send screen opened */
object SendOpened : SendAnalyticEvents(event = "Send Screen Opened")
/** Next button clicked */
data class NextButtonClicked(val source: SendScreenSource) : SendAnalyticEvents(
event = "Button - Next",
params = mapOf(SOURCE to source.name),
)
/** Back button clicked */
data class BackButtonClicked(val source: SendScreenSource) : SendAnalyticEvents(
event = "Button - Back",
params = mapOf(SOURCE to source.name),
)
// region Address
/** Recipient address screen opened */
object AddressScreenOpened : SendAnalyticEvents(event = "Address Screen Opened")
data object AddressScreenOpened : SendAnalyticEvents(event = "Address Screen Opened")
/** Address to send entered */
data class AddressEntered(val source: EnterAddressSource, val isValid: Boolean) : SendAnalyticEvents(
@ -41,21 +29,13 @@ internal sealed class SendAnalyticEvents(
),
)
/** Paste from clipboard button clicked */
data class PasteButtonClicked(val type: PasteType) : SendAnalyticEvents(
event = "Button - Paste",
params = mapOf(
TYPE to type.name,
),
)
/** Qr Code button clicked */
object QrCodeButtonClicked : SendAnalyticEvents(event = "Button - QR Code")
data object QrCodeButtonClicked : SendAnalyticEvents(event = "Button - QR Code")
// endregion
// region Amount
/** Amount screen opened */
object AmountScreenOpened : SendAnalyticEvents(event = "Amount Screen Opened")
data object AmountScreenOpened : SendAnalyticEvents(event = "Amount Screen Opened")
/** Selected currency */
data class SelectedCurrency(val type: SelectedCurrencyType) : SendAnalyticEvents(
@ -63,36 +43,36 @@ internal sealed class SendAnalyticEvents(
params = mapOf(TYPE to type.value),
)
/** Currency selector button clicked */
object SwapCurrencyButtonClicked : SendAnalyticEvents(event = "Button - Swap Currency")
/** Max amount button clicked */
data object MaxAmountButtonClicked : SendAnalyticEvents(event = "Max Amount Taped")
// endregion
// region Fee
/** Fee screen opened */
object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened")
data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened")
/** Selected fee (send after next screen opened) */
data class SelectedFee(val fee: String) : SendAnalyticEvents(
data class SelectedFee(val feeType: SelectedFeeType) : SendAnalyticEvents(
event = "Fee Selected",
params = mapOf("Commission" to fee),
params = mapOf("Fee Type" to feeType.name),
)
/** Custom fee selected */
object CustomFeeButtonClicked : SendAnalyticEvents(event = "Custom Fee Clicked")
data object CustomFeeButtonClicked : SendAnalyticEvents(event = "Custom Fee Clicked")
/** Custom fee edited */
object GasPriceInserter : SendAnalyticEvents(event = "Gas Price Inserted")
data object GasPriceInserter : SendAnalyticEvents(event = "Gas Price Inserted")
/** Subtract from amount selector switched (send after next screen opened) */
object SubtractFromAmount : SendAnalyticEvents(event = "Subtract from Amount")
data class SubtractFromAmount(val status: Boolean) : SendAnalyticEvents(
event = "Subtract from Amount",
params = mapOf("Status" to if (status) OnOffState.On.value else OnOffState.Off.value),
)
// endregion
// region Confirmation
/** Confirmation screen opened */
object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened")
/** Send transaction button clicked */
object SendButtonClicked : SendAnalyticEvents(event = "Button - Send")
data object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened")
/** Screen reopened from confirmation screen */
data class ScreenReopened(val source: SendScreenSource) : SendAnalyticEvents(
@ -103,13 +83,31 @@ internal sealed class SendAnalyticEvents(
// region Transaction Result
/** Transaction send screen opened */
object TransactionScreenOpened : SendAnalyticEvents(event = "Transaction Sent Screen Opened")
data object TransactionScreenOpened : SendAnalyticEvents(event = "Transaction Sent Screen Opened")
/** Share button clicked */
object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share")
data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share")
/** Expore button clicked */
object ExploreButtonClicked : SendAnalyticEvents(event = "Button - Explore")
data object ExploreButtonClicked : SendAnalyticEvents(event = "Button - Explore")
/** If not enough fee notification is present */
data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SendAnalyticEvents(
event = "Notice - Not Enough Fee",
params = mapOf(TOKEN to token, BLOCKCHAIN to blockchain),
)
/** If transaction delays notification is present */
data class NoticeTransactionDelays(val token: String) : SendAnalyticEvents(
event = "Notice - Transaction Delays Are Possible",
params = mapOf(TOKEN to token),
)
/** If error occurs during send transactions */
data class TransactionError(val token: String) : SendAnalyticEvents(
event = "Error - Transaction Rejected",
params = mapOf(TOKEN to token),
)
// endregion
}
@ -125,12 +123,15 @@ internal enum class EnterAddressSource {
RecentAddress,
}
internal enum class PasteType {
Address,
Memo,
}
internal enum class SelectedCurrencyType(val value: String) {
Token("Token"),
AppCurrency("App Currency"),
}
internal enum class SelectedFeeType {
Min,
Max,
Fixed,
Normal,
Custom,
}

View file

@ -1,7 +1,9 @@
package com.tangem.features.send.impl.presentation.analytics.utils
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.features.send.impl.presentation.analytics.SelectedCurrencyType
import com.tangem.features.send.impl.presentation.analytics.SelectedFeeType
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
@ -21,12 +23,12 @@ internal class SendOnNextScreenAnalyticSender(
if (selectedFee == FeeType.Custom && isCustomFeeEdited) {
analyticsEventHandler.send(SendAnalyticEvents.GasPriceInserter)
}
analyticsEventHandler.send(SendAnalyticEvents.SelectedFee(selectedFee.name))
sendSelectedFeeAnalytics(feeSelectorState)
}
}
SendUiStateType.Amount -> {
val isFiatSelected = state.amountState?.amountTextField?.isFiatValue ?: return
val selectedCurrency = if (isFiatSelected) {
val selectedCurrency = if (!isFiatSelected) {
SelectedCurrencyType.Token
} else {
SelectedCurrencyType.AppCurrency
@ -38,4 +40,17 @@ internal class SendOnNextScreenAnalyticSender(
else -> Unit
}
}
private fun sendSelectedFeeAnalytics(feeSelectorState: FeeSelectorState.Content) {
val type = when (feeSelectorState.fees) {
is TransactionFee.Single -> SelectedFeeType.Fixed
is TransactionFee.Choosable -> when (feeSelectorState.selectedFee) {
FeeType.Slow -> SelectedFeeType.Min
FeeType.Market -> SelectedFeeType.Normal
FeeType.Fast -> SelectedFeeType.Max
FeeType.Custom -> SelectedFeeType.Custom
}
}
analyticsEventHandler.send(SendAnalyticEvents.SelectedFee(type))
}
}

View file

@ -1,26 +0,0 @@
package com.tangem.features.send.impl.presentation.analytics.utils
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
import com.tangem.features.send.impl.presentation.analytics.PasteType
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
internal class SendRecipientAnalyticsSender(
private val analyticsEventHandler: AnalyticsEventHandler,
) {
fun sendAddressAnalytics(type: EnterAddressSource?, isValidAddress: Boolean) {
type?.let {
if (type == EnterAddressSource.PasteButton) {
analyticsEventHandler.send(SendAnalyticEvents.PasteButtonClicked(PasteType.Address))
}
analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress))
}
}
fun sendMemoAnalytics(isPasted: Boolean) {
if (isPasted) {
analyticsEventHandler.send(SendAnalyticEvents.PasteButtonClicked(PasteType.Memo))
}
}
}

View file

@ -2,12 +2,14 @@ package com.tangem.features.send.impl.presentation.state
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
@ -30,6 +32,7 @@ internal class SendNotificationFactory(
private val currencyChecksRepository: CurrencyChecksRepository,
private val stateRouterProvider: Provider<StateRouter>,
private val clickIntents: SendClickIntents,
private val analyticsEventHandler: AnalyticsEventHandler,
) {
fun create(): Flow<ImmutableList<SendNotification>> = stateRouterProvider().currentState
@ -285,6 +288,11 @@ internal class SendNotificationFactory(
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
if (feeSelectorState.selectedFee == FeeType.Custom && minimumValue > customValue) {
add(SendNotification.Warning.FeeTooLow)
analyticsEventHandler.send(
SendAnalyticEvents.NoticeTransactionDelays(
cryptoCurrencyStatusProvider().currency.symbol,
),
)
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.features.send.impl.presentation.state
import androidx.fragment.app.FragmentManager
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.analytics.SendScreenSource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
@ -32,51 +31,26 @@ internal class StateRouter(
when {
isSuccess -> popBackStack()
isEditingDisabled -> when (type) {
SendUiStateType.Send -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee))
showFee()
}
SendUiStateType.Send -> showFee()
else -> popBackStack()
}
else -> when (type) {
SendUiStateType.Amount -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Address))
continueToSend(::showRecipient)
}
SendUiStateType.Fee -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Amount))
continueToSend(::showAmount)
}
SendUiStateType.Send -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee))
continueToSend(::showFee)
}
SendUiStateType.Amount -> continueToSend(::showRecipient)
SendUiStateType.Fee -> continueToSend(::showAmount)
SendUiStateType.Send -> continueToSend(::showFee)
else -> continueToSend(::popBackStack)
}
}
}
fun onNextClick(): SendUiStateType {
val prevState = currentState.value.type
fun onNextClick() {
when (currentState.value.type) {
SendUiStateType.Recipient -> {
analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Amount))
continueToSend(::showAmount)
}
SendUiStateType.Amount -> {
analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Fee))
continueToSend(::showFee)
}
SendUiStateType.Fee -> {
analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Fee))
showSend()
}
SendUiStateType.Send -> {
onBackClick()
}
SendUiStateType.Recipient -> continueToSend(::showAmount)
SendUiStateType.Amount -> continueToSend(::showFee)
SendUiStateType.Fee -> showSend()
SendUiStateType.Send -> onBackClick()
else -> popBackStack()
}
return prevState
}
fun onPrevClick() {
@ -84,14 +58,8 @@ internal class StateRouter(
popBackStack()
} else {
when (currentState.value.type) {
SendUiStateType.Amount -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Amount))
showRecipient()
}
SendUiStateType.Fee -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee))
showAmount()
}
SendUiStateType.Amount -> showRecipient()
SendUiStateType.Fee -> showAmount()
else -> popBackStack()
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.state.fee
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.common.extensions.fromNetworkId
@ -11,6 +12,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
import com.tangem.features.send.impl.presentation.state.StateRouter
@ -33,6 +35,7 @@ internal class FeeNotificationFactory(
private val stateRouterProvider: Provider<StateRouter>,
private val clickIntents: SendClickIntents,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
) {
fun create() = stateRouterProvider().currentState
@ -97,7 +100,7 @@ internal class FeeNotificationFactory(
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> {
add(
SendFeeNotification.Error.ExceedsBalance(
warning.coinCurrency.networkIconResId,
networkIconId = warning.coinCurrency.networkIconResId,
networkName = warning.coinCurrency.name,
currencyName = cryptoCurrencyStatus.currency.name,
feeName = warning.coinCurrency.name,
@ -111,6 +114,12 @@ internal class FeeNotificationFactory(
},
),
)
analyticsEventHandler.send(
SendAnalyticEvents.NoticeNotEnoughFee(
token = cryptoCurrencyStatus.currency.symbol,
blockchain = cryptoCurrencyStatus.currency.network.name,
),
)
}
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> {
val currency = warning.feeCurrency
@ -132,6 +141,12 @@ internal class FeeNotificationFactory(
},
),
)
analyticsEventHandler.send(
SendAnalyticEvents.NoticeNotEnoughFee(
token = warning.currency.symbol,
blockchain = warning.networkName,
),
)
}
else -> Unit
}

View file

@ -78,7 +78,7 @@ internal fun SendRecipientContent(uiState: SendStates.RecipientState?, clickInte
placeholder = placeholder,
footer = stringResource(R.string.send_recipient_memo_footer),
onValueChange = memoField.onValueChange,
onPasteClick = { clickIntents.onRecipientMemoValueChange(it, isPasted = true) },
onPasteClick = clickIntents::onRecipientMemoValueChange,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),
isError = memoField.isError,
error = memoField.error,

View file

@ -34,7 +34,7 @@ internal interface SendClickIntents {
// region Recipient
fun onRecipientAddressValueChange(value: String, type: EnterAddressSource? = null)
fun onRecipientMemoValueChange(value: String, isPasted: Boolean = false)
fun onRecipientMemoValueChange(value: String)
// endregion
// region Fee

View file

@ -37,7 +37,6 @@ import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.analytics.SendScreenSource
import com.tangem.features.send.impl.presentation.analytics.utils.SendOnNextScreenAnalyticSender
import com.tangem.features.send.impl.presentation.analytics.utils.SendRecipientAnalyticsSender
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.*
import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory
@ -143,6 +142,7 @@ internal class SendViewModel @Inject constructor(
stateRouterProvider = Provider { stateRouter },
clickIntents = this,
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
analyticsEventHandler = analyticsEventHandler,
)
private val sendNotificationFactory = SendNotificationFactory(
@ -153,14 +153,13 @@ internal class SendViewModel @Inject constructor(
stateRouterProvider = Provider { stateRouter },
currencyChecksRepository = currencyChecksRepository,
clickIntents = this,
analyticsEventHandler = analyticsEventHandler,
)
private val sendOnNextScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) {
SendOnNextScreenAnalyticSender(analyticsEventHandler)
}
private val sendRecipientAnalyticsSender by lazy(LazyThreadSafetyMode.NONE) {
SendRecipientAnalyticsSender(analyticsEventHandler)
}
// todo convert to StateFlow
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
private set
@ -188,7 +187,6 @@ internal class SendViewModel @Inject constructor(
override fun onCreate(owner: LifecycleOwner) {
onStateActive()
analyticsEventHandler.send(SendAnalyticEvents.SendOpened)
}
override fun onCleared() {
@ -415,11 +413,12 @@ internal class SendViewModel @Inject constructor(
return
} else {
uiState = stateFactory.onSubtractSelect(false)
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
}
}
val prevScreen = stateRouter.onNextClick()
sendOnNextScreenAnalyticSender.send(prevScreen, uiState)
sendOnNextScreenAnalyticSender.send(currentState.type, uiState)
stateRouter.onNextClick()
}
override fun onPrevClick() = stateRouter.onPrevClick()
@ -439,7 +438,6 @@ internal class SendViewModel @Inject constructor(
// region amount state clicks
override fun onCurrencyChangeClick(isFiat: Boolean) {
analyticsEventHandler.send(SendAnalyticEvents.SwapCurrencyButtonClicked)
uiState = amountStateFactory.getOnCurrencyChangedState(isFiat)
}
@ -449,6 +447,7 @@ internal class SendViewModel @Inject constructor(
override fun onMaxValueClick() {
uiState = amountStateFactory.getOnMaxAmountClick()
analyticsEventHandler.send(SendAnalyticEvents.MaxAmountButtonClicked)
}
// endregion
@ -475,19 +474,18 @@ internal class SendViewModel @Inject constructor(
uiState = stateFactory.getOnRecipientAddressValidationStarted()
val isValidAddress = validateAddress(value)
uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress)
sendRecipientAnalyticsSender.sendAddressAnalytics(type, isValidAddress)
type?.let { analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress)) }
}
}.saveIn(addressValidationJobHolder)
}
override fun onRecipientMemoValueChange(value: String, isPasted: Boolean) {
override fun onRecipientMemoValueChange(value: String) {
viewModelScope.launch(dispatchers.main) {
if (!checkIfXrpAddressValue(value)) {
uiState = stateFactory.getOnRecipientMemoValueChange(value)
uiState = stateFactory.getOnRecipientAddressValidationStarted()
val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty())
uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress)
sendRecipientAnalyticsSender.sendMemoAnalytics(isPasted)
}
}.saveIn(addressValidationJobHolder)
}
@ -542,6 +540,7 @@ internal class SendViewModel @Inject constructor(
override fun onSubtractSelect() {
uiState = stateFactory.onSubtractSelect(true)
stateRouter.showSend()
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(true))
}
override fun onReadMoreClick() {
@ -598,25 +597,27 @@ internal class SendViewModel @Inject constructor(
onCheckFeeUpdate()
}
sendIdleTimer = System.currentTimeMillis()
analyticsEventHandler.send(SendAnalyticEvents.SendButtonClicked)
}
override fun showAmount() {
uiState = stateFactory.onSubtractSelect(false)
stateRouter.showAmount(isFromConfirmation = true)
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
}
override fun showRecipient() {
uiState = stateFactory.onSubtractSelect(false)
stateRouter.showRecipient(isFromConfirmation = true)
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
}
override fun showFee() {
uiState = stateFactory.onSubtractSelect(false)
stateRouter.showFee(isFromConfirmation = true)
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
}
override fun showSend() {
@ -691,6 +692,7 @@ internal class SendViewModel @Inject constructor(
error = error,
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol))
},
ifRight = {
uiState = stateFactory.getSendingStateUpdate(isSending = false)