Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-18 17:00:20 +05:00
commit 3bd0f8d01a
6 changed files with 358 additions and 0 deletions

View file

@ -1,6 +1,55 @@
package com.tangem.features.swap.v2.impl.common
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.simple
import com.tangem.domain.express.models.ExpressError
import com.tangem.features.swap.v2.impl.R
internal object SwapUtils {
const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12%
const val INCREASE_GAS_LIMIT_FOR_CEX = 105 // 5%
fun getExpressErrorMessage(expressError: ExpressError): TextReference {
return when (expressError) {
is ExpressError.InternalError -> resourceReference(
id = R.string.express_error_swap_unavailable,
formatArgs = wrappedList(expressError.code),
)
is ExpressError.ExchangeNotPossibleError -> resourceReference(
id = R.string.warning_express_pair_unavailable_message,
formatArgs = wrappedList(expressError.code),
)
is ExpressError.UnknownError -> resourceReference(R.string.common_unknown_error)
is ExpressError.ProviderNotActiveError,
is ExpressError.ProviderNotFoundError,
is ExpressError.ProviderNotAvailableError,
is ExpressError.ProviderInternalError,
-> resourceReference(
id = R.string.express_error_swap_pair_unavailable,
formatArgs = wrappedList(expressError.code),
)
is ExpressError.ProviderDifferentAmountError -> resourceReference(
R.string.express_error_provider_amount_roundup,
formatArgs = wrappedList(
expressError.code,
expressError.fromProviderAmount.format { simple(decimals = expressError.decimals) },
),
)
else -> resourceReference(R.string.express_error_code, wrappedList(expressError.code.toString()))
}
}
internal fun getExpressErrorTitle(expressError: ExpressError): TextReference {
return when (expressError) {
is ExpressError.ExchangeNotPossibleError -> resourceReference(
id = R.string.warning_express_pair_unavailable_title,
formatArgs = wrappedList(expressError.code),
)
is ExpressError.UnknownError -> resourceReference(R.string.common_error)
else -> resourceReference(R.string.warning_express_refresh_required_title)
}
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.features.swap.v2.impl.notifications
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.notifications.notifications
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.swap.v2.impl.notifications.model.SwapNotificationsModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.StateFlow
internal class SwapNotificationsComponent(
appComponentContext: AppComponentContext,
params: Params,
) : AppComponentContext by appComponentContext {
private val model: SwapNotificationsModel = getOrCreateModel(params)
val state: StateFlow<ImmutableList<NotificationUM>> = model.uiState
fun LazyListScope.content(
state: ImmutableList<NotificationUM>,
modifier: Modifier = Modifier,
hasPaddingAbove: Boolean = false,
isClickDisabled: Boolean = false,
) {
notifications(
notifications = state,
modifier = modifier,
hasPaddingAbove = hasPaddingAbove,
isClickDisabled = isClickDisabled,
)
}
data class Params(
val swapNotificationData: SwapNotificationData,
) {
data class SwapNotificationData(
val expressError: ExpressError?,
val fromCryptoCurrency: CryptoCurrency?,
)
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.features.swap.v2.impl.notifications
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent.Params.SwapNotificationData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import javax.inject.Inject
import javax.inject.Singleton
internal interface SwapNotificationsUpdateListener {
/** Flow triggers swap notifications update */
val updateTriggerFlow: Flow<SwapNotificationData>
/** Flow returns whether there is swap error notifications */
val hasErrorFlow: Flow<Boolean>
}
internal interface SwapNotificationsUpdateTrigger {
/** Trigger return callback with check result */
suspend fun callbackHasError(hasError: Boolean)
/** Trigger check swap notifications */
suspend fun triggerUpdate(data: SwapNotificationData)
}
@Singleton
internal class DefaultSwapNotificationsUpdateTrigger @Inject constructor() :
SwapNotificationsUpdateListener,
SwapNotificationsUpdateTrigger {
override val updateTriggerFlow: SharedFlow<SwapNotificationData>
field = MutableSharedFlow<SwapNotificationData>()
override val hasErrorFlow: SharedFlow<Boolean>
field = MutableSharedFlow<Boolean>()
override suspend fun callbackHasError(hasError: Boolean) {
hasErrorFlow.emit(hasError)
}
override suspend fun triggerUpdate(data: SwapNotificationData) {
updateTriggerFlow.emit(data)
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.features.swap.v2.impl.notifications.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.swap.v2.impl.notifications.DefaultSwapNotificationsUpdateTrigger
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateTrigger
import com.tangem.features.swap.v2.impl.notifications.model.SwapNotificationsModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(ModelComponent::class)
internal interface SwapNotificationsModule {
@Binds
@IntoMap
@ClassKey(SwapNotificationsModel::class)
fun provideSwapNotificationsModel(impl: SwapNotificationsModel): Model
}
@Module
@InstallIn(SingletonComponent::class)
internal interface SwapNotificationsSingletonModule {
@Singleton
@Binds
fun bindsSwapNotificationsUpdateTrigger(impl: DefaultSwapNotificationsUpdateTrigger): SwapNotificationsUpdateTrigger
@Singleton
@Binds
fun bindsSwapNotificationsUpdateListener(
impl: DefaultSwapNotificationsUpdateTrigger,
): SwapNotificationsUpdateListener
}

View file

@ -0,0 +1,81 @@
package com.tangem.features.swap.v2.impl.notifications.entity
import com.tangem.common.ui.R
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.express.models.ExpressError
import com.tangem.features.swap.v2.impl.common.SwapUtils.getExpressErrorMessage
import com.tangem.features.swap.v2.impl.common.SwapUtils.getExpressErrorTitle
internal object SwapNotificationUM {
sealed class Error(
title: TextReference,
subtitle: TextReference,
iconResId: Int = R.drawable.ic_alert_24,
buttonState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : NotificationUM.Error(
title = title,
subtitle = subtitle,
iconResId = iconResId,
buttonState = buttonState,
onCloseClick = onCloseClick,
) {
data class MinimalAmountError(
val amount: String,
) : Error(
title = resourceReference(
id = R.string.warning_express_too_minimal_amount_title,
formatArgs = wrappedList(amount),
),
subtitle = resourceReference(R.string.warning_express_wrong_amount_description),
)
data class MaximumAmountError(
val amount: String,
) : Error(
title = resourceReference(
id = R.string.warning_express_too_maximum_amount_title,
formatArgs = wrappedList(amount),
),
subtitle = resourceReference(R.string.warning_express_wrong_amount_description),
iconResId = R.drawable.ic_alert_circle_24,
)
data object FCAWarningList : Error(
title = resourceReference(R.string.warning_express_providers_fca_warning_title),
subtitle = resourceReference(R.string.warning_express_providers_fca_warning_description),
iconResId = R.drawable.ic_alert_circle_24,
)
}
sealed class Warning(
title: TextReference,
subtitle: TextReference,
iconResId: Int = R.drawable.img_attention_20,
buttonsState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : NotificationUM.Warning(
title = title,
subtitle = subtitle,
iconResId = iconResId,
buttonsState = buttonsState,
onCloseClick = onCloseClick,
) {
data class ExpressGeneralError(
val expressError: ExpressError,
val onConfirmClick: () -> Unit,
) : Warning(
title = getExpressErrorTitle(expressError),
subtitle = getExpressErrorMessage(expressError),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_button_refresh),
onClick = onConfirmClick,
),
)
}
}

View file

@ -0,0 +1,99 @@
package com.tangem.features.swap.v2.impl.notifications.model
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.express.models.ExpressError
import com.tangem.features.swap.v2.impl.notifications.DefaultSwapNotificationsUpdateTrigger
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent.Params.SwapNotificationData
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener
import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
internal class SwapNotificationsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val swapNotificationsUpdateListener: SwapNotificationsUpdateListener,
private val swapNotificationsUpdateTrigger: DefaultSwapNotificationsUpdateTrigger,
paramsContainer: ParamsContainer,
) : Model() {
private val params: SwapNotificationsComponent.Params = paramsContainer.require()
private var notificationData = params.swapNotificationData
val uiState: StateFlow<ImmutableList<NotificationUM>>
field = MutableStateFlow<ImmutableList<NotificationUM>>(persistentListOf())
init {
subscribeToNotificationUpdateTrigger()
modelScope.launch {
buildNotifications()
}
}
private fun subscribeToNotificationUpdateTrigger() {
swapNotificationsUpdateListener.updateTriggerFlow
.onEach { updateState(it) }
.launchIn(modelScope)
}
private suspend fun updateState(data: SwapNotificationData) {
notificationData = data
buildNotifications()
}
private suspend fun buildNotifications() {
val notifications = buildList {
addExpressErrorNotification()
}
swapNotificationsUpdateTrigger.callbackHasError(notifications.isNotEmpty())
uiState.value = notifications.toImmutableList()
}
fun MutableList<NotificationUM>.addExpressErrorNotification() {
val expressError = notificationData.expressError ?: return
val fromCryptoCurrency = notificationData.fromCryptoCurrency ?: return
val errorNotification = when (expressError) {
is ExpressError.AmountError.TooSmallError -> SwapNotificationUM.Error.MinimalAmountError(
expressError.amount.format {
crypto(
symbol = fromCryptoCurrency.symbol,
decimals = fromCryptoCurrency.decimals,
)
},
)
is ExpressError.AmountError.TooBigError -> SwapNotificationUM.Error.MaximumAmountError(
expressError.amount.format {
crypto(
symbol = fromCryptoCurrency.symbol,
decimals = fromCryptoCurrency.decimals,
)
},
)
else -> SwapNotificationUM.Warning.ExpressGeneralError(
expressError = expressError,
onConfirmClick = {
// todo reload quotes
},
)
}
add(errorNotification)
}
}