Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-05 12:45:38 +05:00
parent d71d29b72b
commit b750588ef9
21 changed files with 277 additions and 67 deletions

View file

@ -1,5 +1,6 @@
package com.tangem.features.onramp.main.model
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.res.stringResource
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
@ -19,6 +20,7 @@ import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampAvailability
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.OnrampMainComponent
@ -34,6 +36,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
@ -73,6 +76,8 @@ internal class OnrampMainComponentModel @Inject constructor(
val state: StateFlow<OnrampMainComponentUM> get() = _state.asStateFlow()
val bottomSheetNavigation: SlotNavigation<OnrampMainBottomSheetConfig> = SlotNavigation()
private val lastAmount = mutableStateOf(BigDecimal.ZERO)
init {
sendScreenOpenAnalytics()
checkResidenceCountry()
@ -83,7 +88,7 @@ internal class OnrampMainComponentModel @Inject constructor(
analyticsEventHandler.send(
OnrampAnalyticsEvent.ScreenOpened(
source = params.source,
cryptoCurrency = params.cryptoCurrency.name,
tokenSymbol = params.cryptoCurrency.symbol,
),
)
}
@ -143,6 +148,16 @@ internal class OnrampMainComponentModel @Inject constructor(
getOnrampQuotesUseCase.invoke()
.onEach { maybeQuotes ->
val quote = maybeQuotes.getOrNull()?.firstOrNull() ?: return@onEach
if (quote is OnrampQuote.Data && lastAmount.value != quote.fromAmount.value) {
lastAmount.value = quote.fromAmount.value
analyticsEventHandler.send(
OnrampAnalyticsEvent.ProviderCalculated(
providerName = quote.provider.info.name,
tokenSymbol = params.cryptoCurrency.symbol,
paymentMethod = quote.paymentMethod.name,
),
)
}
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote) }
}
.launchIn(modelScope)
@ -166,7 +181,7 @@ internal class OnrampMainComponentModel @Inject constructor(
OnrampAnalyticsEvent.OnBuyClick(
providerName = quote.provider.info.name,
currency = currentContentState.amountBlockState.currencyUM.code,
cryptoCurrency = params.cryptoCurrency.name,
tokenSymbol = params.cryptoCurrency.symbol,
),
)
params.openRedirectPage(quote)

View file

@ -108,7 +108,7 @@ internal class SelectProviderModel @Inject constructor(
}
private fun onPaymentMethodSelected(paymentMethod: OnrampPaymentMethod) {
analyticsEventHandler.send(OnrampAnalyticsEvent.OnPaymentMethodChosen)
analyticsEventHandler.send(OnrampAnalyticsEvent.OnPaymentMethodChosen(paymentMethod = paymentMethod.name))
modelScope.launch {
saveSelectedPaymentMethod.invoke(paymentMethod)
_state.update { state ->
@ -141,7 +141,7 @@ internal class SelectProviderModel @Inject constructor(
analyticsEventHandler.send(
OnrampAnalyticsEvent.OnProviderChosen(
providerName = quote.provider.info.name,
cryptoCurrency = params.cryptoCurrency.name,
tokenSymbol = params.cryptoCurrency.symbol,
),
)
params.onProviderClick(quote)

View file

@ -1,12 +1,14 @@
package com.tangem.features.onramp.success.model
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.onramp.GetOnrampStatusUseCase
import com.tangem.domain.onramp.GetOnrampTransactionUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
@ -31,6 +33,7 @@ internal class OnrampSuccessComponentModel @Inject constructor(
private val getOnrampStatusUseCase: GetOnrampStatusUseCase,
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
paramsContainer: ParamsContainer,
) : Model(), OnrampSuccessClickIntents {
@ -64,7 +67,7 @@ internal class OnrampSuccessComponentModel @Inject constructor(
}
private suspend fun loadTransactionStatus(transaction: OnrampTransaction) {
val cryptoCurrencies = getCryptoCurrencyUseCase(
val cryptoCurrency = getCryptoCurrencyUseCase(
transaction.userWalletId,
transaction.toCurrencyId,
).getOrElse { error("Crypto currency not found") }
@ -75,9 +78,18 @@ internal class OnrampSuccessComponentModel @Inject constructor(
Timber.e(it.toString())
},
ifRight = { status ->
analyticsEventHandler.send(
OnrampAnalyticsEvent.SuccessScreenOpened(
providerName = transaction.providerName,
currency = transaction.fromCurrency.code,
tokenSymbol = cryptoCurrency.symbol,
residence = transaction.residency,
paymentMethod = transaction.paymentMethod,
),
)
_state.update {
SetOnrampSuccessContentConverter(
cryptoCurrency = cryptoCurrencies,
cryptoCurrency = cryptoCurrency,
transaction = transaction,
goToProviderClick = ::goToProviderClick,
).convert(status)

View file

@ -33,6 +33,7 @@ internal sealed class ExpressTransactionStateUM {
val providerImageUrl: String, // todo onramp fix after SwapProvider moved to own module
val providerType: String, // todo onramp fix after SwapProvider moved to own module
val activeStatus: OnrampStatus.Status,
val fromCurrencyCode: String,
) : ExpressTransactionStateUM()
}

View file

@ -6,6 +6,7 @@ import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.common.ui.notifications.ExpressNotificationsUM
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.resourceReference
@ -21,6 +22,7 @@ import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateIconUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateInfoUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
@ -35,6 +37,7 @@ internal class TokenDetailsOnrampTransactionStateConverter(
private val cryptoCurrency: CryptoCurrency,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Converter<OnrampTransaction, ExpressTransactionStateUM.OnrampUM> {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
@ -46,7 +49,7 @@ internal class TokenDetailsOnrampTransactionStateConverter(
info = ExpressTransactionStateInfoUM(
title = resourceReference(id = R.string.express_status_buying, wrappedList(cryptoCurrency.name)),
status = convertStatuses(value.status, value.externalTxUrl),
notification = getNotification(value.status, value.externalTxUrl),
notification = getNotification(value.status, value.externalTxUrl, value.providerName),
txId = value.txId,
txExternalId = value.externalTxId,
txExternalUrl = value.externalTxUrl,
@ -86,20 +89,29 @@ internal class TokenDetailsOnrampTransactionStateConverter(
fallbackResId = R.drawable.ic_currency_24,
),
iconState = getIconState(value.status),
onGoToProviderClick = clickIntents::onGoToProviderClick,
onGoToProviderClick = {
analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider)
clickIntents.onGoToProviderClick(it)
},
onClick = { clickIntents.onExpressTransactionClick(value.txId) },
),
providerName = value.providerName,
providerImageUrl = value.providerImageUrl,
providerType = value.providerType,
activeStatus = value.status,
fromCurrencyCode = value.fromCurrency.code,
)
}
private fun getNotification(status: OnrampStatus.Status, externalTxUrl: String?): NotificationUM? {
private fun getNotification(
status: OnrampStatus.Status,
externalTxUrl: String?,
providerName: String,
): NotificationUM? {
if (externalTxUrl == null) return null
return when (status) {
OnrampStatus.Status.Verifying -> {
analyticsEventHandler.send(TokenOnrampAnalyticsEvent.NoticeKYC(cryptoCurrency.symbol, providerName))
ExpressNotificationsUM.NeedVerification {
clickIntents.onGoToProviderClick(externalTxUrl)
}

View file

@ -43,7 +43,7 @@ import java.util.Locale
internal class TokenDetailsSwapTransactionsStateConverter(
private val clickIntents: TokenDetailsClickIntents,
private val cryptoCurrency: CryptoCurrency,
private val analyticsEventsHandlerProvider: Provider<AnalyticsEventHandler>,
private val analyticsEventsHandler: AnalyticsEventHandler,
appCurrencyProvider: Provider<AppCurrency>,
) : Converter<Unit, PersistentList<ExpressTransactionStateUM.ExchangeUM>> {
@ -157,7 +157,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
fromAmountSymbol = fromCryptoCurrency.symbol,
onClick = { clickIntents.onExpressTransactionClick(transaction.txId) },
onGoToProviderClick = { url ->
analyticsEventsHandlerProvider().send(
analyticsEventsHandler.send(
TokenExchangeAnalyticsEvent.GoToProviderStatus(cryptoCurrency.symbol),
)
clickIntents.onGoToProviderClick(url = url)
@ -190,7 +190,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
ExchangeStatus.Failed -> {
if (txUrl == null) return null
ExchangeStatusNotifications.Failed {
analyticsEventsHandlerProvider().send(
analyticsEventsHandler.send(
TokenExchangeAnalyticsEvent.GoToProviderFail(cryptoCurrency.symbol),
)
clickIntents.onGoToProviderClick(txUrl)
@ -199,7 +199,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
ExchangeStatus.Verifying -> {
if (txUrl == null) return null
ExchangeStatusNotifications.NeedVerification {
analyticsEventsHandlerProvider().send(
analyticsEventsHandler.send(
TokenExchangeAnalyticsEvent.GoToProviderKYC(cryptoCurrency.symbol),
)
clickIntents.onGoToProviderClick(txUrl)

View file

@ -1,7 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.swaptx.ExchangeAnalyticsStatus
import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus
import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
@ -38,9 +38,9 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val swapTransactionStatusStore: SwapTransactionStatusStore,
private val analyticsEventsHandler: AnalyticsEventHandler,
@Assisted private val clickIntents: TokenDetailsClickIntents,
@Assisted private val appCurrencyProvider: Provider<AppCurrency>,
@Assisted private val analyticsEventsHandlerProvider: Provider<AnalyticsEventHandler>,
@Assisted private val currentStateProvider: Provider<TokenDetailsState>,
@Assisted private val userWalletId: UserWalletId,
@Assisted private val cryptoCurrency: CryptoCurrency,
@ -51,7 +51,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
clickIntents = clickIntents,
cryptoCurrency = cryptoCurrency,
appCurrencyProvider = appCurrencyProvider,
analyticsEventsHandlerProvider = analyticsEventsHandlerProvider,
analyticsEventsHandler = analyticsEventsHandler,
)
}
@ -132,7 +132,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
val savedStatus = swapTransactionStatusStore.getTransactionStatus(txId)
if (savedStatus != status) {
analyticsEventsHandlerProvider().send(
analyticsEventsHandler.send(
TokenExchangeAnalyticsEvent.CexTxStatusChanged(cryptoCurrency.symbol, status.value, provider.name),
)
swapTransactionStatusStore.setTransactionStatus(txId, status)
@ -183,22 +183,22 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
this == ExchangeStatus.Unknown
}
private fun toAnalyticStatus(status: ExchangeStatus?): ExchangeAnalyticsStatus? {
private fun toAnalyticStatus(status: ExchangeStatus?): ExpressAnalyticsStatus? {
return when (status) {
ExchangeStatus.New,
ExchangeStatus.Waiting,
ExchangeStatus.Sending,
ExchangeStatus.Confirming,
ExchangeStatus.Exchanging,
-> ExchangeAnalyticsStatus.InProgress
ExchangeStatus.WaitingTxHash -> ExchangeAnalyticsStatus.WaitingTxHash
ExchangeStatus.Verifying -> ExchangeAnalyticsStatus.KYC
ExchangeStatus.Failed -> ExchangeAnalyticsStatus.Fail
ExchangeStatus.TxFailed -> ExchangeAnalyticsStatus.FailTx
ExchangeStatus.Finished -> ExchangeAnalyticsStatus.Done
ExchangeStatus.Refunded -> ExchangeAnalyticsStatus.Refunded
ExchangeStatus.Cancelled -> ExchangeAnalyticsStatus.Cancelled
ExchangeStatus.Unknown -> ExchangeAnalyticsStatus.Unknown
-> ExpressAnalyticsStatus.InProgress
ExchangeStatus.WaitingTxHash -> ExpressAnalyticsStatus.WaitingTxHash
ExchangeStatus.Verifying -> ExpressAnalyticsStatus.KYC
ExchangeStatus.Failed -> ExpressAnalyticsStatus.Fail
ExchangeStatus.TxFailed -> ExpressAnalyticsStatus.FailTx
ExchangeStatus.Finished -> ExpressAnalyticsStatus.Done
ExchangeStatus.Refunded -> ExpressAnalyticsStatus.Refunded
ExchangeStatus.Cancelled -> ExpressAnalyticsStatus.Cancelled
ExchangeStatus.Unknown -> ExpressAnalyticsStatus.Unknown
else -> null
}
}
@ -216,7 +216,6 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
fun create(
clickIntents: TokenDetailsClickIntents,
appCurrencyProvider: Provider<AppCurrency>,
analyticsEventsHandlerProvider: Provider<AnalyticsEventHandler>,
currentStateProvider: Provider<TokenDetailsState>,
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,

View file

@ -6,6 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
@ -32,7 +33,6 @@ internal class ExpressStatusFactory @AssistedInject constructor(
@Assisted private val clickIntents: TokenDetailsClickIntents,
@Assisted private val cryptoCurrency: CryptoCurrency,
@Assisted appCurrencyProvider: Provider<AppCurrency>,
@Assisted analyticsEventsHandlerProvider: Provider<AnalyticsEventHandler>,
@Assisted userWalletId: UserWalletId,
@Assisted cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val dispatchers: CoroutineDispatcherProvider,
@ -45,7 +45,6 @@ internal class ExpressStatusFactory @AssistedInject constructor(
exchangeStatusFactory.create(
clickIntents = clickIntents,
appCurrencyProvider = appCurrencyProvider,
analyticsEventsHandlerProvider = analyticsEventsHandlerProvider,
currentStateProvider = currentStateProvider,
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
@ -108,7 +107,18 @@ internal class ExpressStatusFactory @AssistedInject constructor(
}
fun getStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TokenDetailsState {
analyticsEventsHandler.send(TokenExchangeAnalyticsEvent.CexTxStatusOpened(cryptoCurrency.symbol))
val analyticEvent = when (expressState) {
is ExpressTransactionStateUM.ExchangeUM -> TokenExchangeAnalyticsEvent.CexTxStatusOpened(
cryptoCurrency.symbol,
)
is ExpressTransactionStateUM.OnrampUM -> TokenOnrampAnalyticsEvent.OnrampStatusOpened(
tokenSymbol = cryptoCurrency.symbol,
provider = expressState.providerName,
fiatCurrency = expressState.fromCurrencyCode,
)
}
analyticsEventsHandler.send(analyticEvent)
return currentStateProvider().copy(
bottomSheetConfig = TangemBottomSheetConfig(
@ -152,7 +162,6 @@ internal class ExpressStatusFactory @AssistedInject constructor(
fun create(
clickIntents: TokenDetailsClickIntents,
appCurrencyProvider: Provider<AppCurrency>,
analyticsEventsHandlerProvider: Provider<AnalyticsEventHandler>,
currentStateProvider: Provider<TokenDetailsState>,
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,

View file

@ -1,11 +1,17 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.onramp.GetOnrampStatusUseCase
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.onramp.OnrampUpdateTransactionStatusUseCase
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.OnrampStatus.Status.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
@ -26,6 +32,8 @@ internal class OnrampStatusFactory @AssistedInject constructor(
private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase,
private val getOnrampStatusUseCase: GetOnrampStatusUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val onrampUpdateTransactionStatusUseCase: OnrampUpdateTransactionStatusUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
@Assisted private val currentStateProvider: Provider<TokenDetailsState>,
@Assisted private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
@Assisted private val appCurrencyProvider: Provider<AppCurrency>,
@ -40,6 +48,7 @@ internal class OnrampStatusFactory @AssistedInject constructor(
cryptoCurrency = cryptoCurrency,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
appCurrencyProvider = appCurrencyProvider,
analyticsEventHandler = analyticsEventHandler,
)
}
@ -79,6 +88,7 @@ internal class OnrampStatusFactory @AssistedInject constructor(
onrampTx
},
ifRight = { statusModel ->
sendStatusUpdateAnalytics(onrampTx, statusModel)
onrampTx.copy(activeStatus = statusModel.status)
},
)
@ -90,6 +100,44 @@ internal class OnrampStatusFactory @AssistedInject constructor(
.forEach { onrampRemoveTransactionUseCase(txId = it.info.txId) }
}
private suspend fun sendStatusUpdateAnalytics(
onrampTx: ExpressTransactionStateUM.OnrampUM,
statusModel: OnrampStatus,
) {
val txId = statusModel.txId
val status = toAnalyticStatus(statusModel.status) ?: return
if (statusModel.status != onrampTx.activeStatus) {
analyticsEventHandler.send(
TokenOnrampAnalyticsEvent.OnrampStatusChanged(
tokenSymbol = cryptoCurrency.symbol,
status = status.name,
provider = onrampTx.providerName,
fiatCurrency = onrampTx.fromCurrencyCode,
),
)
onrampUpdateTransactionStatusUseCase(txId, statusModel.status)
}
}
private fun toAnalyticStatus(status: OnrampStatus.Status?): ExpressAnalyticsStatus? {
return when (status) {
Expired,
Paused,
-> ExpressAnalyticsStatus.Cancelled
Created,
WaitingForPayment,
PaymentProcessing,
Paid,
Sending,
-> ExpressAnalyticsStatus.InProgress
Verifying -> ExpressAnalyticsStatus.KYC
Failed -> ExpressAnalyticsStatus.Fail
Finished -> ExpressAnalyticsStatus.Done
null -> null
}
}
@AssistedFactory
interface Factory {
fun create(

View file

@ -164,7 +164,6 @@ internal class TokenDetailsViewModel @Inject constructor(
expressStatusFactory.create(
clickIntents = this,
appCurrencyProvider = Provider { selectedAppCurrencyFlow.value },
analyticsEventsHandlerProvider = Provider { analyticsEventsHandler },
currentStateProvider = Provider { uiState.value },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
userWalletId = userWalletId,