diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/SwapSelectTokensComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/SwapSelectTokensComponent.kt new file mode 100644 index 0000000000..2e28ee5552 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/SwapSelectTokensComponent.kt @@ -0,0 +1,22 @@ +package com.tangem.features.onramp.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Swap select tokens component + * +[REDACTED_AUTHOR] + */ +interface SwapSelectTokensComponent : ComposableContentComponent { + + interface Factory : ComponentFactory + + /** + * Params + * + * @property userWalletId user wallet id + */ + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt new file mode 100644 index 0000000000..fdcecebdc0 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt @@ -0,0 +1,68 @@ +package com.tangem.features.onramp.swap + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.onramp.component.SwapSelectTokensComponent +import com.tangem.features.onramp.entity.OnrampOperation +import com.tangem.features.onramp.swap.model.SwapSelectTokensModel +import com.tangem.features.onramp.swap.ui.SwapSelectTokens +import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( + tokenListComponentFactory: OnrampTokenListComponent.Factory, + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: SwapSelectTokensComponent.Params, +) : AppComponentContext by appComponentContext, SwapSelectTokensComponent { + + private val model: SwapSelectTokensModel = getOrCreateModel() + + private val selectFromTokenListComponent: OnrampTokenListComponent = tokenListComponentFactory.create( + context = child(key = "select_from_token_list"), + params = OnrampTokenListComponent.Params( + filterOperation = OnrampOperation.SWAP, + hasSearchBar = true, + userWalletId = params.userWalletId, + onTokenClick = model::selectFromToken, + ), + ) + + private val selectToTokenListComponent: OnrampTokenListComponent = tokenListComponentFactory.create( + context = child(key = "select_to_token_list"), + params = OnrampTokenListComponent.Params( + filterOperation = OnrampOperation.SWAP, + hasSearchBar = true, + userWalletId = params.userWalletId, + onTokenClick = model::selectToToken, + ), + ) + + @Composable + override fun Content(modifier: Modifier) { + val state = model.state.collectAsStateWithLifecycle() + + SwapSelectTokens( + state = state.value, + selectFromTokenListComponent = selectFromTokenListComponent, + selectToTokenListComponent = selectToTokenListComponent, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : SwapSelectTokensComponent.Factory { + + override fun create( + context: AppComponentContext, + params: SwapSelectTokensComponent.Params, + ): DefaultSwapSelectTokensComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/di/SwapSelectTokensComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/di/SwapSelectTokensComponentModule.kt new file mode 100644 index 0000000000..059c28a82a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/di/SwapSelectTokensComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.swap.di + +import com.tangem.features.onramp.component.SwapSelectTokensComponent +import com.tangem.features.onramp.swap.DefaultSwapSelectTokensComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface SwapSelectTokensComponentModule { + + @Binds + @Singleton + fun bindSwapSelectTokensComponentFactory( + factory: DefaultSwapSelectTokensComponent.Factory, + ): SwapSelectTokensComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/di/SwapSelectTokensModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/di/SwapSelectTokensModelModule.kt new file mode 100644 index 0000000000..f7e0741156 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/di/SwapSelectTokensModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.swap.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.swap.model.SwapSelectTokensModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface SwapSelectTokensModelModule { + + @Binds + @IntoMap + @ClassKey(SwapSelectTokensModel::class) + fun bindOnrampTokenListModel(model: SwapSelectTokensModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt new file mode 100644 index 0000000000..64fac02313 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt @@ -0,0 +1,42 @@ +package com.tangem.features.onramp.swap.entity + +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onramp.impl.R +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import timber.log.Timber +import javax.inject.Inject + +/** + * [SwapSelectTokensUM] controller + * + * @property router router + * +[REDACTED_AUTHOR] + */ +internal class SwapSelectTokensController @Inject constructor( + private val router: Router, +) { + + val state: StateFlow get() = _state + + private val _state: MutableStateFlow = MutableStateFlow( + value = SwapSelectTokensUM( + onBackClick = router::pop, + exchangeFrom = ExchangeCardUM.Empty(titleReference = resourceReference(id = R.string.swapping_from_title)), + exchangeTo = ExchangeCardUM.Empty(titleReference = resourceReference(id = R.string.swapping_to_title)), + ), + ) + + fun update(transform: (SwapSelectTokensUM) -> SwapSelectTokensUM) { + Timber.d("Applying non-name transformation") + _state.update(transform) + } + + fun update(transformer: SwapSelectTokensUMTransformer) { + Timber.d("Applying ${transformer::class.simpleName}") + _state.update(transformer::transform) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensUM.kt new file mode 100644 index 0000000000..7defa5ca8f --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensUM.kt @@ -0,0 +1,16 @@ +package com.tangem.features.onramp.swap.entity + +/** + * Swap select tokens UI model + * + * @property onBackClick callback is called when back button is clicked + * @property exchangeFrom exchange "from" card UI model + * @property exchangeTo exchange "to" card UI model + * +[REDACTED_AUTHOR] + */ +internal data class SwapSelectTokensUM( + val onBackClick: () -> Unit, + val exchangeFrom: ExchangeCardUM, + val exchangeTo: ExchangeCardUM, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensUMTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensUMTransformer.kt new file mode 100644 index 0000000000..fb2f9ca537 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensUMTransformer.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.swap.entity + +import com.tangem.utils.transformer.Transformer + +/** + * Base [SwapSelectTokensUM] transformer + * +[REDACTED_AUTHOR] + */ +internal interface SwapSelectTokensUMTransformer : Transformer \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/RemoveSelectedFromTokenTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/RemoveSelectedFromTokenTransformer.kt new file mode 100644 index 0000000000..52d581ddf2 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/RemoveSelectedFromTokenTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.swap.entity.transformer + +import com.tangem.features.onramp.swap.entity.ExchangeCardUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer + +/** + * Transformer for removing selected "from" token + * +[REDACTED_AUTHOR] + */ +internal object RemoveSelectedFromTokenTransformer : SwapSelectTokensUMTransformer { + + override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM { + return prevState.copy( + exchangeFrom = ExchangeCardUM.Empty(titleReference = prevState.exchangeFrom.titleReference), + exchangeTo = ExchangeCardUM.Empty(titleReference = prevState.exchangeTo.titleReference), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt new file mode 100644 index 0000000000..08c088f59e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.features.onramp.swap.entity.transformer + +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.features.onramp.swap.entity.ExchangeCardUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer +import com.tangem.features.onramp.swap.entity.utils.toFilled + +/** + * Transformer for selecting "from" token + * + * @property selectedTokenItemState token item state + * @property onRemoveClick callback is called when remove button is clicked + * +[REDACTED_AUTHOR] + */ +internal class SelectFromTokenTransformer( + private val selectedTokenItemState: TokenItemState, + private val onRemoveClick: () -> Unit, +) : SwapSelectTokensUMTransformer { + + override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM { + return prevState.copy( + exchangeFrom = prevState.exchangeFrom.toFilled( + selectedTokenItemState = selectedTokenItemState, + removeButtonUM = ExchangeCardUM.RemoveButtonUM(onClick = onRemoveClick), + ), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt new file mode 100644 index 0000000000..433b28f5ff --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.features.onramp.swap.entity.transformer + +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.features.onramp.swap.entity.ExchangeCardUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer +import com.tangem.features.onramp.swap.entity.utils.toFilled + +/** + * Transformer for selecting "to" token + * + * @property selectedTokenItemState token item state + * +[REDACTED_AUTHOR] + */ +internal class SelectToTokenTransformer( + private val selectedTokenItemState: TokenItemState, +) : SwapSelectTokensUMTransformer { + + override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM { + return prevState.copy( + exchangeFrom = prevState.exchangeFrom.hideRemoveButton(), + exchangeTo = prevState.exchangeTo.toFilled(selectedTokenItemState = selectedTokenItemState), + ) + } + + private fun ExchangeCardUM.hideRemoveButton(): ExchangeCardUM { + return (this as? ExchangeCardUM.Filled)?.copy(removeButtonUM = null) ?: this + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt new file mode 100644 index 0000000000..5e6a03ea40 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt @@ -0,0 +1,21 @@ +package com.tangem.features.onramp.swap.entity.utils + +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.features.onramp.swap.entity.ExchangeCardUM + +/** + * Convert from [ExchangeCardUM] to [ExchangeCardUM.Filled] + * + * @param selectedTokenItemState token item state + * @param removeButtonUM remove button UI model + */ +internal fun ExchangeCardUM.toFilled( + selectedTokenItemState: TokenItemState, + removeButtonUM: ExchangeCardUM.RemoveButtonUM? = null, +): ExchangeCardUM.Filled { + return ExchangeCardUM.Filled( + titleReference = titleReference, + tokenItemState = selectedTokenItemState, + removeButtonUM = removeButtonUM, + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt new file mode 100644 index 0000000000..858f97a8c0 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt @@ -0,0 +1,62 @@ +package com.tangem.features.onramp.swap.model + +import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.onramp.swap.entity.SwapSelectTokensController +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM +import com.tangem.features.onramp.swap.entity.transformer.RemoveSelectedFromTokenTransformer +import com.tangem.features.onramp.swap.entity.transformer.SelectFromTokenTransformer +import com.tangem.features.onramp.swap.entity.transformer.SelectToTokenTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class SwapSelectTokensModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val controller: SwapSelectTokensController, +) : Model() { + + val state: StateFlow = controller.state + + private var fromCurrencyStatus: CryptoCurrencyStatus? = null + private var toCurrencyStatus: CryptoCurrencyStatus? = null + + /** + * Select "from" token + * + * @param selectedTokenItemState selected token item state + * @param status crypto currency status + */ + fun selectFromToken(selectedTokenItemState: TokenItemState, status: CryptoCurrencyStatus) { + fromCurrencyStatus = status + + controller.update( + transformer = SelectFromTokenTransformer( + selectedTokenItemState = selectedTokenItemState, + onRemoveClick = ::onRemoveClick, + ), + ) + } + + /** + * Select "to" token + * + * @param selectedTokenItemState selected token item state + * @param status crypto currency status + */ + fun selectToToken(selectedTokenItemState: TokenItemState, status: CryptoCurrencyStatus) { + toCurrencyStatus = status + + controller.update(transformer = SelectToTokenTransformer(selectedTokenItemState)) + + // TODO: [REDACTED_JIRA] + } + + private fun onRemoveClick() { + fromCurrencyStatus = null + + controller.update(transformer = RemoveSelectedFromTokenTransformer) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt new file mode 100644 index 0000000000..3179099e4d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt @@ -0,0 +1,100 @@ +package com.tangem.features.onramp.swap.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.swap.entity.ExchangeCardUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM +import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent + +/** + * Swap select tokens + * + * @param state state + * @param selectFromTokenListComponent select "from" token list component + * @param selectToTokenListComponent select "to" token list component + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun SwapSelectTokens( + state: SwapSelectTokensUM, + selectFromTokenListComponent: OnrampTokenListComponent, + selectToTokenListComponent: OnrampTokenListComponent, + modifier: Modifier = Modifier, +) { + BackHandler(onBack = state.onBackClick) + + LazyColumn( + modifier = modifier + .background(TangemTheme.colors.background.secondary) + .imePadding() + .systemBarsPadding(), + contentPadding = PaddingValues(bottom = 8.dp), + ) { + stickyHeader(key = "header") { + AppBarWithBackButton( + onBackClick = state.onBackClick, + text = stringResource(id = R.string.common_swap), + iconRes = R.drawable.ic_close_24, + containerColor = TangemTheme.colors.background.secondary, + ) + } + + item(key = "exchange_from", contentType = "exchange_from") { + ExchangeCard( + state = state.exchangeFrom, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 8.dp, bottom = 12.dp) + .animateItem(), + ) + } + + if (state.exchangeFrom is ExchangeCardUM.Empty) { + item(key = "select_from", contentType = "select_from") { + selectFromTokenListComponent.Content( + modifier = Modifier + .padding(horizontal = 16.dp) + .animateItem(), + ) + } + } + + if (state.exchangeFrom is ExchangeCardUM.Filled) { + item(key = "exchange_to", contentType = "exchange_to") { + ExchangeCard( + state = state.exchangeTo, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp) + .animateItem(), + ) + } + + if (state.exchangeTo is ExchangeCardUM.Empty) { + item(key = "select_to", contentType = "select_to") { + selectToTokenListComponent.Content( + modifier = Modifier + .padding(horizontal = 16.dp) + .animateItem(), + ) + } + } + } + } +} \ No newline at end of file