Updated on 2026-08-14
This commit is contained in:
parent
a950ca16b7
commit
3b4d1ad6f6
17 changed files with 367 additions and 324 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.selectcountry.entity
|
||||
package com.tangem.features.onramp.entity
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CountriesListItemUM>) {
|
||||
@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<CountriesListItemUM> {
|
||||
if (getSearchBar() == null) return items
|
||||
data class Loading(
|
||||
override val searchBarUM: SearchBarUM,
|
||||
val items: ImmutableList<CountryItemState.Loading>,
|
||||
) : 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<CountryItemState.WithContent>,
|
||||
) : 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CountryItemState.Loading>,
|
||||
) {
|
||||
|
||||
val state: StateFlow<CountryListUM> get() = _state.asStateFlow()
|
||||
private val _state: MutableStateFlow<CountryListUM> = MutableStateFlow(
|
||||
value = CountryListUM(
|
||||
items = getLoadingItems().map(CountriesListItemUM::Country).toImmutableList(),
|
||||
),
|
||||
)
|
||||
private val _state: MutableStateFlow<CountryListUM> =
|
||||
MutableStateFlow(value = CountryListUM.Loading(searchBarUM = searchBarUM, items = loadingItems))
|
||||
|
||||
fun update(transformer: UpdateCountryItemsTransformer) {
|
||||
fun update(transformer: Transformer<CountryListUM>) {
|
||||
_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<CountryItemState> =
|
||||
MutableList(LOADING_ITEMS_COUNT) { CountryItemState.Loading("Loading #$it") }
|
||||
}
|
||||
|
|
@ -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<CountryItemState.Loading>,
|
||||
) : Transformer<CountryListUM> {
|
||||
override fun transform(prevState: CountryListUM): CountryListUM {
|
||||
return CountryListUM.Loading(searchBarUM = prevState.searchBarUM, items = loadingItems)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Throwable, List<OnrampCountry>>,
|
||||
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<CountryListUM> {
|
||||
|
||||
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<OnrampCountry>.toUiModels(): List<CountriesListItemUM.Country> {
|
||||
private fun List<OnrampCountry>.toUiModels(): List<CountryItemState.WithContent> {
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<CountryItemState>> = flow {
|
||||
delay(3_000) // simulate network call
|
||||
emit(getMockedCountries())
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CountryListUM> = countryListUMController.state
|
||||
val state: StateFlow<CountryListUM> get() = countryListUMController.state
|
||||
private val params: SelectCountryComponent.Params = paramsContainer.require()
|
||||
private val countryListUMController = CountryListUMController(
|
||||
searchBarUM = createSearchBarUM(),
|
||||
loadingItems = loadingItems,
|
||||
)
|
||||
private val refreshTrigger = MutableSharedFlow<Unit>()
|
||||
|
||||
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<CountryItemState.Loading> = MutableList(LOADING_ITEMS_COUNT) {
|
||||
CountryItemState.Loading("Loading #$it")
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CurrenciesSection<CurrencyItemState.Loading>>,
|
||||
) {
|
||||
val state: StateFlow<CurrenciesListUM> get() = _state.asStateFlow()
|
||||
private val _state: MutableStateFlow<CurrenciesListUM> = MutableStateFlow(value = getInitialState())
|
||||
private val _state: MutableStateFlow<CurrenciesListUM> = MutableStateFlow(
|
||||
value = CurrenciesListUM.Loading(
|
||||
searchBarUM = currencySearchBarUM,
|
||||
sections = loadingSections,
|
||||
),
|
||||
)
|
||||
|
||||
fun update(transformer: UpdateCurrencyItemsTransformer) {
|
||||
fun update(transformer: Transformer<CurrenciesListUM>) {
|
||||
_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<CurrencyItemState.Loading> =
|
||||
MutableList(size) { index -> CurrencyItemState.Loading("$prefix#$index") }.toImmutableList()
|
||||
}
|
||||
|
|
@ -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<CurrenciesSection<CurrencyItemState.Loading>>,
|
||||
) : Transformer<CurrenciesListUM> {
|
||||
|
||||
override fun transform(prevState: CurrenciesListUM): CurrenciesListUM {
|
||||
return CurrenciesListUM.Loading(searchBarUM = prevState.searchBarUM, sections = loadingSections)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CurrenciesListUM> 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<Unit>()
|
||||
|
||||
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<CurrencyItemState.Loading> =
|
||||
MutableList(size) { index -> CurrencyItemState.Loading("$prefix#$index") }.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()) }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue