Updated on 2026-08-14
This commit is contained in:
parent
b517fe39dd
commit
bad7b92f65
7 changed files with 154 additions and 37 deletions
|
|
@ -295,6 +295,7 @@ sealed class AnalyticsParam {
|
|||
const val REFERRAL = "Referral"
|
||||
const val REFERRAL_ID = "Referral_ID"
|
||||
const val SEARCHED = "Searched"
|
||||
const val RATE_TYPE = "Rate Type"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.swap.models
|
||||
|
||||
import com.tangem.domain.express.models.ExpressProvider
|
||||
import com.tangem.domain.express.models.ExpressRateType
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
|
||||
/**
|
||||
|
|
@ -50,4 +51,16 @@ data class SwapCurrenciesGroup(
|
|||
data class SwapCryptoCurrency(
|
||||
val currencyStatus: CryptoCurrencyStatus,
|
||||
val providers: List<ExpressProvider>,
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Get initial rate type based on available providers
|
||||
*/
|
||||
fun List<ExpressProvider>.getInitialRateType(): ExpressRateType {
|
||||
val availableRateTypes = this.flatMap { it.rateTypes }.toSet()
|
||||
return if (availableRateTypes.contains(ExpressRateType.Fixed)) {
|
||||
ExpressRateType.Fixed
|
||||
} else {
|
||||
ExpressRateType.Float
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,8 @@ import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderCompone
|
|||
import com.tangem.features.swap.v2.impl.common.SwapAlertFactory
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents.NoticeFixedRate.toAnalyticsRateType
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.Debouncer
|
||||
import com.tangem.utils.coroutines.PeriodicTask
|
||||
|
|
@ -117,6 +119,7 @@ internal class SwapAmountModel @Inject constructor(
|
|||
val rateInfoNavigation: SlotNavigation<ExpressRateType> = SlotNavigation()
|
||||
|
||||
private var isShowBestRateAnimation: Boolean = false
|
||||
private var lastAmountScreenOpenedCurrencyId: CryptoCurrency.ID? = null
|
||||
|
||||
private var autoUpdateSubscriberJob: Job? = null
|
||||
|
||||
|
|
@ -328,6 +331,11 @@ internal class SwapAmountModel @Inject constructor(
|
|||
override fun onRateClick() {
|
||||
val content = uiState.value as? SwapAmountUM.Content ?: return
|
||||
rateInfoNavigation.activate(content.swapRateType)
|
||||
val event = when (content.swapRateType) {
|
||||
ExpressRateType.Float -> SendWithSwapAnalyticEvents.NoticeFloatRate
|
||||
ExpressRateType.Fixed -> SendWithSwapAnalyticEvents.NoticeFixedRate
|
||||
}
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
|
||||
override fun onSeparatorClick() {
|
||||
|
|
@ -593,6 +601,7 @@ internal class SwapAmountModel @Inject constructor(
|
|||
if (currentState != null && currentState.swapRateMode != SwapRateMode.FLOAT_ONLY) {
|
||||
computeAndSetSecondaryAmount(currentState)
|
||||
}
|
||||
sendAmountScreenOpenedIfNeeded(secondaryStatus)
|
||||
startLoadingQuotesTask(isSilentReload = false)
|
||||
} else {
|
||||
@Suppress("NullableToStringCall")
|
||||
|
|
@ -603,11 +612,33 @@ internal class SwapAmountModel @Inject constructor(
|
|||
| Secondary -> $secondaryStatus
|
||||
""".trimIndent(),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
SendWithSwapAnalyticEvents.SendWithSwapError(
|
||||
errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Amount,
|
||||
message = "Invalid cryptocurrencies status: primary=$primaryStatus, secondary=$secondaryStatus",
|
||||
),
|
||||
)
|
||||
showErrorAlert(errorMessage = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendAmountScreenOpenedIfNeeded(secondaryStatus: CryptoCurrencyStatus) {
|
||||
val currencyId = secondaryStatus.currency.id
|
||||
if (lastAmountScreenOpenedCurrencyId == currencyId) return
|
||||
lastAmountScreenOpenedCurrencyId = currencyId
|
||||
|
||||
val content = uiState.value as? SwapAmountUM.Content ?: return
|
||||
|
||||
analyticsEventHandler.send(
|
||||
SendWithSwapAnalyticEvents.AmountScreenOpened(
|
||||
rateType = content.swapRateType.toAnalyticsRateType(),
|
||||
fromToken = content.primaryCryptoCurrencyStatus.currency,
|
||||
toToken = secondaryStatus.currency,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun initCurrencies(primaryStatus: CryptoCurrencyStatus, secondaryStatus: CryptoCurrencyStatus?) {
|
||||
primaryMinimumAmountBoundary = EnterAmountBoundary(
|
||||
amount = getMinimumTransactionAmountSyncUseCase
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.domain.swap.models.SwapCurrencies
|
|||
import com.tangem.domain.swap.models.SwapDirection
|
||||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.domain.swap.models.SwapRateMode
|
||||
import com.tangem.domain.swap.models.getInitialRateType
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
|
|
@ -52,11 +53,7 @@ internal class SwapAmountSecondaryReadyStateTransformer(
|
|||
rateTypes.contains(ExpressRateType.Fixed) -> SwapRateMode.FIXED_ONLY
|
||||
else -> SwapRateMode.FLOAT_ONLY
|
||||
}
|
||||
val selectedAmountType = if (swapRateMode != SwapRateMode.FLOAT_ONLY) {
|
||||
ExpressRateType.Fixed
|
||||
} else {
|
||||
ExpressRateType.Float
|
||||
}
|
||||
val selectedRateType = providers.getInitialRateType()
|
||||
return SwapAmountUM.Content(
|
||||
isPrimaryButtonEnabled = false,
|
||||
primaryAmount = prevState.primaryAmount,
|
||||
|
|
@ -68,13 +65,13 @@ internal class SwapAmountSecondaryReadyStateTransformer(
|
|||
),
|
||||
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
|
||||
swapCurrencies = swapCurrencies,
|
||||
selectedAmountType = if (selectedAmountType == ExpressRateType.Fixed) {
|
||||
selectedAmountType = if (selectedRateType == ExpressRateType.Fixed) {
|
||||
SwapAmountType.To
|
||||
} else {
|
||||
SwapAmountType.From
|
||||
},
|
||||
swapDirection = swapDirection,
|
||||
swapRateType = selectedAmountType,
|
||||
swapRateType = selectedRateType,
|
||||
swapQuotes = persistentListOf(),
|
||||
selectedQuote = SwapQuoteUM.Empty,
|
||||
appCurrency = appCurrency,
|
||||
|
|
|
|||
|
|
@ -20,11 +20,9 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter
|
|||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.swap.models.R
|
||||
import com.tangem.domain.swap.models.SwapDirection
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.v2.api.entry.SendEntryRoute
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.DestinationRoute
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams
|
||||
|
|
@ -34,6 +32,8 @@ import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams
|
|||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.common.SwapUtils.SEND_WITH_SWAP_PROVIDER_TYPES
|
||||
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents.NoticeFixedRate.toAnalyticsRateType
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.model.SendWithSwapModel
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.success.SendWithSwapSuccessComponent
|
||||
|
|
@ -87,17 +87,6 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
|
|||
componentScope.launch {
|
||||
when (val activeComponent = stack.active.instance) {
|
||||
is SwapAmountComponent -> {
|
||||
if (
|
||||
params.currentRoute.value is SendEntryRoute.SendWithSwap &&
|
||||
model.currentRoute.value != stack.active.configuration
|
||||
) {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.AmountScreenOpened(
|
||||
categoryName = model.analyticCategoryName,
|
||||
source = model.analyticsSendSource,
|
||||
),
|
||||
)
|
||||
}
|
||||
activeComponent.updateState(model.uiState.value.amountUM)
|
||||
}
|
||||
is SendDestinationComponent -> {
|
||||
|
|
@ -110,25 +99,23 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
|
|||
activeComponent.updateState(model.uiState.value.destinationUM)
|
||||
}
|
||||
is SendWithSwapConfirmComponent -> {
|
||||
val fromCurrency = params.currency
|
||||
val fromDerivationIndex = when (val account = model.accountFlow.value) {
|
||||
is Account.CryptoPortfolio -> account.derivationIndex.value
|
||||
is Account.Payment -> TODO("[REDACTED_JIRA]")
|
||||
null -> null
|
||||
}.takeIf { model.isAccountModeFlow.value }
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.ConfirmationScreenOpened(
|
||||
categoryName = model.analyticCategoryName,
|
||||
source = model.analyticsSendSource,
|
||||
sendBlockchain = fromCurrency.network.name,
|
||||
sendToken = fromCurrency.symbol,
|
||||
fromDerivationIndex = fromDerivationIndex,
|
||||
toDerivationIndex = null,
|
||||
),
|
||||
)
|
||||
if (model.currentRoute.value.isEditMode) {
|
||||
activeComponent.updateState(model.uiState.value)
|
||||
}
|
||||
val fromCurrency = params.currency
|
||||
val content = model.uiState.value.amountUM as? SwapAmountUM.Content ?: return@launch
|
||||
val toCurrency = content.secondaryCryptoCurrencyStatus?.currency ?: return@launch
|
||||
val rateType = content.swapRateType.toAnalyticsRateType()
|
||||
val providerName = content.selectedQuote.provider?.name.orEmpty()
|
||||
|
||||
analyticsEventHandler.send(
|
||||
SendWithSwapAnalyticEvents.ConfirmationScreenOpened(
|
||||
providerName = providerName,
|
||||
rateType = rateType,
|
||||
fromToken = fromCurrency,
|
||||
toToken = toCurrency,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
model.currentRoute.emit(stack.active.configuration)
|
||||
|
|
|
|||
|
|
@ -4,13 +4,16 @@ import com.tangem.core.analytics.models.AnalyticsEvent
|
|||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_TO
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_BLOCKCHAIN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.RATE_TYPE
|
||||
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
|
||||
import com.tangem.domain.express.models.ExpressRateType
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
|
||||
|
||||
|
|
@ -19,6 +22,40 @@ internal sealed class SendWithSwapAnalyticEvents(
|
|||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category = CommonSendAnalyticEvents.SEND_CATEGORY, event = event, params = params) {
|
||||
|
||||
/** Confirmation screen opened */
|
||||
data class ConfirmationScreenOpened(
|
||||
val providerName: String,
|
||||
val rateType: RateType,
|
||||
val fromToken: CryptoCurrency,
|
||||
val toToken: CryptoCurrency,
|
||||
) : SendWithSwapAnalyticEvents(
|
||||
event = "Send With Swap Confirm Screen Opened",
|
||||
params = buildMap {
|
||||
put(SEND_TOKEN, fromToken.symbol)
|
||||
put(RECEIVE_TOKEN, toToken.symbol)
|
||||
put(SEND_BLOCKCHAIN, fromToken.network.name)
|
||||
put(RECEIVE_BLOCKCHAIN, toToken.network.name)
|
||||
put(RATE_TYPE, rateType.name)
|
||||
put(PROVIDER, providerName)
|
||||
},
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
/** Amount screen opened */
|
||||
data class AmountScreenOpened(
|
||||
val rateType: RateType,
|
||||
val fromToken: CryptoCurrency,
|
||||
val toToken: CryptoCurrency,
|
||||
) : SendWithSwapAnalyticEvents(
|
||||
event = "Send With Swap Amount Screen Opened",
|
||||
params = buildMap {
|
||||
put(SEND_TOKEN, fromToken.symbol)
|
||||
put(RECEIVE_TOKEN, toToken.symbol)
|
||||
put(SEND_BLOCKCHAIN, fromToken.network.name)
|
||||
put(RECEIVE_BLOCKCHAIN, toToken.network.name)
|
||||
put(RATE_TYPE, rateType.name)
|
||||
},
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
data class TransactionScreenOpened(
|
||||
val providerName: String,
|
||||
val feeType: AnalyticsParam.FeeType,
|
||||
|
|
@ -72,4 +109,42 @@ internal sealed class SendWithSwapAnalyticEvents(
|
|||
SEND_BLOCKCHAIN to fromToken.network.name,
|
||||
),
|
||||
)
|
||||
|
||||
data object NoticeFixedRate : SendWithSwapAnalyticEvents(
|
||||
event = "Notice - Fixed Rate",
|
||||
params = emptyMap(),
|
||||
)
|
||||
|
||||
data object NoticeFloatRate : SendWithSwapAnalyticEvents(
|
||||
event = "Notice - Float Rate",
|
||||
params = emptyMap(),
|
||||
)
|
||||
|
||||
data class SendWithSwapError(
|
||||
val errorScreen: ErrorScreen,
|
||||
val message: String,
|
||||
) : SendWithSwapAnalyticEvents(
|
||||
event = when (errorScreen) {
|
||||
ErrorScreen.Amount -> "Send With Swap Amount Screen Error"
|
||||
ErrorScreen.Confirm -> "Send With Swap Confirm Screen Error"
|
||||
},
|
||||
params = mapOf(
|
||||
ERROR_MESSAGE to message,
|
||||
),
|
||||
)
|
||||
|
||||
enum class ErrorScreen {
|
||||
Amount,
|
||||
Confirm,
|
||||
}
|
||||
|
||||
enum class RateType {
|
||||
Float,
|
||||
Fixed,
|
||||
}
|
||||
|
||||
fun ExpressRateType.toAnalyticsRateType(): RateType = when (this) {
|
||||
ExpressRateType.Float -> RateType.Float
|
||||
ExpressRateType.Fixed -> RateType.Fixed
|
||||
}
|
||||
}
|
||||
|
|
@ -310,6 +310,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun onSendClick() {
|
||||
val provider = confirmData.quote?.provider ?: return
|
||||
modelScope.launch {
|
||||
|
|
@ -322,6 +323,12 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
onExpressError = { expressError ->
|
||||
uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false))
|
||||
analyticsEventHandler.send(
|
||||
SendWithSwapAnalyticEvents.SendWithSwapError(
|
||||
errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Confirm,
|
||||
message = "Express error: $expressError",
|
||||
),
|
||||
)
|
||||
swapAlertFactory.getGenericErrorState(
|
||||
expressError = expressError,
|
||||
onFailedTxEmailClick = {
|
||||
|
|
@ -339,6 +346,12 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
},
|
||||
onSendError = { error ->
|
||||
uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false))
|
||||
analyticsEventHandler.send(
|
||||
SendWithSwapAnalyticEvents.SendWithSwapError(
|
||||
errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Confirm,
|
||||
message = "Send error: ${error?.toString().orEmpty()}",
|
||||
),
|
||||
)
|
||||
swapAlertFactory.getSendTransactionErrorState(
|
||||
error = error,
|
||||
onFailedTxEmailClick = { _ ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue