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