Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-11 13:33:40 +05:00
parent a950ca16b7
commit 3b4d1ad6f6
17 changed files with 367 additions and 324 deletions

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package com.tangem.features.onramp.selectcountry.entity
package com.tangem.features.onramp.entity
import kotlinx.serialization.Serializable

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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