From 7f393a301c9f14e06146b7beac427dee8926634b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Jun 2025 15:27:29 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../message/MessageBottomSheetV2.kt | 2 +- .../managetokens/CreateCoinCurrencyUseCase.kt | 0 features/manage-tokens/impl/build.gradle.kts | 1 + .../DefaultChooseManagedTokensComponent.kt | 34 +- .../model/ChooseManagedTokensModel.kt | 7 +- .../SwapChooseTokenNetworkComponent.kt | 20 ++ .../SwapChooseTokenNetworkTrigger.kt | 11 + .../choosetoken/di/SwapChooseTokenModule.kt | 46 +++ .../DefaultSwapChooseTokenNetworkComponent.kt | 42 +++ .../DefaultSwapChooseTokenNetworkTrigger.kt | 28 ++ .../entity/SwapChooseTokenNetworkUM.kt | 40 +++ .../model/SwapChooseTokenFactory.kt | 26 ++ .../model/SwapChooseTokenNetworkModel.kt | 118 +++++++ .../SwapChooseContentStateTransformer.kt | 54 ++++ .../SwapChooseErrorStateTransformer.kt | 24 ++ .../ui/SwapChooseTokenNetworkContent.kt | 301 ++++++++++++++++++ .../feature/swap/ui/SwapSelectTokenScreen.kt | 2 +- 17 files changed, 741 insertions(+), 15 deletions(-) delete mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCoinCurrencyUseCase.kt create mode 100644 features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt create mode 100644 features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkTrigger.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/di/SwapChooseTokenModule.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkComponent.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkTrigger.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/entity/SwapChooseTokenNetworkUM.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenFactory.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseContentStateTransformer.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseErrorStateTransformer.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt index 407a375ade..57f8d0f0d5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt @@ -62,7 +62,7 @@ fun MessageBottomSheetV2(state: MessageBottomSheetUMV2, onDismissRequest: () -> } @Composable -private fun MessageBottomSheetV2Content(state: MessageBottomSheetUMV2, modifier: Modifier = Modifier) { +fun MessageBottomSheetV2Content(state: MessageBottomSheetUMV2, modifier: Modifier = Modifier) { Column(modifier = modifier) { state.elements.fastForEach { when (it) { diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCoinCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCoinCurrencyUseCase.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 87ecb9bb7b..84a0eb5e9a 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { /* Project - API */ implementation(projects.features.manageTokens.api) + implementation(projects.features.swapV2.api) /* Project - Core */ implementation(projects.core.decompose) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt index c42147407a..ecd9dd8f6d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt @@ -7,20 +7,27 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBottomSheetConfig import com.tangem.features.managetokens.choosetoken.model.ChooseManagedTokensModel import com.tangem.features.managetokens.choosetoken.ui.ChooseManagedTokenContent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkTrigger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch internal class DefaultChooseManagedTokensComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: ChooseManagedTokensComponent.Params, + private val swapChooseTokenNetworkFactory: SwapChooseTokenNetworkComponent.Factory, + private val swapChooseTokenNetworkTrigger: SwapChooseTokenNetworkTrigger, ) : ChooseManagedTokensComponent, AppComponentContext by context { private val model: ChooseManagedTokensModel = getOrCreateModel(params) @@ -48,18 +55,21 @@ internal class DefaultChooseManagedTokensComponent @AssistedInject constructor( config: ChooseManageTokensBottomSheetConfig, componentContext: ComponentContext, ): ComposableBottomSheetComponent = when (config) { - else -> getStubComponent() - } - - private fun getStubComponent() = StubComponent() - - class StubComponent : ComposableBottomSheetComponent { - override fun dismiss() {} - - @Composable - override fun BottomSheet() { - /* no-op */ - } + is ChooseManageTokensBottomSheetConfig.SwapTokensBottomSheetConfig -> swapChooseTokenNetworkFactory.create( + context = childByContext(componentContext), + params = SwapChooseTokenNetworkComponent.Params( + userWalletId = config.userWalletId, + initialCurrency = config.initialCurrency, + token = config.token, + onDismiss = model.bottomSheetNavigation::dismiss, + onResult = { + componentScope.launch { + swapChooseTokenNetworkTrigger.trigger(it) + } + router.pop() + }, + ), + ) } @AssistedFactory diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt index 65284e752d..ad0f6fe8f2 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt @@ -30,6 +30,7 @@ import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.PaginationStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -142,7 +143,11 @@ internal class ChooseManagedTokensModel @Inject constructor( private fun updateItems(items: ImmutableList) { uiState.update { state -> state.copy( - readContent = state.readContent.copy(items = items), + readContent = state.readContent.copy( + items = items.filterNot { + it.id.value == params.initialCurrency.id.rawCurrencyId?.value + }.toPersistentList(), + ), ) } } diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt new file mode 100644 index 0000000000..b283dd585c --- /dev/null +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt @@ -0,0 +1,20 @@ +package com.tangem.features.swap.v2.api.choosetoken + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +interface SwapChooseTokenNetworkComponent : ComposableBottomSheetComponent { + + data class Params( + val userWalletId: UserWalletId, + val initialCurrency: CryptoCurrency, + val token: ManagedCryptoCurrency.Token, + val onDismiss: () -> Unit, + val onResult: (CryptoCurrency) -> Unit, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkTrigger.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkTrigger.kt new file mode 100644 index 0000000000..5940a10981 --- /dev/null +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkTrigger.kt @@ -0,0 +1,11 @@ +package com.tangem.features.swap.v2.api.choosetoken + +import com.tangem.domain.models.currency.CryptoCurrency + +/** + * Triggers swap token selection + */ +interface SwapChooseTokenNetworkTrigger { + + suspend fun trigger(cryptoCurrency: CryptoCurrency) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/di/SwapChooseTokenModule.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/di/SwapChooseTokenModule.kt new file mode 100644 index 0000000000..187db962f7 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/di/SwapChooseTokenModule.kt @@ -0,0 +1,46 @@ +package com.tangem.features.swap.v2.impl.choosetoken.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkTrigger +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.DefaultSwapChooseTokenNetworkComponent +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.DefaultSwapChooseTokenNetworkTrigger +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.SwapChooseTokenNetworkListener +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenNetworkModel +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 + +@InstallIn(ModelComponent::class) +@Module +internal interface SwapChooseTokenModule { + + @Binds + @IntoMap + @ClassKey(SwapChooseTokenNetworkModel::class) + fun provideSwapChooseTokenNetworkModel(impl: SwapChooseTokenNetworkModel): Model +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface SwapChooseTokenModuleBinds { + + @Binds + @Singleton + fun provideSwapChooseTokenNetworksBottomSheetComponent( + impl: DefaultSwapChooseTokenNetworkComponent.Factory, + ): SwapChooseTokenNetworkComponent.Factory + + @Binds + @Singleton + fun bindSwapChooseTokenNetworkTrigger(impl: DefaultSwapChooseTokenNetworkTrigger): SwapChooseTokenNetworkTrigger + + @Binds + @Singleton + fun bindSwapChooseTokenNetworkListener(impl: DefaultSwapChooseTokenNetworkTrigger): SwapChooseTokenNetworkListener +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkComponent.kt new file mode 100644 index 0000000000..b073b75725 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkComponent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenNetworkModel +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.ui.SwapChooseTokenNetworkBottomSheet +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultSwapChooseTokenNetworkComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: SwapChooseTokenNetworkComponent.Params, +) : SwapChooseTokenNetworkComponent, AppComponentContext by context { + + private val model: SwapChooseTokenNetworkModel = getOrCreateModel(params) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state by model.uiState.collectAsStateWithLifecycle() + + SwapChooseTokenNetworkBottomSheet( + config = state.bottomSheetConfig, + ) + } + + @AssistedFactory + interface Factory : SwapChooseTokenNetworkComponent.Factory { + override fun create( + context: AppComponentContext, + params: SwapChooseTokenNetworkComponent.Params, + ): DefaultSwapChooseTokenNetworkComponent + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkTrigger.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkTrigger.kt new file mode 100644 index 0000000000..887637781f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkTrigger.kt @@ -0,0 +1,28 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkTrigger +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Listens to swap token selection + */ +interface SwapChooseTokenNetworkListener { + val swapChooseTokenNetworkResultFlow: SharedFlow +} + +@Singleton +internal class DefaultSwapChooseTokenNetworkTrigger @Inject constructor() : + SwapChooseTokenNetworkTrigger, + SwapChooseTokenNetworkListener { + + override val swapChooseTokenNetworkResultFlow: SharedFlow + field = MutableSharedFlow() + + override suspend fun trigger(result: CryptoCurrency) { + swapChooseTokenNetworkResultFlow.emit(result) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/entity/SwapChooseTokenNetworkUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/entity/SwapChooseTokenNetworkUM.kt new file mode 100644 index 0000000000..202ba8410f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/entity/SwapChooseTokenNetworkUM.kt @@ -0,0 +1,40 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +internal data class SwapChooseTokenNetworkUM( + val bottomSheetConfig: TangemBottomSheetConfig, +) + +@Immutable +internal sealed class SwapChooseTokenNetworkContentUM : TangemBottomSheetConfigContent { + + abstract val messageContent: MessageBottomSheetUMV2 + + data class Loading( + override val messageContent: MessageBottomSheetUMV2, + ) : SwapChooseTokenNetworkContentUM() + + data class Error( + override val messageContent: MessageBottomSheetUMV2, + ) : SwapChooseTokenNetworkContentUM() + + data class Content( + override val messageContent: MessageBottomSheetUMV2, + val swapNetworks: ImmutableList, + ) : SwapChooseTokenNetworkContentUM() +} + +internal data class SwapChooseNetworkUM( + val title: TextReference, + val subtitle: TextReference, + @DrawableRes val iconResId: Int, + val hasFixedRate: Boolean, + val onNetworkClick: () -> Unit, +) \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenFactory.kt new file mode 100644 index 0000000000..b7e4427eb3 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenFactory.kt @@ -0,0 +1,26 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model + +import com.tangem.core.ui.components.bottomsheets.message.* +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.swap.v2.impl.R + +internal object SwapChooseTokenFactory { + + fun getErrorMessage(tokenName: String, onDismiss: () -> Unit): MessageBottomSheetUMV2 { + return messageBottomSheetUM { + infoBlock { + icon(R.drawable.img_attention_20) { + type = MessageBottomSheetUMV2.Icon.Type.Attention + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.express_swap_not_supported_title, wrappedList(tokenName)) + body = resourceReference(R.string.express_swap_not_supported_text) + } + secondaryButton { + text = resourceReference(R.string.warning_button_ok) + onClick { onDismiss() } + } + } + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt new file mode 100644 index 0000000000..40bd268b58 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt @@ -0,0 +1,118 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model + +import arrow.core.getOrElse +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.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase +import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenFactory.getErrorMessage +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.transformers.SwapChooseContentStateTransformer +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.transformers.SwapChooseErrorStateTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.transformer.update +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@ModelScoped +internal class SwapChooseTokenNetworkModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, + private val getSwapSupportedPairsUseCase: GetSwapSupportedPairsUseCase, + private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val uiMessageSender: UiMessageSender, +) : Model() { + + private val params: SwapChooseTokenNetworkComponent.Params = paramsContainer.require() + + val uiState: StateFlow + field: MutableStateFlow = MutableStateFlow( + SwapChooseTokenNetworkUM( + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = params.onDismiss, + content = SwapChooseTokenNetworkContentUM.Loading( + messageContent = getErrorMessage( + tokenName = params.token.name, + onDismiss = params.onDismiss, + ), + ), + ), + ), + ) + + init { + initContent() + } + + private fun initContent() { + val userWallet = getUserWalletUseCase(params.userWalletId).getOrElse { + Timber.e("Failed to get user wallet: $it") + getGenericErrorState() + return + } + + modelScope.launch { + val cryptoCurrencyList = createCryptoCurrencyUseCase( + token = params.token, + userWalletId = params.userWalletId, + ).getOrElse { + Timber.e("Failed to get crypto currency") + getGenericErrorState() + return@launch + } + val pairs = getSwapSupportedPairsUseCase.invoke( + userWallet = userWallet, + initialCurrency = params.initialCurrency, + cryptoCurrencyList = cryptoCurrencyList + params.initialCurrency, + ).getOrElse { + Timber.e(it.toString()) + uiState.update( + SwapChooseErrorStateTransformer( + tokenName = params.token.name, + onDismiss = params.onDismiss, + ), + ) + return@launch + } + delay(MINIMUM_LOADING_TIME) + uiState.update( + SwapChooseContentStateTransformer( + pairs = pairs, + onNetworkClick = params.onResult, + tokenName = params.token.name, + onDismiss = params.onDismiss, + ), + ) + } + } + + private fun getGenericErrorState() { + uiMessageSender.send( + DialogMessage( + title = resourceReference(id = R.string.common_error), + message = resourceReference(id = R.string.common_unknown_error), + onDismissRequest = params.onDismiss, + firstActionBuilder = { okAction() }, + ), + ) + } + + private companion object { + const val MINIMUM_LOADING_TIME = 100L + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseContentStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseContentStateTransformer.kt new file mode 100644 index 0000000000..f876199b86 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseContentStateTransformer.kt @@ -0,0 +1,54 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.transformers + +import com.tangem.core.ui.extensions.iconResId +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseNetworkUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenFactory.getErrorMessage +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class SwapChooseContentStateTransformer( + private val pairs: SwapCurrencies, + private val tokenName: String, + private val onNetworkClick: (CryptoCurrency) -> Unit, + private val onDismiss: () -> Unit, +) : Transformer { + override fun transform(prevState: SwapChooseTokenNetworkUM): SwapChooseTokenNetworkUM { + val swapNetworks = pairs.fromGroup.available.map { + val network = it.currencyStatus.currency.network + SwapChooseNetworkUM( + title = stringReference(network.name), + subtitle = stringReference(network.standardType.name), + iconResId = network.iconResId, + hasFixedRate = it.providers.any { it.rateTypes.any { it == ExpressRateType.Fixed } }, + onNetworkClick = { onNetworkClick(it.currencyStatus.currency) }, + ) + }.toPersistentList() + + return prevState.copy( + bottomSheetConfig = prevState.bottomSheetConfig.copy( + content = if (swapNetworks.isNotEmpty()) { + SwapChooseTokenNetworkContentUM.Content( + swapNetworks = swapNetworks, + messageContent = getErrorMessage( + tokenName = tokenName, + onDismiss = onDismiss, + ), + ) + } else { + SwapChooseTokenNetworkContentUM.Error( + messageContent = getErrorMessage( + tokenName = tokenName, + onDismiss = onDismiss, + ), + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseErrorStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseErrorStateTransformer.kt new file mode 100644 index 0000000000..41ea5a2ea3 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseErrorStateTransformer.kt @@ -0,0 +1,24 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.transformers + +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenFactory.getErrorMessage +import com.tangem.utils.transformer.Transformer + +internal class SwapChooseErrorStateTransformer( + private val tokenName: String, + private val onDismiss: () -> Unit, +) : Transformer { + override fun transform(prevState: SwapChooseTokenNetworkUM): SwapChooseTokenNetworkUM { + return prevState.copy( + bottomSheetConfig = prevState.bottomSheetConfig.copy( + content = SwapChooseTokenNetworkContentUM.Error( + messageContent = getErrorMessage( + tokenName = tokenName, + onDismiss = onDismiss, + ), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt new file mode 100644 index 0000000000..aa9e2fc590 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt @@ -0,0 +1,301 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.message.* +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseNetworkUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +@Composable +internal fun SwapChooseTokenNetworkBottomSheet(config: TangemBottomSheetConfig) { + TangemModalBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + title = { + AnimatedContent( + targetState = config.content is SwapChooseTokenNetworkContentUM.Content, + ) { isContent -> + TangemModalBottomSheetTitle( + title = resourceReference(R.string.common_choose_network).takeIf { isContent }, + endIconRes = R.drawable.ic_close_24, + onEndClick = config.onDismissRequest, + ) + } + }, + content = { contentConfig -> + SwapChooseTokenNetworkContent(contentConfig) + }, + ) +} + +@Composable +internal fun SwapChooseTokenNetworkContent(state: SwapChooseTokenNetworkContentUM) { + AnimatedContent( + targetState = state, + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + ) { targetState -> + when (targetState) { + is SwapChooseTokenNetworkContentUM.Content -> SwapChooseTokenNetworkContentList(targetState.swapNetworks) + else -> { + Box { + MessageBottomSheetV2Content( + state = targetState.messageContent, + modifier = Modifier + .conditional(targetState is SwapChooseTokenNetworkContentUM.Loading) { + alpha(0f) + }, + ) + if (targetState is SwapChooseTokenNetworkContentUM.Loading) { + CircularProgressIndicator( + color = TangemTheme.colors.icon.inactive, + modifier = Modifier + .align(Alignment.Center), + ) + } + } + } + } + } +} + +@Composable +private fun SwapChooseTokenNetworkContentList(swapNetworks: ImmutableList) { + Column { + swapNetworks.fastForEach { network -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(), + onClick = { network.onNetworkClick() }, + ) + .padding( + vertical = 12.dp, + horizontal = 14.dp, + ), + ) { + Image( + modifier = Modifier.size(36.dp), + painter = painterResource(id = network.iconResId), + contentDescription = null, + ) + Text( + text = network.title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = network.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + if (network.hasFixedRate) { + SpacerWMax() + Text( + text = stringResourceSafe(R.string.swap_fixed_rate), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .border( + width = 1.dp, + color = TangemTheme.colors.stroke.primary, + shape = RoundedCornerShape(6.dp), + ) + .padding( + vertical = 2.dp, + horizontal = 4.dp, + ), + ) + } + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun SwapChooseTokenNetworkContent_Preview( + @PreviewParameter(PreviewProvider::class) params: SwapChooseTokenNetworkContentUM, +) { + TangemThemePreview { + SwapChooseTokenNetworkContent(state = params) + } +} + +private class PreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + SwapChooseTokenNetworkContentUM.Loading( + messageContent = messageBottomSheetUM { + infoBlock { + icon(R.drawable.ic_alert_triangle_20) { + type = MessageBottomSheetUMV2.Icon.Type.Attention + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Unspecified + } + title = stringReference("Shiba Inu is not suppoerted") + body = stringReference("This token is not supported. Please choose a different token to swap.") + } + secondaryButton { + text = resourceReference(R.string.warning_button_ok) + onClick { closeBs() } + } + }, + ), + SwapChooseTokenNetworkContentUM.Error( + messageContent = messageBottomSheetUM { + infoBlock { + icon(R.drawable.ic_alert_triangle_20) { + type = MessageBottomSheetUMV2.Icon.Type.Attention + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = stringReference("Shiba Inu is not supported") + body = stringReference("This token is not supported. Please choose a different token to swap.") + } + secondaryButton { + text = resourceReference(R.string.warning_button_ok) + onClick { closeBs() } + } + }, + ), + SwapChooseTokenNetworkContentUM.Content( + messageContent = messageBottomSheetUM { + infoBlock { + icon(R.drawable.img_attention_20) { + type = MessageBottomSheetUMV2.Icon.Type.Attention + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = stringReference("Shiba Inu is not suppoerted") + body = stringReference("This token is not supported. Please choose a different token to swap.") + } + secondaryButton { + text = resourceReference(R.string.warning_button_ok) + onClick { closeBs() } + } + }, + swapNetworks = persistentListOf( + SwapChooseNetworkUM( + title = stringReference("Cardano"), + subtitle = stringReference("MAIN"), + iconResId = R.drawable.img_cardano_22, + hasFixedRate = false, + onNetworkClick = {}, + ), + ), + ), + SwapChooseTokenNetworkContentUM.Content( + messageContent = messageBottomSheetUM { + infoBlock { + icon(R.drawable.img_attention_20) { + type = MessageBottomSheetUMV2.Icon.Type.Attention + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = stringReference("Shiba Inu is not suppoerted") + body = stringReference("This token is not supported. Please choose a different token to swap.") + } + secondaryButton { + text = resourceReference(R.string.warning_button_ok) + onClick { closeBs() } + } + }, + swapNetworks = persistentListOf( + SwapChooseNetworkUM( + title = stringReference("Cardano"), + subtitle = stringReference("MAIN"), + iconResId = R.drawable.img_cardano_22, + hasFixedRate = false, + onNetworkClick = {}, + ), + SwapChooseNetworkUM( + title = stringReference("BNB Smart Chain"), + subtitle = stringReference("BEP20"), + iconResId = R.drawable.img_bsc_22, + hasFixedRate = false, + onNetworkClick = {}, + ), + SwapChooseNetworkUM( + title = stringReference("BNB Smart Chain"), + subtitle = stringReference("BEP20"), + iconResId = R.drawable.img_bsc_22, + hasFixedRate = true, + onNetworkClick = {}, + ), + SwapChooseNetworkUM( + title = stringReference("BNB Smart Chain"), + subtitle = stringReference("BEP20"), + iconResId = R.drawable.img_bsc_22, + hasFixedRate = false, + onNetworkClick = {}, + ), + ), + ), + SwapChooseTokenNetworkContentUM.Content( + messageContent = messageBottomSheetUM { + infoBlock { + icon(R.drawable.img_attention_20) { + type = MessageBottomSheetUMV2.Icon.Type.Attention + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = stringReference("Shiba Inu is not suppoerted") + body = stringReference("This token is not supported. Please choose a different token to swap.") + } + secondaryButton { + text = resourceReference(R.string.warning_button_ok) + onClick { closeBs() } + } + }, + swapNetworks = buildList { + repeat(10) { + add( + SwapChooseNetworkUM( + title = stringReference("Cardano"), + subtitle = stringReference("MAIN"), + iconResId = R.drawable.img_cardano_22, + hasFixedRate = false, + onNetworkClick = {}, + ), + ) + } + }.toPersistentList(), + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 6da65023c5..03ebfc66ca 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -60,7 +60,7 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) }, topBar = { ExpandableSearchView( - title = stringResourceSafe(R.string.swapping_token_list_title), + title = stringResourceSafe(R.string.common_choose_token), onBackClick = onBack, placeholderSearchText = stringResourceSafe(id = R.string.common_search_tokens), onSearchChange = state.onSearchEntered,