Updated on 2026-08-14
This commit is contained in:
parent
0be9414914
commit
0fe5355c74
26 changed files with 666 additions and 66 deletions
|
|
@ -62,9 +62,9 @@ internal class OnboardingManageTokensModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
|
||||
combine(
|
||||
manageTokensListManager.currenciesToAdd,
|
||||
manageTokensListManager.currenciesToRemove,
|
||||
::handleChangedCurrencies,
|
||||
flow = manageTokensListManager.currenciesToAdd,
|
||||
flow2 = manageTokensListManager.currenciesToRemove,
|
||||
transform = ::handleChangedCurrencies,
|
||||
).launchIn(modelScope)
|
||||
|
||||
observeSearchQueryChanges()
|
||||
|
|
|
|||
|
|
@ -4,12 +4,20 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.jetpack.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.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.features.onramp.component.ConfirmResidencyComponent
|
||||
import com.tangem.features.onramp.model.ConfirmResidencyModel
|
||||
import com.tangem.features.onramp.selectcountry.SelectCountryComponent
|
||||
import com.tangem.features.onramp.selectcountry.entity.ConfirmResidencyBottomSheetConfig
|
||||
import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheet
|
||||
import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheetContent
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -19,9 +27,16 @@ import dagger.assisted.AssistedInject
|
|||
internal class DefaultConfirmResidencyComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: ConfirmResidencyComponent.Params,
|
||||
private val selectCountryComponentFactory: SelectCountryComponent.Factory,
|
||||
) : ConfirmResidencyComponent, AppComponentContext by context {
|
||||
|
||||
private val model: ConfirmResidencyModel = getOrCreateModel(params)
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = null,
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
override fun dismiss() {
|
||||
params.onDismiss()
|
||||
|
|
@ -30,6 +45,7 @@ internal class DefaultConfirmResidencyComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
val bottomSheetConfig = remember {
|
||||
TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
|
|
@ -43,6 +59,18 @@ internal class DefaultConfirmResidencyComponent @AssistedInject constructor(
|
|||
ConfirmResidencyBottomSheetContent(model = state, modifier = modifier)
|
||||
},
|
||||
)
|
||||
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: ConfirmResidencyBottomSheetConfig,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = when (config) {
|
||||
ConfirmResidencyBottomSheetConfig.SelectCountry -> selectCountryComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = SelectCountryComponent.Params(onDismiss = model.bottomSheetNavigation::dismiss),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.features.onramp.model
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -7,6 +9,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.features.onramp.component.ConfirmResidencyComponent
|
||||
import com.tangem.features.onramp.entity.ConfirmResidencyUM
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.selectcountry.entity.ConfirmResidencyBottomSheetConfig
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import javax.inject.Inject
|
||||
|
|
@ -18,6 +21,7 @@ internal class ConfirmResidencyModel @Inject constructor(
|
|||
) : Model() {
|
||||
|
||||
private val params: ConfirmResidencyComponent.Params = paramsContainer.require()
|
||||
val bottomSheetNavigation: SlotNavigation<ConfirmResidencyBottomSheetConfig> = SlotNavigation()
|
||||
val state: MutableStateFlow<ConfirmResidencyUM> = MutableStateFlow(
|
||||
ConfirmResidencyUM(
|
||||
country = params.countryName,
|
||||
|
|
@ -48,6 +52,6 @@ internal class ConfirmResidencyModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onChangeClick() {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
bottomSheetNavigation.activate(ConfirmResidencyBottomSheetConfig.SelectCountry)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.features.onramp.selectcountry
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.features.onramp.selectcountry.model.OnrampSelectCountryModel
|
||||
import com.tangem.features.onramp.selectcountry.ui.OnrampCountryList
|
||||
import com.tangem.features.onramp.selectcountry.ui.SelectCountryBottomSheet
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultSelectCountryComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SelectCountryComponent.Params,
|
||||
) : SelectCountryComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: OnrampSelectCountryModel = getOrCreateModel(params)
|
||||
|
||||
override fun dismiss() {
|
||||
params.onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bottomSheetConfig = remember {
|
||||
TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
)
|
||||
}
|
||||
SelectCountryBottomSheet(
|
||||
config = bottomSheetConfig,
|
||||
content = { OnrampCountryList(state = state, modifier = Modifier.fillMaxSize()) },
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SelectCountryComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: SelectCountryComponent.Params,
|
||||
): DefaultSelectCountryComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.onramp.selectcountry
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
|
||||
interface SelectCountryComponent : ComposableBottomSheetComponent {
|
||||
|
||||
data class Params(val onDismiss: () -> Unit)
|
||||
|
||||
interface Factory : ComponentFactory<Params, SelectCountryComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.onramp.selectcountry.di
|
||||
|
||||
import com.tangem.features.onramp.selectcountry.DefaultSelectCountryComponent
|
||||
import com.tangem.features.onramp.selectcountry.SelectCountryComponent
|
||||
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 OnrampSelectCountryComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindOnrampSelectCountryComponentFactory(
|
||||
factory: DefaultSelectCountryComponent.Factory,
|
||||
): SelectCountryComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.onramp.selectcountry.di
|
||||
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.onramp.selectcountry.model.OnrampSelectCountryModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(DecomposeComponent::class)
|
||||
internal interface OnrampSelectCountryModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnrampSelectCountryModel::class)
|
||||
fun bindOnrampSelectCountryModel(model: OnrampSelectCountryModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.onramp.selectcountry.entity
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal sealed class ConfirmResidencyBottomSheetConfig {
|
||||
|
||||
@Serializable
|
||||
data object SelectCountry : ConfirmResidencyBottomSheetConfig()
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.onramp.selectcountry.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
|
||||
@Immutable
|
||||
internal sealed interface CountriesListItemUM {
|
||||
/** Unique ID */
|
||||
val id: Any
|
||||
|
||||
data class SearchBar(
|
||||
override val id: Any = "search_bar",
|
||||
val searchBarUM: SearchBarUM,
|
||||
) : CountriesListItemUM
|
||||
|
||||
data class Country(val state: CountryItemState) : CountriesListItemUM {
|
||||
override val id = state.id
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.onramp.selectcountry.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
internal sealed interface CountryItemState {
|
||||
/** Unique id */
|
||||
val id: String
|
||||
|
||||
data class Loading(override val id: String) : CountryItemState
|
||||
|
||||
data class Unavailable(
|
||||
override val id: String,
|
||||
val flagUrl: String,
|
||||
val countryName: String,
|
||||
) : CountryItemState
|
||||
|
||||
data class Content(
|
||||
override val id: String,
|
||||
val flagUrl: String,
|
||||
val countryName: String,
|
||||
val isSelected: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
) : CountryItemState
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.onramp.selectcountry.entity
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal data class CountryListUM(val items: ImmutableList<CountriesListItemUM>) {
|
||||
|
||||
/** Get search bar if it exists */
|
||||
fun getSearchBar(): CountriesListItemUM.SearchBar? {
|
||||
return items.firstOrNull() as? CountriesListItemUM.SearchBar
|
||||
}
|
||||
|
||||
/** Get tokens */
|
||||
fun getTokens(): ImmutableList<CountriesListItemUM> {
|
||||
if (getSearchBar() == null) return items
|
||||
|
||||
return if (items.size > 1) {
|
||||
items.subList(fromIndex = 1, toIndex = items.size)
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.features.onramp.selectcountry.entity
|
||||
|
||||
import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsTransformer
|
||||
import com.tangem.features.onramp.selectcountry.model.MockedCountriesData
|
||||
import com.tangem.features.onramp.utils.SearchBarUMTransformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class CountryListUMController @Inject constructor() {
|
||||
|
||||
val state: StateFlow<CountryListUM> get() = _state.asStateFlow()
|
||||
private val _state: MutableStateFlow<CountryListUM> = MutableStateFlow(
|
||||
value = CountryListUM(
|
||||
items = MockedCountriesData.getLoadingItems().map(CountriesListItemUM::Country).toImmutableList(),
|
||||
),
|
||||
)
|
||||
|
||||
fun update(transformer: UpdateCountryItemsTransformer) {
|
||||
Timber.d("Applying ${transformer::class.simpleName}")
|
||||
_state.update(transformer::transform)
|
||||
}
|
||||
|
||||
fun update(transformer: SearchBarUMTransformer) {
|
||||
Timber.d("Applying ${transformer::class.simpleName}")
|
||||
_state.update { prevState ->
|
||||
val searchBarItem = prevState.getSearchBar()
|
||||
|
||||
if (searchBarItem != null) {
|
||||
val updatedSearchBar =
|
||||
searchBarItem.copy(searchBarUM = transformer.transform(searchBarItem.searchBarUM))
|
||||
|
||||
prevState.copy(
|
||||
items = persistentListOf(
|
||||
updatedSearchBar,
|
||||
*prevState.getTokens().toTypedArray(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Get search bar if it exists */
|
||||
fun getSearchBar(): CountriesListItemUM.SearchBar? {
|
||||
return _state.value.getSearchBar()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.onramp.selectcountry.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.selectcountry.entity.CountriesListItemUM
|
||||
import com.tangem.features.onramp.selectcountry.entity.CountryItemState
|
||||
import com.tangem.features.onramp.selectcountry.entity.CountryListUM
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class UpdateCountryItemsTransformer(
|
||||
private val countries: List<CountryItemState>,
|
||||
private val onQueryChange: (String) -> Unit,
|
||||
private val onActiveChange: (Boolean) -> Unit,
|
||||
) {
|
||||
|
||||
fun transform(prevState: CountryListUM): CountryListUM {
|
||||
val searchBarItem = prevState.getSearchBar() ?: createSearchBarItem()
|
||||
|
||||
return prevState.copy(
|
||||
items = (listOfNotNull(searchBarItem) + countries.map(::convertCountry)).toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Temporarily. Will be refactored after implement domain
|
||||
private fun convertCountry(state: CountryItemState): CountriesListItemUM.Country =
|
||||
CountriesListItemUM.Country(state)
|
||||
|
||||
private fun createSearchBarItem(): CountriesListItemUM.SearchBar {
|
||||
return CountriesListItemUM.SearchBar(
|
||||
searchBarUM = SearchBarUM(
|
||||
placeholderText = resourceReference(id = R.string.common_search),
|
||||
query = "",
|
||||
onQueryChange = onQueryChange,
|
||||
isActive = false,
|
||||
onActiveChange = onActiveChange,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.onramp.selectcountry.model
|
||||
|
||||
import com.tangem.features.onramp.selectcountry.entity.CountryItemState
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
// TODO: Temporarily. Will be deleted after implement domain
|
||||
@Suppress("MagicNumber")
|
||||
internal object MockedCountriesData {
|
||||
|
||||
fun getCountryItems(): Flow<List<CountryItemState>> = flow {
|
||||
delay(3_000) // simulate network call
|
||||
emit(getMockedCountries())
|
||||
}
|
||||
fun getLoadingItems(): List<CountryItemState> = MutableList(5) {
|
||||
CountryItemState.Loading("Loading #$it")
|
||||
}
|
||||
|
||||
private fun getMockedCountries(): List<CountryItemState> = MutableList(50) { index ->
|
||||
if (index % 5 == 0) {
|
||||
CountryItemState.Unavailable(
|
||||
id = "Country #$index",
|
||||
flagUrl = "",
|
||||
countryName = "Country $index",
|
||||
)
|
||||
} else {
|
||||
CountryItemState.Content(
|
||||
id = "Country #$index",
|
||||
flagUrl = "",
|
||||
countryName = "Country $index",
|
||||
onClick = {},
|
||||
isSelected = index == 7,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.tangem.features.onramp.selectcountry.model
|
||||
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.selectcountry.entity.CountryItemState
|
||||
import com.tangem.features.onramp.selectcountry.entity.CountryListUM
|
||||
import com.tangem.features.onramp.selectcountry.entity.CountryListUMController
|
||||
import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsTransformer
|
||||
import com.tangem.features.onramp.utils.SearchManager
|
||||
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class OnrampSelectCountryModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val searchManager: SearchManager,
|
||||
private val countryListUMController: CountryListUMController,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<CountryListUM> = countryListUMController.state
|
||||
|
||||
init {
|
||||
subscribeOnUpdateState()
|
||||
}
|
||||
|
||||
private fun subscribeOnUpdateState() {
|
||||
combine(
|
||||
flow = MockedCountriesData.getCountryItems(),
|
||||
flow2 = searchManager.query,
|
||||
) { countryItems, query ->
|
||||
val filteredCountryItems = countryItems.filterByQuery(query = query)
|
||||
|
||||
UpdateCountryItemsTransformer(
|
||||
countries = filteredCountryItems,
|
||||
onQueryChange = ::onSearchQueryChange,
|
||||
onActiveChange = ::onSearchBarActiveChange,
|
||||
)
|
||||
}
|
||||
.onEach(countryListUMController::update)
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onSearchQueryChange(newQuery: String) {
|
||||
val searchBar = countryListUMController.getSearchBar()
|
||||
if (searchBar?.searchBarUM?.query == newQuery) return
|
||||
|
||||
modelScope.launch {
|
||||
countryListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery))
|
||||
|
||||
searchManager.update(newQuery)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchBarActiveChange(isActive: Boolean) {
|
||||
countryListUMController.update(
|
||||
transformer = UpdateSearchBarActiveStateTransformer(
|
||||
isActive = isActive,
|
||||
placeHolder = resourceReference(id = R.string.common_search),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Temporarily. Will be refactored after implement domain
|
||||
private fun List<CountryItemState>.filterByQuery(query: String): List<CountryItemState> {
|
||||
return filter {
|
||||
(it as? CountryItemState.Content)?.countryName?.contains(query) == true ||
|
||||
(it as? CountryItemState.Unavailable)?.countryName?.contains(query) == true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
package com.tangem.features.onramp.selectcountry.ui
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import coil.compose.AsyncImage
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.selectcountry.entity.CountriesListItemUM
|
||||
import com.tangem.features.onramp.selectcountry.entity.CountryItemState
|
||||
|
||||
@Composable
|
||||
internal fun CountryListItem(state: CountriesListItemUM, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is CountriesListItemUM.Country -> CountryItem(
|
||||
state = state.state,
|
||||
modifier = modifier.countryClickable(state.state).padding(all = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
is CountriesListItemUM.SearchBar -> SearchBar(
|
||||
state = state.searchBarUM,
|
||||
modifier = modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CountryItem(state: CountryItemState, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is CountryItemState.Content -> ContentCountryItem(state = state, modifier = modifier)
|
||||
is CountryItemState.Loading -> LoadingCountryItem(modifier)
|
||||
is CountryItemState.Unavailable -> UnavailableCountryItem(state = state, modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentCountryItem(state: CountryItemState.Content, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size36),
|
||||
model = state.flagUrl,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = state.countryName,
|
||||
modifier = Modifier.weight(1F),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
if (state.isSelected) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_check_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingCountryItem(modifier: Modifier = Modifier) {
|
||||
Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12)) {
|
||||
CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size36))
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12),
|
||||
radius = TangemTheme.dimens.radius4,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("MagicNumber")
|
||||
private fun UnavailableCountryItem(state: CountryItemState.Unavailable, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.alpha(0.4F),
|
||||
model = state.flagUrl,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.weight(1F),
|
||||
text = state.countryName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
)
|
||||
Text(
|
||||
text = "Unavailable",
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun Modifier.countryClickable(state: CountryItemState): Modifier = composed {
|
||||
when (state) {
|
||||
is CountryItemState.Content -> combinedClickable(onClick = state.onClick)
|
||||
is CountryItemState.Loading,
|
||||
is CountryItemState.Unavailable,
|
||||
-> this
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.onramp.selectcountry.ui
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.features.onramp.selectcountry.entity.CountryListUM
|
||||
|
||||
@Composable
|
||||
internal fun SelectCountryBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) {
|
||||
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = config,
|
||||
content = { content() },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun OnrampCountryList(state: CountryListUM, modifier: Modifier = Modifier) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
items(
|
||||
items = state.items,
|
||||
key = { item -> item.id },
|
||||
contentType = { item -> item::class.java },
|
||||
itemContent = { item ->
|
||||
CountryListItem(
|
||||
state = item,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.onramp.tokenlist.entity
|
||||
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.features.onramp.utils.SearchBarUMTransformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -35,6 +36,28 @@ internal class TokenListUMController @Inject constructor() {
|
|||
_state.update(transformer::transform)
|
||||
}
|
||||
|
||||
fun update(transformer: SearchBarUMTransformer) {
|
||||
Timber.d("Applying ${transformer::class.simpleName}")
|
||||
_state.update { prevState ->
|
||||
val searchBarItem = prevState.getSearchBar()
|
||||
|
||||
if (searchBarItem != null) {
|
||||
val updatedSearchBar = searchBarItem.copy(
|
||||
searchBarUM = transformer.transform(searchBarItem.searchBarUM),
|
||||
)
|
||||
|
||||
prevState.copy(
|
||||
availableItems = persistentListOf(
|
||||
updatedSearchBar,
|
||||
*prevState.getTokens().toTypedArray(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Get search bar if it exists */
|
||||
fun getSearchBar(): TokensListItemUM.SearchBar? {
|
||||
return _state.value.getSearchBar()
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
package com.tangem.features.onramp.tokenlist.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* Base [SearchBarUM] transformer
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal abstract class SearchBarUMTransformer : TokenListUMTransformer {
|
||||
|
||||
abstract fun transform(prevState: SearchBarUM): SearchBarUM
|
||||
|
||||
override fun transform(prevState: TokenListUM): TokenListUM {
|
||||
val searchBarItem = prevState.getSearchBar()
|
||||
|
||||
return if (searchBarItem != null) {
|
||||
val updatedSearchBar = searchBarItem.copy(searchBarUM = transform(searchBarItem.searchBarUM))
|
||||
|
||||
prevState.copy(
|
||||
availableItems = persistentListOf(
|
||||
updatedSearchBar,
|
||||
*prevState.getTokens().toTypedArray(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.features.onramp.tokenlist.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.onramp.impl.R
|
||||
|
||||
internal class UpdateSearchBarActiveStateTransformer(private val isActive: Boolean) : SearchBarUMTransformer() {
|
||||
|
||||
override fun transform(prevState: SearchBarUM): SearchBarUM {
|
||||
val placeholderText = if (isActive) {
|
||||
TextReference.EMPTY
|
||||
} else {
|
||||
resourceReference(id = R.string.common_search)
|
||||
}
|
||||
|
||||
return prevState.copy(placeholderText = placeholderText, isActive = isActive)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.onramp.tokenlist.model
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
|
|
@ -11,15 +12,16 @@ import com.tangem.domain.exchange.RampStateManager
|
|||
import com.tangem.domain.tokens.GetTokenListUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.entity.OnrampOperation
|
||||
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateSearchBarActiveStateTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateSearchQueryTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
|
||||
import com.tangem.features.onramp.tokenlist.utils.SearchTokensManager
|
||||
import com.tangem.features.onramp.utils.SearchManager
|
||||
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -30,7 +32,7 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val tokenListUMController: TokenListUMController,
|
||||
private val searchTokensManager: SearchTokensManager,
|
||||
private val searchManager: SearchManager,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val getTokenListUseCase: GetTokenListUseCase,
|
||||
|
|
@ -54,7 +56,7 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
flow = getTokenListUseCase.launch(userWalletId = params.userWalletId).distinctUntilChanged(),
|
||||
flow2 = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(),
|
||||
flow3 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(),
|
||||
flow4 = searchTokensManager.query,
|
||||
flow4 = searchManager.query,
|
||||
) { maybeTokenList, appCurrency, isBalanceHidden, query ->
|
||||
val currencies = maybeTokenList.getOrElse(
|
||||
ifLoading = { it ?: TokenList.Empty },
|
||||
|
|
@ -88,12 +90,17 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
tokenListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery))
|
||||
|
||||
searchTokensManager.update(newQuery)
|
||||
searchManager.update(newQuery)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchBarActiveChange(isActive: Boolean) {
|
||||
tokenListUMController.update(transformer = UpdateSearchBarActiveStateTransformer(isActive))
|
||||
tokenListUMController.update(
|
||||
transformer = UpdateSearchBarActiveStateTransformer(
|
||||
isActive = isActive,
|
||||
placeHolder = resourceReference(id = R.string.common_search),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<CryptoCurrencyStatus>.filterByQuery(query: String): List<CryptoCurrencyStatus> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.onramp.utils
|
||||
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
/**
|
||||
* Base [SearchBarUM] transformer
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal abstract class SearchBarUMTransformer : Transformer<SearchBarUM> {
|
||||
|
||||
abstract override fun transform(prevState: SearchBarUM): SearchBarUM
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.tokenlist.utils
|
||||
package com.tangem.features.onramp.utils
|
||||
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.withDebounce
|
||||
|
|
@ -12,7 +12,7 @@ import javax.inject.Inject
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SearchTokensManager @Inject constructor() {
|
||||
internal class SearchManager @Inject constructor() {
|
||||
|
||||
val query: Flow<String>
|
||||
get() = _query
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.onramp.utils
|
||||
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal class UpdateSearchBarActiveStateTransformer(
|
||||
private val isActive: Boolean,
|
||||
private val placeHolder: TextReference,
|
||||
) : SearchBarUMTransformer() {
|
||||
|
||||
override fun transform(prevState: SearchBarUM): SearchBarUM {
|
||||
return prevState.copy(placeholderText = if (isActive) TextReference.EMPTY else placeHolder, isActive = isActive)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.tokenlist.entity.transformer
|
||||
package com.tangem.features.onramp.utils
|
||||
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue