diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapUtils.kt index 01da434698..a7fc9a0236 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapUtils.kt @@ -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) + } + } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt new file mode 100644 index 0000000000..b52a113dee --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt @@ -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> = model.uiState + + fun LazyListScope.content( + state: ImmutableList, + 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?, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsUpdateTrigger.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsUpdateTrigger.kt new file mode 100644 index 0000000000..e595eb19a1 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsUpdateTrigger.kt @@ -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 + + /** Flow returns whether there is swap error notifications */ + val hasErrorFlow: Flow +} + +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 + field = MutableSharedFlow() + + override val hasErrorFlow: SharedFlow + field = MutableSharedFlow() + + override suspend fun callbackHasError(hasError: Boolean) { + hasErrorFlow.emit(hasError) + } + + override suspend fun triggerUpdate(data: SwapNotificationData) { + updateTriggerFlow.emit(data) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/di/SwapNotificationsModule.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/di/SwapNotificationsModule.kt new file mode 100644 index 0000000000..e537f4b691 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/di/SwapNotificationsModule.kt @@ -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 +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt new file mode 100644 index 0000000000..eadc69c82c --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt @@ -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, + ), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt new file mode 100644 index 0000000000..02c24f08fe --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt @@ -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> + field = MutableStateFlow>(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.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) + } +} \ No newline at end of file