Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-09 16:32:30 +03:00
commit 81cdcd0f38
19 changed files with 363 additions and 56 deletions

View file

@ -125,6 +125,7 @@ sealed class AnalyticsParam {
const val SOURCE = "Source"
const val BALANCE = "Balance"
const val BATCH = "Batch"
const val TYPE = "Type"
const val FEE_TYPE = "Fee Type"
const val PERMISSION_TYPE = "Permission Type"
const val PRODUCT_TYPE = "Product Type"

View file

@ -45,6 +45,8 @@ dependencies {
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.navigation)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
/** Common */
implementation(projects.common)

View file

@ -8,6 +8,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
@ -41,6 +42,9 @@ internal class SendFragment : ComposeFragment() {
@Inject
lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase
@Inject
lateinit var analyticsEventsHandler: AnalyticsEventHandler
private val viewModel by viewModels<SendViewModel>()
private val innerSendRouter: InnerSendRouter
get() = requireNotNull(router as? InnerSendRouter) {
@ -57,6 +61,7 @@ internal class SendFragment : ComposeFragment() {
StateRouter(
fragmentManager = WeakReference(parentFragmentManager),
isEditingDisabled = isEditingDisabled,
analyticsEventsHandler = analyticsEventsHandler,
),
)
listenToQrCode()

View file

@ -0,0 +1,136 @@
package com.tangem.features.send.impl.presentation.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.VALIDATION
/**
* Send screen analytics
*/
internal sealed class SendAnalyticEvents(
event: String,
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")
/** Address to send entered */
data class AddressEntered(val source: EnterAddressSource, val isValid: Boolean) : SendAnalyticEvents(
event = "Address Entered",
params = mapOf(
SOURCE to source.name,
VALIDATION to if (isValid) "Success" else "Fail",
),
)
/** 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")
// endregion
// region Amount
/** Amount screen opened */
object AmountScreenOpened : SendAnalyticEvents(event = "Amount Screen Opened")
/** Selected currency */
data class SelectedCurrency(val type: SelectedCurrencyType) : SendAnalyticEvents(
event = "Selected Currency",
params = mapOf(TYPE to type.value),
)
/** Currency selector button clicked */
object SwapCurrencyButtonClicked : SendAnalyticEvents(event = "Button - Swap Currency")
// endregion
// region Fee
/** Fee screen opened */
object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened")
/** Selected fee (send after next screen opened) */
data class SelectedFee(val fee: String) : SendAnalyticEvents(
event = "Fee Selected",
params = mapOf("Commission" to fee),
)
/** Custom fee selected */
object CustomFeeButtonClicked : SendAnalyticEvents(event = "Custom Fee Clicked")
/** Custom fee edited */
object GasPriceInserter : SendAnalyticEvents(event = "Gas Price Inserted")
/** Subtract from amount selector switched (send after next screen opened) */
object SubtractFromAmount : SendAnalyticEvents(event = "Subtract from Amount")
// endregion
// region Confirmation
/** Confirmation screen opened */
object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened")
/** Send transaction button clicked */
object SendButtonClicked : SendAnalyticEvents(event = "Button - Send")
/** Screen reopened from confirmation screen */
data class ScreenReopened(val source: SendScreenSource) : SendAnalyticEvents(
event = "Screen Reopened",
params = mapOf(SOURCE to source.name),
)
// endregion
// region Transaction Result
/** Transaction send screen opened */
object TransactionScreenOpened : SendAnalyticEvents(event = "Transaction Sent Screen Opened")
/** Share button clicked */
object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share")
/** Expore button clicked */
object ExploreButtonClicked : SendAnalyticEvents(event = "Button - Explore")
// endregion
}
internal enum class SendScreenSource {
Address,
Amount,
Fee,
}
internal enum class EnterAddressSource {
QRCode,
PasteButton,
RecentAddress,
}
internal enum class PasteType {
Address,
Memo,
}
internal enum class SelectedCurrencyType(val value: String) {
Token("Token"),
AppCurrency("App Currency"),
}

View file

@ -0,0 +1,44 @@
package com.tangem.features.send.impl.presentation.analytics.utils
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.features.send.impl.presentation.analytics.SelectedCurrencyType
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.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
internal class SendOnNextScreenAnalyticSender(
private val analyticsEventHandler: AnalyticsEventHandler,
) {
fun send(prevScreen: SendUiStateType, state: SendUiState) {
when (prevScreen) {
SendUiStateType.Fee -> {
val feeState = state.feeState ?: return
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content
feeSelectorState?.selectedFee?.let { selectedFee ->
val isCustomFeeEdited = feeState.fee?.amount?.value != feeSelectorState.fees.normal.amount.value
if (selectedFee == FeeType.Custom && isCustomFeeEdited) {
analyticsEventHandler.send(SendAnalyticEvents.GasPriceInserter)
}
analyticsEventHandler.send(SendAnalyticEvents.SelectedFee(selectedFee.name))
}
if (feeState.isSubtract) {
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount)
}
}
SendUiStateType.Amount -> {
val isFiatSelected = state.amountState?.amountTextField?.isFiatValue ?: return
val selectedCurrency = if (isFiatSelected) {
SelectedCurrencyType.Token
} else {
SelectedCurrencyType.AppCurrency
}
analyticsEventHandler.send(
SendAnalyticEvents.SelectedCurrency(selectedCurrency),
)
}
else -> Unit
}
}
}

View file

@ -0,0 +1,26 @@
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

@ -50,10 +50,10 @@ internal class SendEventStateFactory(
is TransactionFee.Single -> fee.normal
is TransactionFee.Choosable -> {
when (feeSelector.selectedFee) {
FeeType.SLOW -> fee.minimum
FeeType.MARKET -> fee.normal
FeeType.FAST -> fee.priority
FeeType.CUSTOM -> return state
FeeType.Slow -> fee.minimum
FeeType.Market -> fee.normal
FeeType.Fast -> fee.priority
FeeType.Custom -> return state
}
}
}

View file

@ -1,6 +1,9 @@
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
@ -8,6 +11,7 @@ import java.lang.ref.WeakReference
internal class StateRouter(
private val fragmentManager: WeakReference<FragmentManager>,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val isEditingDisabled: Boolean,
) {
private var mutableCurrentState: MutableStateFlow<SendUiStateType> = MutableStateFlow(
@ -28,26 +32,51 @@ internal class StateRouter(
when {
isSuccess -> popBackStack()
isEditingDisabled -> when (currentState.value) {
SendUiStateType.Send -> showFee()
SendUiStateType.Send -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee))
showFee()
}
else -> popBackStack()
}
else -> when (currentState.value) {
SendUiStateType.Amount -> showRecipient()
SendUiStateType.Fee -> showAmount()
SendUiStateType.Send -> showFee()
SendUiStateType.Amount -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Address))
showRecipient()
}
SendUiStateType.Fee -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Amount))
showAmount()
}
SendUiStateType.Send -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee))
showFee()
}
else -> popBackStack()
}
}
}
fun onNextClick() {
fun onNextClick(): SendUiStateType {
val prevState = currentState.value
when (currentState.value) {
SendUiStateType.Recipient -> showAmount()
SendUiStateType.Amount -> showFee()
SendUiStateType.Fee -> showSend()
SendUiStateType.Send -> onBackClick()
SendUiStateType.Recipient -> {
analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Amount))
showAmount()
}
SendUiStateType.Amount -> {
analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Fee))
showFee()
}
SendUiStateType.Fee -> {
analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Fee))
showSend()
}
SendUiStateType.Send -> {
onBackClick()
}
else -> popBackStack()
}
return prevState
}
fun onPrevClick() {
@ -55,26 +84,36 @@ internal class StateRouter(
popBackStack()
} else {
when (currentState.value) {
SendUiStateType.Amount -> showRecipient()
SendUiStateType.Fee -> showAmount()
SendUiStateType.Amount -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Amount))
showRecipient()
}
SendUiStateType.Fee -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee))
showAmount()
}
else -> popBackStack()
}
}
}
fun showAmount() {
analyticsEventsHandler.send(SendAnalyticEvents.AmountScreenOpened)
mutableCurrentState.update { SendUiStateType.Amount }
}
fun showRecipient() {
analyticsEventsHandler.send(SendAnalyticEvents.AddressScreenOpened)
mutableCurrentState.update { SendUiStateType.Recipient }
}
fun showFee() {
analyticsEventsHandler.send(SendAnalyticEvents.FeeScreenOpened)
mutableCurrentState.update { SendUiStateType.Fee }
}
private fun showSend() {
analyticsEventsHandler.send(SendAnalyticEvents.ConfirmationScreenOpened)
mutableCurrentState.update { SendUiStateType.Send }
}
}

