Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-03 17:56:53 +04:00
parent edc5a0cf13
commit aa6ddb9428
13 changed files with 461 additions and 0 deletions

View file

@ -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, SwapSelectTokensComponent>
/**
* Params
*
* @property userWalletId user wallet id
*/
data class Params(val userWalletId: UserWalletId)
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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<SwapSelectTokensUM> get() = _state
private val _state: MutableStateFlow<SwapSelectTokensUM> = 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)
}
}

View file

@ -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,
)

View file

@ -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<SwapSelectTokensUM>

View file

@ -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),
)
}
}

View file

@ -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),
),
)
}
}

View file

@ -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
}
}

View file

@ -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,
)
}

View file

@ -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<SwapSelectTokensUM> = 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)
}
}

View file

@ -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(),
)
}
}
}
}
}