Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-09 11:02:33 +05:00
parent ed0b07bc82
commit f4422dd3fb
25 changed files with 274 additions and 115 deletions

View file

@ -200,12 +200,10 @@ sealed class AnalyticsParam {
const val INPUT = "Input"
const val COUNT = "Count"
const val DERIVATION = "Derivation"
// region swap
const val TOKEN_CATEGORY = "Token"
const val STATUS = "Status"
const val PROVIDER = "Provider"
const val PLACE = "Place"
//
const val RESIDENCE = "Residence"
const val PAYMENT_METHOD = "Payment Method"
}
}

View file

@ -1,20 +1,17 @@
package com.tangem.data.onramp
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.onramp.converters.error.OnrampErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.model.error.OnrampRedirectError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
internal class DefaultOnrampErrorResolver(
private val analyticsEventHandler: AnalyticsEventHandler,
private val onrampErrorConverter: OnrampErrorConverter,
) : OnrampErrorResolver {
override fun resolve(throwable: Throwable): OnrampError {
val error = when (throwable) {
return when (throwable) {
is ApiResponseError.HttpException -> {
onrampErrorConverter.convert(throwable.errorBody.orEmpty())
}
@ -24,20 +21,5 @@ internal class DefaultOnrampErrorResolver(
OnrampError.DomainError(throwable.message)
}
}
when (error) {
is OnrampError.AmountError.TooBigError -> analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError)
is OnrampError.AmountError.TooSmallError -> analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError)
is OnrampError.DataError -> analyticsEventHandler.send(
OnrampAnalyticsEvent.Errors(
tokenSymbol = "tokenSymbol",
providerName = "providerName",
errorCode = error.code,
),
)
else -> { /* no-op */
}
}
return error
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.data.onramp.di
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
@ -72,13 +71,9 @@ internal object OnrampDataModule {
@Provides
@Singleton
fun provideOnrampErrorResolver(
@NetworkMoshi moshi: Moshi,
analyticsEventHandler: AnalyticsEventHandler,
): OnrampErrorResolver {
fun provideOnrampErrorResolver(@NetworkMoshi moshi: Moshi): OnrampErrorResolver {
val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java)
return DefaultOnrampErrorResolver(
analyticsEventHandler,
OnrampErrorConverter(jsonAdapter),
)
}

View file

@ -6,6 +6,7 @@ sealed class OnrampError {
sealed class AmountError : OnrampError() {
abstract val requiredAmount: BigDecimal
data class TooSmallError(
override val requiredAmount: BigDecimal,
) : AmountError()
@ -25,5 +26,7 @@ sealed class OnrampError {
val description: String?,
) : OnrampError()
data class DomainError(val error: String?) : OnrampError()
data class DomainError(
val description: String?,
) : OnrampError()
}

View file

@ -1,6 +1,14 @@
package com.tangem.domain.onramp.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.CURRENCY
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_DESCRIPTION
import com.tangem.core.analytics.models.AnalyticsParam.Key.PAYMENT_METHOD
import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER
import com.tangem.core.analytics.models.AnalyticsParam.Key.RESIDENCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
import com.tangem.domain.onramp.model.OnrampSource
sealed class OnrampAnalyticsEvent(
@ -18,8 +26,8 @@ sealed class OnrampAnalyticsEvent(
) : OnrampAnalyticsEvent(
event = "Buy Screen Opened",
params = mapOf(
"Source" to source.analyticsName,
"Token" to tokenSymbol,
SOURCE to source.analyticsName,
TOKEN_PARAM to tokenSymbol,
),
)
@ -29,7 +37,7 @@ sealed class OnrampAnalyticsEvent(
private val currency: String,
) : OnrampAnalyticsEvent(
event = "Currency Chosen",
params = mapOf("Currency" to currency),
params = mapOf(CURRENCY to currency),
)
data object CloseOnramp : OnrampAnalyticsEvent(event = "Button - Close")
@ -42,14 +50,14 @@ sealed class OnrampAnalyticsEvent(
private val residence: String,
) : OnrampAnalyticsEvent(
event = "Residence Chosen",
params = mapOf("Residence" to residence),
params = mapOf(RESIDENCE to residence),
)
data class ResidenceConfirmScreenOpened(
private val residence: String,
) : OnrampAnalyticsEvent(
event = "Residence Confirm Screen",
params = mapOf("Residence" to residence),
params = mapOf(RESIDENCE to residence),
)
data object OnResidenceChange : OnrampAnalyticsEvent(event = "Button - Change")
@ -58,7 +66,7 @@ sealed class OnrampAnalyticsEvent(
private val residence: String,
) : OnrampAnalyticsEvent(
event = "Button - Confirm",
params = mapOf("Residence" to residence),
params = mapOf(RESIDENCE to residence),
)
data object ProvidersScreenOpened : OnrampAnalyticsEvent(event = "Providers Screen Opened")
@ -70,9 +78,9 @@ sealed class OnrampAnalyticsEvent(
) : OnrampAnalyticsEvent(
event = "Provider Calculated",
params = mapOf(
"Provider" to providerName,
"Token" to tokenSymbol,
"Payment Method" to paymentMethod,
PROVIDER to providerName,
TOKEN_PARAM to tokenSymbol,
PAYMENT_METHOD to paymentMethod,
),
)
@ -83,7 +91,7 @@ sealed class OnrampAnalyticsEvent(
) : OnrampAnalyticsEvent(
event = "Method Chosen",
params = mapOf(
"Payment Method" to paymentMethod,
PAYMENT_METHOD to paymentMethod,
),
)
@ -93,8 +101,8 @@ sealed class OnrampAnalyticsEvent(
) : OnrampAnalyticsEvent(
event = "Provider Chosen",
params = mapOf(
"Provider" to providerName,
"Token" to tokenSymbol,
PROVIDER to providerName,
TOKEN_PARAM to tokenSymbol,
),
)
@ -105,9 +113,9 @@ sealed class OnrampAnalyticsEvent(
) : OnrampAnalyticsEvent(
event = "Button - Buy",
params = mapOf(
"Provider" to providerName,
"Currency" to currency,
"Token" to tokenSymbol,
PROVIDER to providerName,
CURRENCY to currency,
TOKEN_PARAM to tokenSymbol,
),
)
@ -120,11 +128,11 @@ sealed class OnrampAnalyticsEvent(
) : OnrampAnalyticsEvent(
event = "Buying In Progress Screen Opened",
params = mapOf(
"Token" to tokenSymbol,
"Provider" to providerName,
"Residence" to residence,
"Currency" to currency,
"Method" to paymentMethod,
TOKEN_PARAM to tokenSymbol,
PROVIDER to providerName,
RESIDENCE to residence,
CURRENCY to currency,
PAYMENT_METHOD to paymentMethod,
),
)
@ -133,14 +141,25 @@ sealed class OnrampAnalyticsEvent(
data class Errors(
private val tokenSymbol: String,
private val providerName: String,
private val errorCode: String,
private val providerName: String?,
) : OnrampAnalyticsEvent(
event = "Errors",
params = buildMap {
put(TOKEN_PARAM, tokenSymbol)
providerName?.let { put(PROVIDER, providerName) }
put(ERROR_CODE, errorCode)
},
)
data class AppErrors(
private val tokenSymbol: String,
private val errorDescription: String,
) : OnrampAnalyticsEvent(
event = "App Errors",
params = mapOf(
"Token" to tokenSymbol,
"Provider" to providerName,
"Error Code" to errorCode,
TOKEN_PARAM to tokenSymbol,
ERROR_DESCRIPTION to errorDescription,
),
)
}

View file

@ -4,13 +4,12 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.PLACE
import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER
import com.tangem.core.analytics.models.AnalyticsParam.Key.STATUS
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_CATEGORY
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
class TokenExchangeAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(TOKEN_CATEGORY, event, params, null) {
) : AnalyticsEvent("Token", event, params, null) {
class CexTxStatusOpened(token: String) : TokenScreenAnalyticsEvent(
event = "Swap Status Opened",

View file

@ -3,10 +3,15 @@ package com.tangem.features.onramp.confirmresidency
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.tokens.model.CryptoCurrency
internal interface ConfirmResidencyComponent : ComposableBottomSheetComponent {
data class Params(val country: OnrampCountry, val onDismiss: (OnrampCountry) -> Unit)
data class Params(
val cryptoCurrency: CryptoCurrency,
val country: OnrampCountry,
val onDismiss: (OnrampCountry) -> Unit,
)
interface Factory : ComponentFactory<Params, ConfirmResidencyComponent>
}

View file

@ -14,10 +14,10 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyBottomSheetConfig
import com.tangem.features.onramp.confirmresidency.model.ConfirmResidencyModel
import com.tangem.features.onramp.confirmresidency.ui.ConfirmResidencyBottomSheet
import com.tangem.features.onramp.confirmresidency.ui.ConfirmResidencyBottomSheetContent
import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyBottomSheetConfig
import com.tangem.features.onramp.selectcountry.SelectCountryComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -68,7 +68,10 @@ internal class DefaultConfirmResidencyComponent @AssistedInject constructor(
): ComposableBottomSheetComponent = when (config) {
ConfirmResidencyBottomSheetConfig.SelectCountry -> selectCountryComponentFactory.create(
context = childByContext(componentContext),
params = SelectCountryComponent.Params(onDismiss = model.bottomSheetNavigation::dismiss),
params = SelectCountryComponent.Params(
cryptoCurrency = params.cryptoCurrency,
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
}

View file

@ -15,6 +15,7 @@ import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent
import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyBottomSheetConfig
import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyUM
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
@ -51,6 +52,9 @@ internal class ConfirmResidencyModel @Inject constructor(
analyticsEventHandler.send(OnrampAnalyticsEvent.ResidenceConfirmScreenOpened(params.country.name))
getOnrampCountryUseCase.invoke()
.onEach { maybeCountry ->
maybeCountry.onLeft {
analyticsEventHandler.sendOnrampErrorEvent(it, params.cryptoCurrency.symbol)
}
val country = maybeCountry.getOrNull()
if (country != null) {
params.onDismiss(country)

View file

@ -54,6 +54,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
is OnrampMainBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create(
context = childByContext(componentContext),
params = ConfirmResidencyComponent.Params(
cryptoCurrency = params.cryptoCurrency,
country = config.country,
onDismiss = {
model.bottomSheetNavigation.dismiss()
@ -63,7 +64,10 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
)
is OnrampMainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create(
context = childByContext(componentContext),
params = SelectCurrencyComponent.Params(model.bottomSheetNavigation::dismiss),
params = SelectCurrencyComponent.Params(
cryptoCurrency = params.cryptoCurrency,
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
is OnrampMainBottomSheetConfig.ProvidersList -> selectProviderComponentFactory.create(
context = childByContext(componentContext),

View file

@ -1,11 +1,13 @@
package com.tangem.features.onramp.main.entity.factory.amount
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.model.OnrampQuote
@ -18,6 +20,7 @@ import com.tangem.utils.Provider
internal class OnrampAmountStateFactory(
private val currentStateProvider: Provider<OnrampMainComponentUM>,
private val analyticsEventHandler: AnalyticsEventHandler,
private val onrampIntents: OnrampIntents,
) {
@ -171,8 +174,14 @@ internal class OnrampAmountStateFactory(
}
val errorTextRes = when (error) {
is OnrampError.AmountError.TooBigError -> R.string.onramp_max_amount_restriction
is OnrampError.AmountError.TooSmallError -> R.string.onramp_min_amount_restriction
is OnrampError.AmountError.TooBigError -> {
analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError)
R.string.onramp_max_amount_restriction
}
is OnrampError.AmountError.TooSmallError -> {
analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError)
R.string.onramp_min_amount_restriction
}
}
return OnrampAmountSecondaryFieldUM.Error(

View file

@ -21,6 +21,7 @@ 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.onramp.model.error.OnrampError
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.OnrampMainComponent
@ -32,6 +33,7 @@ import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory
import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory
import com.tangem.features.onramp.providers.entity.SelectProviderResult
import com.tangem.features.onramp.utils.InputManager
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.PeriodicTask
@ -69,6 +71,7 @@ internal class OnrampMainComponentModel @Inject constructor(
)
private val amountStateFactory = OnrampAmountStateFactory(
currentStateProvider = Provider { _state.value },
analyticsEventHandler = analyticsEventHandler,
onrampIntents = this,
)
private val selectedUserWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
@ -111,9 +114,12 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun checkResidenceCountry() {
modelScope.launch {
checkOnrampAvailabilityUseCase.invoke()
checkOnrampAvailabilityUseCase()
.onRight(::handleOnrampAvailability)
.onLeft { Timber.e(it.toString()) }
.onLeft { error ->
Timber.e(error.toString())
sendOnrampErrorAnalytic(error)
}
}
}
@ -129,9 +135,14 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun subscribeToCurrencyUpdates() {
getOnrampCurrencyUseCase.invoke()
.onEach { maybeCurrency ->
val currency = maybeCurrency.getOrNull() ?: return@onEach
_state.update { amountStateFactory.getUpdatedCurrencyState(currency) }
updatePairsAndQuotes()
maybeCurrency.fold(
ifLeft = ::sendOnrampErrorAnalytic,
ifRight = { currency ->
if (currency == null) return@onEach
_state.update { amountStateFactory.getUpdatedCurrencyState(currency) }
updatePairsAndQuotes()
},
)
}
.launchIn(modelScope)
}
@ -146,7 +157,7 @@ internal class OnrampMainComponentModel @Inject constructor(
}
private suspend fun updatePairsAndQuotes() {
fetchPairsUseCase.invoke(params.cryptoCurrency)
fetchPairsUseCase.invoke(params.cryptoCurrency).onLeft(::sendOnrampErrorAnalytic)
startLoadingQuotes()
}
@ -165,7 +176,7 @@ internal class OnrampMainComponentModel @Inject constructor(
fetchQuotesUseCase.invoke(
amount = content.amountBlockState.amountFieldModel.fiatAmount,
cryptoCurrency = params.cryptoCurrency,
)
).onLeft(::sendOnrampErrorAnalytic)
}
},
onSuccess = {},
@ -176,20 +187,27 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun subscribeToQuotesUpdate() {
getOnrampQuotesUseCase.invoke()
.onEach { maybeQuotes ->
val quotes = maybeQuotes.getOrNull() ?: return@onEach
val isNoQuotes = quotes.none { it is OnrampQuote.Data }
val quote = quotes.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, isNoQuotes) }
maybeQuotes.fold(
ifLeft = ::sendOnrampErrorAnalytic,
ifRight = { quotes ->
quotes.filterIsInstance<OnrampQuote.Error>().forEach { errorState ->
sendOnrampErrorAnalytic(errorState.error)
}
val isNoQuotes = quotes.none { it is OnrampQuote.Data }
val quote = quotes.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, isNoQuotes) }
},
)
}
.launchIn(modelScope)
}
@ -270,6 +288,16 @@ internal class OnrampMainComponentModel @Inject constructor(
)
}
private fun sendOnrampErrorAnalytic(error: OnrampError) {
val content = state.value as? OnrampMainComponentUM.Content
val providerContent = content?.providerBlockState as? OnrampProviderBlockUM.Content
analyticsEventHandler.sendOnrampErrorEvent(
error = error,
tokenSymbol = params.cryptoCurrency.symbol,
providerName = providerContent?.providerName,
)
}
private companion object {
const val UPDATE_DELAY = 10_000L
}

View file

@ -5,11 +5,12 @@ import com.tangem.core.ui.extensions.TextReference
internal sealed interface ProviderListItemUM {
val providerId: String
val name: String
data class Available(
override val providerId: String,
override val name: String,
val imageUrl: String,
val name: String,
val rate: String,
val isBestRate: Boolean,
val diffRate: TextReference,
@ -19,16 +20,16 @@ internal sealed interface ProviderListItemUM {
data class AvailableWithError(
override val providerId: String,
override val name: String,
val imageUrl: String,
val name: String,
val subtitle: TextReference,
val onClick: () -> Unit,
) : ProviderListItemUM
data class Unavailable(
override val providerId: String,
override val name: String,
val imageUrl: String,
val name: String,
val subtitle: TextReference,
) : ProviderListItemUM
}

View file

@ -25,6 +25,7 @@ import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM
import com.tangem.features.onramp.providers.SelectProviderComponent
import com.tangem.features.onramp.providers.entity.*
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
import com.tangem.utils.StringsSigns.MINUS
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
@ -60,7 +61,12 @@ internal class SelectProviderModel @Inject constructor(
private fun subscribeToPaymentMethodUpdates() {
getOnrampSelectedPaymentMethodUseCase.invoke()
.onEach { it.getOrNull()?.let(::getProviders) }
.onEach { maybePaymentMethod ->
maybePaymentMethod.fold(
ifLeft = ::sendOnrampErrorEvent,
ifRight = ::getProviders,
)
}
.launchIn(modelScope)
}
@ -76,7 +82,10 @@ internal class SelectProviderModel @Inject constructor(
)
}
}
.onLeft { Timber.e(it.toString()) }
.onLeft { error ->
sendOnrampErrorEvent(error)
Timber.e(error.toString())
}
}
}
@ -96,7 +105,13 @@ internal class SelectProviderModel @Inject constructor(
private fun openPaymentMethods() {
analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened)
modelScope.launch {
val methods = getSelectedPaymentMethodsUseCase.invoke().getOrNull().orEmpty()
val methods = getSelectedPaymentMethodsUseCase().fold(
ifLeft = { error ->
sendOnrampErrorEvent(error)
emptySet()
},
ifRight = { it },
)
bottomSheetNavigation.activate(
ProviderListBottomSheetConfig.PaymentMethods(
selectedMethodId = _state.value.paymentMethod.id,
@ -221,6 +236,17 @@ internal class SelectProviderModel @Inject constructor(
params.onDismiss()
}
private fun sendOnrampErrorEvent(error: OnrampError) {
val selectedProvider = state.value.providers.firstOrNull {
it.providerId == params.selectedProviderId
}
analyticsEventHandler.sendOnrampErrorEvent(
error = error,
tokenSymbol = params.cryptoCurrency.symbol,
providerName = selectedProvider?.name,
)
}
/**
* Sorting providers by rule:
*

View file

@ -1,6 +1,7 @@
package com.tangem.features.onramp.redirect.model
import androidx.compose.ui.res.stringResource
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.decompose.navigation.Router
@ -20,16 +21,19 @@ import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.redirect.OnrampRedirectComponent
import com.tangem.features.onramp.redirect.entity.OnrampRedirectTopBarUM
import com.tangem.features.onramp.redirect.entity.OnrampRedirectUM
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
internal class OnrampRedirectModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val urlOpener: UrlOpener,
private val getOnrampRedirectUrlUseCase: GetOnrampRedirectUrlUseCase,
private val messageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler,
router: Router,
paramsContainer: ParamsContainer,
) : Model() {
@ -79,7 +83,11 @@ internal class OnrampRedirectModel @Inject constructor(
private fun handleError(error: OnrampError) {
Timber.e(error.toString())
analyticsEventHandler.sendOnrampErrorEvent(
error = error,
tokenSymbol = params.cryptoCurrency.symbol,
providerName = params.onrampProviderWithQuote.provider.info.name,
)
val contentMessage = ContentMessage { onDismiss ->
BasicDialog(
message = stringResource(id = R.string.common_unknown_error),

View file

@ -60,7 +60,10 @@ internal class DefaultOnrampComponent @AssistedInject constructor(
when (config) {
OnrampChild.Settings -> settingsComponentFactory.create(
context = childByContext(componentContext),
params = OnrampSettingsComponent.Params(navigation::pop),
params = OnrampSettingsComponent.Params(
params.cryptoCurrency,
navigation::pop,
),
)
OnrampChild.Main -> onrampMainComponentFactory.create(
context = childByContext(componentContext),

View file

@ -2,10 +2,11 @@ package com.tangem.features.onramp.selectcountry
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.tokens.model.CryptoCurrency
internal interface SelectCountryComponent : ComposableBottomSheetComponent {
data class Params(val onDismiss: () -> Unit)
data class Params(val cryptoCurrency: CryptoCurrency, val onDismiss: () -> Unit)
interface Factory : ComponentFactory<Params, SelectCountryComponent>
}

View file

@ -21,6 +21,7 @@ import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountry
import com.tangem.features.onramp.utils.InputManager
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -61,10 +62,16 @@ internal class OnrampSelectCountryModel @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun subscribeOnUpdateState() {
combine(
flow = refreshTrigger.onStart { emit(Unit) }.flatMapLatest { flowOf(getOnrampCountriesUseCase.invoke()) },
flow2 = getOnrampCountryUseCase.invoke(),
flow = refreshTrigger.onStart { emit(Unit) }.flatMapLatest { flowOf(getOnrampCountriesUseCase()) },
flow2 = getOnrampCountryUseCase(),
flow3 = searchManager.query,
) { maybeCountries, maybeCountry, query ->
maybeCountries.onLeft {
analyticsEventHandler.sendOnrampErrorEvent(it, params.cryptoCurrency.symbol)
}
maybeCountry.onLeft {
analyticsEventHandler.sendOnrampErrorEvent(it, params.cryptoCurrency.symbol)
}
UpdateCountryItemsTransformer(
maybeCountries = maybeCountries,
defaultCountry = maybeCountry.getOrNull(),

View file

@ -2,10 +2,11 @@ package com.tangem.features.onramp.selectcurrency
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.tokens.model.CryptoCurrency
internal interface SelectCurrencyComponent : ComposableBottomSheetComponent {
data class Params(val onDismiss: () -> Unit)
data class Params(val cryptoCurrency: CryptoCurrency, val onDismiss: () -> Unit)
interface Factory : ComponentFactory<Params, SelectCurrencyComponent>
}

View file

@ -22,6 +22,7 @@ import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurren
import com.tangem.features.onramp.utils.InputManager
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -59,6 +60,9 @@ internal class OnrampSelectCurrencyModel @Inject constructor(
flow = refreshTrigger.onStart { emit(Unit) }.flatMapLatest { flowOf(getOnrampCurrenciesUseCase.invoke()) },
flow2 = searchManager.query,
) { maybeCurrencies, query ->
maybeCurrencies.onLeft {
analyticsEventHandler.sendOnrampErrorEvent(it, params.cryptoCurrency.symbol)
}
UpdateCurrencyItemsTransformer(
maybeCurrencies = maybeCurrencies,
query = query,

View file

@ -51,7 +51,10 @@ internal class DefaultOnrampSettingsComponent @AssistedInject constructor(
): ComposableBottomSheetComponent = when (config) {
OnrampSettingsConfig.SelectCountry -> selectCountryComponentFactory.create(
context = childByContext(componentContext),
params = SelectCountryComponent.Params(model.bottomSheetNavigation::dismiss),
params = SelectCountryComponent.Params(
params.cryptoCurrency,
model.bottomSheetNavigation::dismiss,
),
)
}

View file

@ -2,10 +2,11 @@ package com.tangem.features.onramp.settings
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.tokens.model.CryptoCurrency
internal interface OnrampSettingsComponent : ComposableContentComponent {
data class Params(val onBack: () -> Unit)
data class Params(val cryptoCurrency: CryptoCurrency, val onBack: () -> Unit)
interface Factory : ComponentFactory<Params, OnrampSettingsComponent>
}

View file

@ -15,6 +15,7 @@ import com.tangem.features.onramp.settings.OnrampSettingsComponent
import com.tangem.features.onramp.settings.entity.OnrampSettingsConfig
import com.tangem.features.onramp.settings.entity.OnrampSettingsItemUM
import com.tangem.features.onramp.settings.entity.OnrampSettingsUM
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
@ -46,19 +47,24 @@ internal class OnrampSettingsModel @Inject constructor(
}
private fun updateResidenceState(maybeCountry: Either<OnrampError, OnrampCountry?>) {
maybeCountry.onRight { country ->
_state.update { state ->
state.copy(
items = listOf(
OnrampSettingsItemUM.Residence(
countryName = country?.name.orEmpty(),
flagUrl = country?.image.orEmpty(),
onClick = ::openSelectCountryBottomSheet,
),
).toImmutableList(),
)
}
}
maybeCountry.fold(
ifLeft = {
analyticsEventHandler.sendOnrampErrorEvent(it, params.cryptoCurrency.symbol)
},
ifRight = { country ->
_state.update { state ->
state.copy(
items = listOf(
OnrampSettingsItemUM.Residence(
countryName = country?.name.orEmpty(),
flagUrl = country?.image.orEmpty(),
onClick = ::openSelectCountryBottomSheet,
),
).toImmutableList(),
)
}
},
)
}
private fun openSelectCountryBottomSheet() {

View file

@ -12,10 +12,12 @@ 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
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.onramp.component.OnrampSuccessComponent
import com.tangem.features.onramp.success.entity.OnrampSuccessClickIntents
import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM
import com.tangem.features.onramp.success.entity.conterter.SetOnrampSuccessContentConverter
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -74,8 +76,13 @@ internal class OnrampSuccessComponentModel @Inject constructor(
getOnrampStatusUseCase(externalTxId = params.externalTxId)
.fold(
ifLeft = {
Timber.e(it.toString())
ifLeft = { error ->
analyticsEventHandler.sendOnrampErrorEvent(
error = error,
tokenSymbol = cryptoCurrency.symbol,
providerName = transaction.providerName,
)
Timber.e(error.toString())
},
ifRight = { status ->
analyticsEventHandler.send(
@ -94,15 +101,25 @@ internal class OnrampSuccessComponentModel @Inject constructor(
goToProviderClick = ::goToProviderClick,
).convert(status)
}
removeTransactionIfTerminalStatus(status)
removeTransactionIfTerminalStatus(cryptoCurrency, transaction.providerName, status)
},
)
}
private fun removeTransactionIfTerminalStatus(status: OnrampStatus) {
private fun removeTransactionIfTerminalStatus(
cryptoCurrency: CryptoCurrency,
providerName: String,
status: OnrampStatus,
) {
modelScope.launch {
if (status.status.isTerminal) {
onrampRemoveTransactionUseCase(status.txId)
onrampRemoveTransactionUseCase(status.txId).onLeft { error ->
analyticsEventHandler.sendOnrampErrorEvent(
error = error,
tokenSymbol = cryptoCurrency.symbol,
providerName = providerName,
)
}
}
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.features.onramp.utils
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.error.OnrampError
internal fun AnalyticsEventHandler.sendOnrampErrorEvent(
error: OnrampError,
tokenSymbol: String,
providerName: String? = null,
) = when (error) {
is OnrampError.DataError -> send(
OnrampAnalyticsEvent.Errors(
tokenSymbol = tokenSymbol,
providerName = providerName,
errorCode = error.code,
),
)
is OnrampError.DomainError -> {
send(
OnrampAnalyticsEvent.AppErrors(
tokenSymbol = tokenSymbol,
errorDescription = error.description.orEmpty(),
),
)
}
is OnrampError.AmountError.TooBigError,
is OnrampError.AmountError.TooSmallError,
OnrampError.RedirectError.VerificationFailed,
OnrampError.RedirectError.WrongRequestId,
-> { /* no-op */ }
}