View file

@ -28,10 +28,10 @@ internal class FeeConverter(
return when (val fees = value.fees) {
is TransactionFee.Choosable -> {
when (value.selectedFee) {
FeeType.SLOW -> fees.minimum
FeeType.MARKET -> fees.normal
FeeType.FAST -> fees.priority
FeeType.CUSTOM -> convertCustom(value, fees)
FeeType.Slow -> fees.minimum
FeeType.Market -> fees.normal
FeeType.Fast -> fees.priority
FeeType.Custom -> convertCustom(value, fees)
}
}
is TransactionFee.Single -> fees.normal

View file

@ -69,7 +69,7 @@ internal class FeeNotificationFactory(
val minimumValue = multipleFees.minimum.amount.value ?: return
val customAmount = customFee.firstOrNull() ?: return
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
if (selectedFee == FeeType.CUSTOM && minimumValue > customValue) {
if (selectedFee == FeeType.Custom && minimumValue > customValue) {
add(SendFeeNotification.Warning.TooLow)
}
}
@ -84,7 +84,7 @@ internal class FeeNotificationFactory(
val customAmount = customFee.firstOrNull() ?: return
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
val diff = customValue / highValue
if (selectedFee == FeeType.CUSTOM && diff > FEE_MAX_DIFF) {
if (selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF) {
add(SendFeeNotification.Warning.TooHigh(diff.toFormattedString(HIGH_FEE_DIFF_DECIMALS)))
}
}

View file

@ -13,7 +13,7 @@ internal sealed class FeeSelectorState {
data class Content(
val fees: TransactionFee,
val selectedFee: FeeType = FeeType.MARKET,
val selectedFee: FeeType = FeeType.Market,
val customValues: ImmutableList<SendTextField.CustomFee> = persistentListOf(),
) : FeeSelectorState()
@ -21,8 +21,8 @@ internal sealed class FeeSelectorState {
}
enum class FeeType {
SLOW,
MARKET,
FAST,
CUSTOM,
Slow,
Market,
Fast,
Custom,
}

View file

@ -191,7 +191,7 @@ internal class FeeStateFactory(
val fee = feeConverter.convert(feeSelectorState)
val feeValue = fee.amount.value ?: BigDecimal.ZERO
val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM
val isNotCustom = feeSelectorState.selectedFee != FeeType.Custom
val isNotEmptyCustom = if (customValue != null) {
!customValue.value.parseToBigDecimal(customValue.decimals).isZero() && !isNotCustom
} else {

View file

@ -110,6 +110,7 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier
textRes = textId,
txUrl = txUrl,
onExploreClick = { uiState.clickIntents.onExploreClick(txUrl) },
onShareClick = uiState.clickIntents::onShareClick,
onDoneClick = buttonClick,
modifier = Modifier,
)
@ -130,6 +131,7 @@ private fun PrimaryButtonsDone(
@StringRes textRes: Int,
txUrl: String,
onExploreClick: () -> Unit,
onShareClick: () -> Unit,
onDoneClick: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -152,6 +154,7 @@ private fun PrimaryButtonsDone(
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(txUrl)
onShareClick()
},
modifier = Modifier.weight(1f),
)

View file

@ -20,7 +20,7 @@ internal fun SendCustomFeeEthereum(
selectedFee: FeeType,
modifier: Modifier = Modifier,
) {
if (selectedFee == FeeType.CUSTOM && customValues.isNotEmpty()) {
if (selectedFee == FeeType.Custom && customValues.isNotEmpty()) {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = modifier,

View file

@ -175,7 +175,7 @@ internal fun LazyListScope.subtractButton(
item {
val feeStateContent = feeSendState as? FeeSelectorState.Content
val isCustomAvailable = feeStateContent?.customValues.isNullOrEmpty().not()
val isCustomSelected = feeStateContent?.selectedFee == FeeType.CUSTOM
val isCustomSelected = feeStateContent?.selectedFee == FeeType.Custom
val topPadding = if (isCustomSelected && isCustomAvailable) {
TangemTheme.dimens.spacing12
} else {

View file

@ -88,8 +88,8 @@ internal fun SendSpeedSelector(
amount = getCryptoReference(minimumAmount, state.isFeeApproximate),
fiatAmount = getFiatReference(minimumAmount, state.rate, state.appCurrency),
symbolLength = minimumAmount.currencySymbol.length,
isSelected = isSelected == FeeType.SLOW,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.SLOW) },
isSelected = isSelected == FeeType.Slow,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Slow) },
)
val normalAmount = fees.normal.amount
SendSpeedSelectorItem(
@ -98,8 +98,8 @@ internal fun SendSpeedSelector(
amount = getCryptoReference(normalAmount, state.isFeeApproximate),
fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency),
symbolLength = normalAmount.currencySymbol.length,
isSelected = isSelected == FeeType.MARKET,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) },
isSelected = isSelected == FeeType.Market,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) },
)
val priorityAmount = fees.priority.amount
SendSpeedSelectorItem(
@ -108,8 +108,8 @@ internal fun SendSpeedSelector(
amount = getCryptoReference(priorityAmount, state.isFeeApproximate),
fiatAmount = getFiatReference(priorityAmount, state.rate, state.appCurrency),
symbolLength = priorityAmount.currencySymbol.length,
isSelected = isSelected == FeeType.FAST,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.FAST) },
isSelected = isSelected == FeeType.Fast,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Fast) },
showDivider = fees.normal is Fee.Ethereum,
)
AnimatedVisibility(
@ -123,8 +123,8 @@ internal fun SendSpeedSelector(
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_custom,
iconRes = R.drawable.ic_edit_24,
isSelected = isSelected == FeeType.CUSTOM,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.CUSTOM) },
isSelected = isSelected == FeeType.Custom,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Custom) },
showDivider = fees.normal !is Fee.Ethereum,
showWarning = showWarning,
)
@ -139,7 +139,7 @@ internal fun SendSpeedSelector(
amount = getCryptoReference(normalAmount, state.isFeeApproximate),
fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency),
symbolLength = normalAmount.currencySymbol.length,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) },
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) },
showDivider = false,
)
}

