From 3b4d1ad6f6ee63b4fe2618041392b7dba4241b23 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Nov 2024 13:33:40 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../DefaultConfirmResidencyComponent.kt | 4 +- .../model/ConfirmResidencyModel.kt | 2 +- .../ConfirmResidencyBottomSheetConfig.kt | 2 +- .../entity/CountriesListItemUM.kt | 19 -- .../selectcountry/entity/CountryItemState.kt | 26 +-- .../selectcountry/entity/CountryListUM.kt | 34 ++-- .../entity/CountryListUMController.kt | 48 ++--- .../UpdateCountryItemsLoadingTransformer.kt | 14 ++ .../UpdateCountryItemsTransformer.kt | 40 ++-- .../model/MockedCountriesData.kt | 34 ---- .../model/OnrampSelectCountryModel.kt | 47 ++++- .../selectcountry/ui/CountryListItem.kt | 136 ------------- .../ui/SelectCountryBottomSheet.kt | 180 ++++++++++++++++-- .../entity/CurrencyListController.kt | 31 ++- .../UpdateCurrencyItemsLoadingTransformer.kt | 16 ++ .../model/OnrampSelectCurrencyModel.kt | 26 ++- .../ui/SelectCurrencyBottomSheet.kt | 32 +++- 17 files changed, 367 insertions(+), 324 deletions(-) rename features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/{selectcountry => }/entity/ConfirmResidencyBottomSheetConfig.kt (78%) delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountriesListItemUM.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsLoadingTransformer.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/MockedCountriesData.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/CountryListItem.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsLoadingTransformer.kt diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt index 5d6b10d508..fa72ff4f67 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt @@ -15,10 +15,10 @@ 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.confirmresidency.model.ConfirmResidencyModel -import com.tangem.features.onramp.selectcountry.SelectCountryComponent -import com.tangem.features.onramp.selectcountry.entity.ConfirmResidencyBottomSheetConfig import com.tangem.features.onramp.confirmresidency.ui.ConfirmResidencyBottomSheet import com.tangem.features.onramp.confirmresidency.ui.ConfirmResidencyBottomSheetContent +import com.tangem.features.onramp.entity.ConfirmResidencyBottomSheetConfig +import com.tangem.features.onramp.selectcountry.SelectCountryComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt index bedc4c6094..44a4678bbe 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyUM import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.selectcountry.entity.ConfirmResidencyBottomSheetConfig +import com.tangem.features.onramp.entity.ConfirmResidencyBottomSheetConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/ConfirmResidencyBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyBottomSheetConfig.kt similarity index 78% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/ConfirmResidencyBottomSheetConfig.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyBottomSheetConfig.kt index be03ad39a9..cccafaaa8d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/ConfirmResidencyBottomSheetConfig.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyBottomSheetConfig.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.selectcountry.entity +package com.tangem.features.onramp.entity import kotlinx.serialization.Serializable diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountriesListItemUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountriesListItemUM.kt deleted file mode 100644 index 04b6f8bdee..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountriesListItemUM.kt +++ /dev/null @@ -1,19 +0,0 @@ -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 - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryItemState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryItemState.kt index 049c7dc55d..033e746ae1 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryItemState.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryItemState.kt @@ -9,17 +9,19 @@ internal sealed interface CountryItemState { data class Loading(override val id: String) : CountryItemState - data class Unavailable( - override val id: String, - val flagUrl: String, - val countryName: String, - ) : CountryItemState + sealed interface WithContent : CountryItemState { + data class Unavailable( + override val id: String, + val flagUrl: String, + val countryName: String, + ) : WithContent - data class Content( - override val id: String, - val flagUrl: String, - val countryName: String, - val isSelected: Boolean, - val onClick: () -> Unit, - ) : CountryItemState + data class Content( + override val id: String, + val flagUrl: String, + val countryName: String, + val isSelected: Boolean, + val onClick: () -> Unit, + ) : WithContent + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUM.kt index 6006c973c8..652407fced 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUM.kt @@ -1,23 +1,31 @@ package com.tangem.features.onramp.selectcountry.entity +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.fields.entity.SearchBarUM import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -internal data class CountryListUM(val items: ImmutableList) { +@Immutable +internal sealed interface CountryListUM { - /** Get search bar if it exists */ - fun getSearchBar(): CountriesListItemUM.SearchBar? { - return items.firstOrNull() as? CountriesListItemUM.SearchBar - } + val searchBarUM: SearchBarUM - /** Get tokens */ - fun getCountries(): ImmutableList { - if (getSearchBar() == null) return items + data class Loading( + override val searchBarUM: SearchBarUM, + val items: ImmutableList, + ) : CountryListUM - return if (items.size > 1) { - items.subList(fromIndex = 1, toIndex = items.size) - } else { - persistentListOf() + data class Error(override val searchBarUM: SearchBarUM, val onRetry: () -> Unit) : CountryListUM + + data class Content( + override val searchBarUM: SearchBarUM, + val items: ImmutableList, + ) : CountryListUM + + fun copySealed(searchBarUM: SearchBarUM = this.searchBarUM): CountryListUM { + return when (this) { + is Loading -> this.copy(searchBarUM = searchBarUM) + is Error -> this.copy(searchBarUM = searchBarUM) + is Content -> this.copy(searchBarUM = searchBarUM) } } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUMController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUMController.kt index 88015101bb..94114f2c2a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUMController.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUMController.kt @@ -1,55 +1,31 @@ package com.tangem.features.onramp.selectcountry.entity -import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsTransformer +import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.features.onramp.utils.SearchBarUMTransformer -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import javax.inject.Inject -private const val LOADING_ITEMS_COUNT = 5 - -internal class CountryListUMController @Inject constructor() { +internal class CountryListUMController( + searchBarUM: SearchBarUM, + loadingItems: ImmutableList, +) { val state: StateFlow get() = _state.asStateFlow() - private val _state: MutableStateFlow = MutableStateFlow( - value = CountryListUM( - items = getLoadingItems().map(CountriesListItemUM::Country).toImmutableList(), - ), - ) + private val _state: MutableStateFlow = + MutableStateFlow(value = CountryListUM.Loading(searchBarUM = searchBarUM, items = loadingItems)) - fun update(transformer: UpdateCountryItemsTransformer) { + fun update(transformer: Transformer) { _state.update(transformer::transform) } fun update(transformer: SearchBarUMTransformer) { _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.getCountries().toTypedArray(), - ), - ) - } else { - prevState - } + val searchBarUM = transformer.transform(prevState.searchBarUM) + prevState.copySealed(searchBarUM) } } - - /** Get search bar if it exists */ - fun getSearchBar(): CountriesListItemUM.SearchBar? { - return _state.value.getSearchBar() - } - - private fun getLoadingItems(): List = - MutableList(LOADING_ITEMS_COUNT) { CountryItemState.Loading("Loading #$it") } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsLoadingTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsLoadingTransformer.kt new file mode 100644 index 0000000000..56f0b5f5cb --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsLoadingTransformer.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onramp.selectcountry.entity.transformer + +import com.tangem.features.onramp.selectcountry.entity.CountryItemState +import com.tangem.features.onramp.selectcountry.entity.CountryListUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList + +internal class UpdateCountryItemsLoadingTransformer( + private val loadingItems: ImmutableList, +) : Transformer { + override fun transform(prevState: CountryListUM): CountryListUM { + return CountryListUM.Loading(searchBarUM = prevState.searchBarUM, items = loadingItems) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsTransformer.kt index 5489a9910c..0005ddd810 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsTransformer.kt @@ -1,11 +1,7 @@ package com.tangem.features.onramp.selectcountry.entity.transformer import arrow.core.Either -import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.onramp.model.OnrampCountry -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 com.tangem.utils.transformer.Transformer @@ -15,26 +11,27 @@ internal class UpdateCountryItemsTransformer( private val maybeCountries: Either>, private val defaultCountry: OnrampCountry?, private val query: String, - private val onQueryChange: (String) -> Unit, - private val onActiveChange: (Boolean) -> Unit, + private val onRetry: () -> Unit, private val onCountryClick: (OnrampCountry) -> Unit, ) : Transformer { override fun transform(prevState: CountryListUM): CountryListUM { - val searchBarItem = prevState.getSearchBar() ?: createSearchBarItem() return maybeCountries.fold( - ifLeft = { prevState }, // TODO: [REDACTED_JIRA] + ifLeft = { CountryListUM.Error(searchBarUM = prevState.searchBarUM, onRetry = onRetry) }, ifRight = { countries -> val countriesListItems = countries.filterByQuery().toUiModels() - prevState.copy(items = (listOf(searchBarItem) + countriesListItems).toImmutableList()) + CountryListUM.Content( + searchBarUM = prevState.searchBarUM, + items = countriesListItems.toImmutableList(), + ) }, ) } - private fun List.toUiModels(): List { + private fun List.toUiModels(): List { return map { country -> - val countryItemState = if (country.onrampAvailable) { - CountryItemState.Content( + if (country.onrampAvailable) { + CountryItemState.WithContent.Content( id = country.id, flagUrl = country.image, countryName = country.name, @@ -42,9 +39,12 @@ internal class UpdateCountryItemsTransformer( onClick = { onCountryClick(country) }, ) } else { - CountryItemState.Unavailable(id = country.id, flagUrl = country.image, countryName = country.name) + CountryItemState.WithContent.Unavailable( + id = country.id, + flagUrl = country.image, + countryName = country.name, + ) } - CountriesListItemUM.Country(countryItemState) } } @@ -54,16 +54,4 @@ internal class UpdateCountryItemsTransformer( country.name.lowercase().contains(query.lowercase()) } } - - private fun createSearchBarItem(): CountriesListItemUM.SearchBar { - return CountriesListItemUM.SearchBar( - searchBarUM = SearchBarUM( - placeholderText = resourceReference(id = R.string.common_search), - query = "", - onQueryChange = onQueryChange, - isActive = false, - onActiveChange = onActiveChange, - ), - ) - } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/MockedCountriesData.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/MockedCountriesData.kt deleted file mode 100644 index 59c6d5d7c0..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/MockedCountriesData.kt +++ /dev/null @@ -1,34 +0,0 @@ -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> = flow { - delay(3_000) // simulate network call - emit(getMockedCountries()) - } - - private fun getMockedCountries(): List = 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, - ) - } - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt index 8c4915a779..6dbb2cf7d5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt @@ -3,6 +3,7 @@ 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.decompose.model.ParamsContainer +import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.onramp.GetOnrampCountriesUseCase import com.tangem.domain.onramp.GetOnrampCountryUseCase @@ -10,31 +11,39 @@ import com.tangem.domain.onramp.OnrampSaveDefaultCountryUseCase import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selectcountry.SelectCountryComponent +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.UpdateCountryItemsLoadingTransformer 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.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @ComponentScoped -@Suppress("LongParameterList") internal class OnrampSelectCountryModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val searchManager: SearchManager, - private val countryListUMController: CountryListUMController, private val getOnrampCountriesUseCase: GetOnrampCountriesUseCase, private val saveDefaultCountryUseCase: OnrampSaveDefaultCountryUseCase, private val getOnrampCountryUseCase: GetOnrampCountryUseCase, paramsContainer: ParamsContainer, ) : Model() { - val state: StateFlow = countryListUMController.state + val state: StateFlow get() = countryListUMController.state private val params: SelectCountryComponent.Params = paramsContainer.require() + private val countryListUMController = CountryListUMController( + searchBarUM = createSearchBarUM(), + loadingItems = loadingItems, + ) + private val refreshTrigger = MutableSharedFlow() init { modelScope.launch { subscribeOnUpdateState() } @@ -44,9 +53,10 @@ internal class OnrampSelectCountryModel @Inject constructor( params.onDismiss() } + @OptIn(ExperimentalCoroutinesApi::class) private suspend fun subscribeOnUpdateState() { combine( - flow = flowOf(getOnrampCountriesUseCase.invoke()), + flow = refreshTrigger.onStart { emit(Unit) }.flatMapLatest { flowOf(getOnrampCountriesUseCase.invoke()) }, flow2 = getOnrampCountryUseCase.invoke(), flow3 = searchManager.query, ) { maybeCountries, maybeCountry, query -> @@ -54,8 +64,7 @@ internal class OnrampSelectCountryModel @Inject constructor( maybeCountries = maybeCountries, defaultCountry = maybeCountry.getOrNull(), query = query, - onQueryChange = ::onSearchQueryChange, - onActiveChange = ::onSearchBarActiveChange, + onRetry = ::onRetry, onCountryClick = ::saveCountry, ) } @@ -70,9 +79,14 @@ internal class OnrampSelectCountryModel @Inject constructor( } } + private fun onRetry() { + modelScope.launch { refreshTrigger.emit(Unit) } + countryListUMController.update(UpdateCountryItemsLoadingTransformer(loadingItems)) + } + private fun onSearchQueryChange(newQuery: String) { - val searchBar = countryListUMController.getSearchBar() - if (searchBar?.searchBarUM?.query == newQuery) return + val searchBarUM = countryListUMController.state.value.searchBarUM + if (searchBarUM.query == newQuery) return modelScope.launch { countryListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery)) @@ -89,4 +103,21 @@ internal class OnrampSelectCountryModel @Inject constructor( ), ) } + + private fun createSearchBarUM(): SearchBarUM { + return SearchBarUM( + placeholderText = resourceReference(R.string.onramp_country_search), + query = "", + onQueryChange = ::onSearchQueryChange, + isActive = false, + onActiveChange = ::onSearchBarActiveChange, + ) + } + + private companion object { + private const val LOADING_ITEMS_COUNT = 5 + val loadingItems: ImmutableList = MutableList(LOADING_ITEMS_COUNT) { + CountryItemState.Loading("Loading #$it") + }.toImmutableList() + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/CountryListItem.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/CountryListItem.kt deleted file mode 100644 index 7bc00833a0..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/CountryListItem.kt +++ /dev/null @@ -1,136 +0,0 @@ -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 - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt index 89c664ce1e..6fbf4c8d16 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt @@ -1,13 +1,32 @@ package com.tangem.features.onramp.selectcountry.ui -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items +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.draw.alpha +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import coil.compose.AsyncImage +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.TextShimmer 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.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.selectcountry.entity.CountryItemState import com.tangem.features.onramp.selectcountry.entity.CountryListUM @Composable @@ -20,19 +39,156 @@ internal fun SelectCountryBottomSheet(config: TangemBottomSheetConfig, 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, + LazyColumn( + modifier = modifier, + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), + ) { + item(key = "search_bar") { + SearchBar( + state = state.searchBarUM, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing20), + ) + } + when (state) { + is CountryListUM.Content -> countryListWithContent(state = state) + is CountryListUM.Error -> countryListError(state = state) + is CountryListUM.Loading -> countryListLoading(state = state) + } + } +} + +private fun LazyListScope.countryListError(state: CountryListUM.Error) { + item(key = "error_content") { + Column( + modifier = Modifier + .fillParentMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(id = R.string.markets_loading_error_title), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + SecondarySmallButton( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + config = SmallButtonConfig( + text = resourceReference(id = R.string.try_to_load_data_again_button_title), + onClick = state.onRetry, + ), + ) + } + } +} + +private fun LazyListScope.countryListLoading(state: CountryListUM.Loading) { + items( + items = state.items, + key = { item -> item.id }, + contentType = { item -> item::class.java }, + itemContent = { _ -> LoadingCountryItem(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16)) }, + ) +} + +private fun LazyListScope.countryListWithContent(state: CountryListUM.Content) { + items( + items = state.items, + key = { item -> item.id }, + contentType = { item -> item::class.java }, + itemContent = { item -> + when (item) { + is CountryItemState.WithContent.Content -> ContentCountryItem( modifier = Modifier - .animateItem() - .fillMaxWidth(), + .fillMaxWidth() + .clickable(onClick = item.onClick) + .padding(vertical = TangemTheme.dimens.spacing16), + state = item, ) - }, + is CountryItemState.WithContent.Unavailable -> UnavailableCountryItem( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens.spacing16), + state = item, + ) + } + }, + ) +} + +@Composable +private fun LoadingCountryItem(modifier: Modifier = Modifier) { + Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12)) { + CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size36)) + TextShimmer( + modifier = Modifier + .align(Alignment.CenterVertically) + .width(width = TangemTheme.dimens.size70), + style = TangemTheme.typography.subtitle2, + radius = TangemTheme.dimens.radius4, + ) + } +} + +@Composable +private fun ContentCountryItem(state: CountryItemState.WithContent.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 +@Suppress("MagicNumber") +private fun UnavailableCountryItem(state: CountryItemState.WithContent.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, ) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrencyListController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrencyListController.kt index 157e5a9133..eaad4b3365 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrencyListController.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrencyListController.kt @@ -1,21 +1,27 @@ package com.tangem.features.onramp.selectcurrency.entity import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsTransformer import com.tangem.features.onramp.utils.SearchBarUMTransformer +import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList -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 -internal class CurrencyListController(private val currencySearchBarUM: SearchBarUM) { +internal class CurrencyListController( + currencySearchBarUM: SearchBarUM, + loadingSections: ImmutableList>, +) { val state: StateFlow get() = _state.asStateFlow() - private val _state: MutableStateFlow = MutableStateFlow(value = getInitialState()) + private val _state: MutableStateFlow = MutableStateFlow( + value = CurrenciesListUM.Loading( + searchBarUM = currencySearchBarUM, + sections = loadingSections, + ), + ) - fun update(transformer: UpdateCurrencyItemsTransformer) { + fun update(transformer: Transformer) { _state.update(transformer::transform) } @@ -25,17 +31,4 @@ internal class CurrencyListController(private val currencySearchBarUM: SearchBar prevState.copySealed(searchBarUM) } } - - private fun getInitialState(): CurrenciesListUM { - return CurrenciesListUM.Loading( - searchBarUM = currencySearchBarUM, - sections = listOf( - CurrenciesSection(stringReference("Popular fiats"), items = createLoadingItems("popular")), - CurrenciesSection(stringReference("Other currencies"), items = createLoadingItems("other")), - ).toImmutableList(), - ) - } - - private fun createLoadingItems(prefix: String, size: Int = 5): ImmutableList = - MutableList(size) { index -> CurrencyItemState.Loading("$prefix#$index") }.toImmutableList() } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsLoadingTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsLoadingTransformer.kt new file mode 100644 index 0000000000..50eddcba44 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsLoadingTransformer.kt @@ -0,0 +1,16 @@ +package com.tangem.features.onramp.selectcurrency.entity.transformer + +import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM +import com.tangem.features.onramp.selectcurrency.entity.CurrenciesSection +import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList + +internal class UpdateCurrencyItemsLoadingTransformer( + private val loadingSections: ImmutableList>, +) : Transformer { + + override fun transform(prevState: CurrenciesListUM): CurrenciesListUM { + return CurrenciesListUM.Loading(searchBarUM = prevState.searchBarUM, sections = loadingSections) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt index b6668e3f40..35b364808d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt @@ -5,18 +5,24 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.onramp.GetOnrampCurrenciesUseCase import com.tangem.domain.onramp.OnrampSaveDefaultCurrencyUseCase import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM +import com.tangem.features.onramp.selectcurrency.entity.CurrenciesSection +import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState import com.tangem.features.onramp.selectcurrency.entity.CurrencyListController +import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsLoadingTransformer import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsTransformer 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.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -34,7 +40,10 @@ internal class OnrampSelectCurrencyModel @Inject constructor( val state: StateFlow get() = controller.state private val params: SelectCurrencyComponent.Params = paramsContainer.require() - private val controller = CurrencyListController(currencySearchBarUM = createSearchBarUM()) + private val controller = CurrencyListController( + currencySearchBarUM = createSearchBarUM(), + loadingSections = loadingSections, + ) private val refreshTrigger = MutableSharedFlow() init { @@ -70,9 +79,8 @@ internal class OnrampSelectCurrencyModel @Inject constructor( } private fun onRetry() { - modelScope.launch { - refreshTrigger.emit(Unit) - } + modelScope.launch { refreshTrigger.emit(Unit) } + controller.update(UpdateCurrencyItemsLoadingTransformer(loadingSections)) } private fun onSearchQueryChange(newQuery: String) { @@ -104,4 +112,14 @@ internal class OnrampSelectCurrencyModel @Inject constructor( onActiveChange = ::onSearchBarActiveChange, ) } + + private companion object { + val loadingSections = listOf( + CurrenciesSection(stringReference("Popular fiats"), items = createLoadingItems("popular")), + CurrenciesSection(stringReference("Other currencies"), items = createLoadingItems("other")), + ).toImmutableList() + + private fun createLoadingItems(prefix: String, size: Int = 5): ImmutableList = + MutableList(size) { index -> CurrencyItemState.Loading("$prefix#$index") }.toImmutableList() + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt index 88dfc1d6d9..8177c32631 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource import androidx.compose.ui.util.fastForEach import coil.compose.AsyncImage import com.tangem.core.ui.components.CircleShimmer @@ -18,9 +19,13 @@ import com.tangem.core.ui.components.TextShimmer 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.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState @@ -46,12 +51,37 @@ internal fun OnrampCurrencyList(state: CurrenciesListUM, modifier: Modifier = Mo } when (state) { is CurrenciesListUM.Content -> currencyListContent(state = state) - is CurrenciesListUM.Error -> TODO() + is CurrenciesListUM.Error -> currencyListError(state = state) is CurrenciesListUM.Loading -> currencyListLoading(state = state) } } } +private fun LazyListScope.currencyListError(state: CurrenciesListUM.Error) { + item(key = "error_content") { + Column( + modifier = Modifier + .fillParentMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(id = R.string.markets_loading_error_title), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + SecondarySmallButton( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + config = SmallButtonConfig( + text = resourceReference(id = R.string.try_to_load_data_again_button_title), + onClick = state.onRetry, + ), + ) + } + } +} + private fun LazyListScope.currencyListLoading(state: CurrenciesListUM.Loading) { state.sections.fastForEach { section -> item(key = section.title.toString()) { GroupTitle(title = section.title.resolveReference()) }