View file

@ -24,6 +24,7 @@ import com.tangem.core.ui.components.inputrow.InputRowRecipient
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
@ -58,7 +59,7 @@ internal fun SendRecipientContent(
title = address.label,
placeholder = address.placeholder,
onValueChange = address.onValueChange,
onPasteClick = clickIntents::onRecipientAddressValueChange,
onPasteClick = { clickIntents.onRecipientAddressValueChange(it, EnterAddressSource.PasteButton) },
isError = isError,
isLoading = isValidating,
error = address.error,
@ -80,7 +81,7 @@ internal fun SendRecipientContent(
placeholder = placeholder,
footer = stringResource(R.string.send_recipient_memo_footer),
onValueChange = memoField.onValueChange,
onPasteClick = clickIntents::onRecipientMemoValueChange,
onPasteClick = { clickIntents.onRecipientMemoValueChange(it, isPasted = true) },
modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),
isError = memoField.isError,
error = memoField.error,
@ -165,7 +166,9 @@ private fun LazyListScope.recipientListItem(
},
)
.background(TangemTheme.colors.background.action),
onClick = { clickIntents.onRecipientAddressValueChange(title) },
onClick = {
clickIntents.onRecipientAddressValueChange(title, EnterAddressSource.RecentAddress)
},
)
}
}
@ -203,7 +206,7 @@ private fun RecipientWalletListItem(
ListItemWithIcon(
title = wallet.title.resolveReference(),
subtitle = wallet.subtitle.resolveReference(),
onClick = { clickIntents.onRecipientAddressValueChange(title) },
onClick = { clickIntents.onRecipientAddressValueChange(title, EnterAddressSource.RecentAddress) },
)
}
if (!item.isWalletsOnly) {

View file

@ -2,10 +2,11 @@ package com.tangem.features.send.impl.presentation.viewmodel
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
import com.tangem.features.send.impl.presentation.state.fee.FeeType
@Suppress("TooManyFunctions")
interface SendClickIntents {
internal interface SendClickIntents {
fun popBackStack()
@ -30,9 +31,9 @@ interface SendClickIntents {
// endregion
// region Recipient
fun onRecipientAddressValueChange(value: String)
fun onRecipientAddressValueChange(value: String, type: EnterAddressSource? = null)
fun onRecipientMemoValueChange(value: String)
fun onRecipientMemoValueChange(value: String, isPasted: Boolean = false)
// endregion
// region Fee
@ -56,6 +57,8 @@ interface SendClickIntents {
fun onExploreClick(txUrl: String)
fun onShareClick()
fun onAmountReduceClick(reducedAmount: String)
fun onAmountReduceIgnoreClick()

View file

@ -9,6 +9,7 @@ import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
@ -34,6 +35,11 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.*
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
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
@ -75,6 +81,7 @@ internal class SendViewModel @Inject constructor(
private val reduxStateHolder: ReduxStateHolder,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
currencyChecksRepository: CurrencyChecksRepository,
isFeeApproximateUseCase: IsFeeApproximateUseCase,
getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
@ -147,6 +154,12 @@ internal class SendViewModel @Inject constructor(
clickIntents = this,
)
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
@ -170,6 +183,7 @@ internal class SendViewModel @Inject constructor(
subscribeOnCurrencyStatusUpdates(owner)
onStateActive()
subscribeOnBalanceHidden(owner)
analyticsEventHandler.send(SendAnalyticEvents.SendOpened)
}
fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) {
@ -273,11 +287,11 @@ internal class SendViewModel @Inject constructor(
if (transactionId != null && amount != null && destinationAddress != null) {
uiState = stateFactory.getReadyState(amount, destinationAddress)
showFee()
stateRouter.showFee()
} else {
getWalletsAndRecent()
uiState = stateFactory.getReadyState()
showRecipient()
stateRouter.showRecipient()
}
updateNotifications()
}
@ -392,10 +406,17 @@ internal class SendViewModel @Inject constructor(
// region screen state navigation
override fun popBackStack() = stateRouter.popBackStack()
override fun onBackClick() = stateRouter.onBackClick(uiState.sendState.isSuccess)
override fun onNextClick() = stateRouter.onNextClick()
override fun onNextClick() {
val prevScreen = stateRouter.onNextClick()
sendOnNextScreenAnalyticSender.send(prevScreen, uiState)
}
override fun onPrevClick() = stateRouter.onPrevClick()
override fun onQrCodeScanClick() = innerRouter.openQrCodeScanner(cryptoCurrency.network.name)
override fun onQrCodeScanClick() {
analyticsEventHandler.send(SendAnalyticEvents.QrCodeButtonClicked)
innerRouter.openQrCodeScanner(cryptoCurrency.network.name)
}
override fun onFailedTxEmailClick(errorMessage: String) {
reduxStateHolder.dispatch(LegacyAction.SendEmailTransactionFailed(errorMessage))
@ -407,6 +428,7 @@ internal class SendViewModel @Inject constructor(
// region amount state clicks
override fun onCurrencyChangeClick(isFiat: Boolean) {
analyticsEventHandler.send(SendAnalyticEvents.SwapCurrencyButtonClicked)
uiState = amountStateFactory.getOnCurrencyChangedState(isFiat)
}
@ -424,7 +446,7 @@ internal class SendViewModel @Inject constructor(
viewModelScope.launch(dispatchers.main) {
parseSharedAddressUseCase(address, cryptoCurrency.network).fold(
ifRight = { parsedCode ->
onRecipientAddressValueChange(parsedCode.address)
onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode)
parsedCode.amount?.let { onAmountValueChange(it.toPlainString()) }
parsedCode.memo?.let { onRecipientMemoValueChange(it) }
},
@ -435,24 +457,26 @@ internal class SendViewModel @Inject constructor(
}.saveIn(qrScannerJobHolder)
}
override fun onRecipientAddressValueChange(value: String) {
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {
viewModelScope.launch(dispatchers.main) {
if (!checkIfXrpAddressValue(value)) {
uiState = stateFactory.onRecipientAddressValueChange(value)
uiState = stateFactory.getOnRecipientAddressValidationStarted()
val isValidAddress = validateAddress(value)
uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress)
sendRecipientAnalyticsSender.sendAddressAnalytics(type, isValidAddress)
}
}.saveIn(addressValidationJobHolder)
}
override fun onRecipientMemoValueChange(value: String) {
override fun onRecipientMemoValueChange(value: String, isPasted: Boolean) {
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)
}
@ -482,6 +506,9 @@ internal class SendViewModel @Inject constructor(
override fun onFeeSelectorClick(feeType: FeeType) {
uiState = feeStateFactory.onFeeSelectedState(feeType)
updateFeeNotifications()
if (feeType == FeeType.Custom) {
analyticsEventHandler.send(SendAnalyticEvents.CustomFeeButtonClicked)
}
}
override fun onCustomFeeValueChange(index: Int, value: String) {
@ -542,15 +569,32 @@ internal class SendViewModel @Inject constructor(
onCheckFeeUpdate()
}
sendIdleTimer = System.currentTimeMillis()
analyticsEventHandler.send(SendAnalyticEvents.SendButtonClicked)
}
override fun showAmount() = stateRouter.showAmount()
override fun showAmount() {
stateRouter.showAmount()
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
}
override fun showRecipient() = stateRouter.showRecipient()
override fun showRecipient() {
stateRouter.showRecipient()
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
}
override fun showFee() = stateRouter.showFee()
override fun showFee() {
stateRouter.showFee()
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
}
override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl)
override fun onExploreClick(txUrl: String) {
analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked)
innerRouter.openUrl(txUrl)
}
override fun onShareClick() {
analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked)
}
override fun onAmountReduceClick(reducedAmount: String) {
uiState = amountStateFactory.getOnAmountValueChange(reducedAmount)
@ -616,6 +660,7 @@ internal class SendViewModel @Inject constructor(
uiState = stateFactory.getSendingStateUpdate(isSending = false)
uiState = stateFactory.getTransactionSendState(txData)
scheduleBalanceUpdate()
analyticsEventHandler.send(SendAnalyticEvents.TransactionScreenOpened)
},
)
}