Updated on 2026-08-14
This commit is contained in:
parent
7228c72553
commit
5b60d94e60
29 changed files with 1034 additions and 110 deletions
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.managetokens.component
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface OnboardingManageTokensComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnboardingManageTokensComponent>
|
||||
}
|
||||
|
|
@ -23,8 +23,9 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
|
@ -49,4 +50,5 @@ dependencies {
|
|||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.reKotlin) // need for legacy onboarding
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.managetokens.component.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.model.OnboardingManageTokensModel
|
||||
import com.tangem.features.managetokens.ui.OnboardingManageTokensContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultOnboardingManageTokensComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted params: OnboardingManageTokensComponent.Params,
|
||||
) : OnboardingManageTokensComponent, AppComponentContext by context {
|
||||
|
||||
private val model: OnboardingManageTokensModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
OnboardingManageTokensContent(modifier = modifier, state = state)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : OnboardingManageTokensComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: OnboardingManageTokensComponent.Params,
|
||||
): DefaultOnboardingManageTokensComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class PreviewManageTokensComponent(
|
||||
private val isLoading: Boolean = false,
|
||||
private val isLoading: Boolean,
|
||||
params: ManageTokensComponent.Params,
|
||||
) : ManageTokensComponent {
|
||||
|
||||
private val changedItemsIds: MutableSet<String> = mutableSetOf()
|
||||
|
|
@ -36,14 +37,21 @@ internal class PreviewManageTokensComponent(
|
|||
value = ManageTokensUM.ManageContent(
|
||||
popBack = {},
|
||||
items = items,
|
||||
topBar = ManageTokensTopBarUM.ManageContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = {},
|
||||
endButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
onIconClicked = {},
|
||||
),
|
||||
),
|
||||
topBar = if (params.userWalletId != null) {
|
||||
ManageTokensTopBarUM.ManageContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = {},
|
||||
endButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
onIconClicked = {},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ManageTokensTopBarUM.ReadContent(
|
||||
title = resourceReference(R.string.common_search_tokens),
|
||||
onBackButtonClick = {},
|
||||
)
|
||||
},
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
package com.tangem.features.managetokens.component.preview
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
|
||||
import com.tangem.features.managetokens.entity.managetokens.OnboardingManageTokensUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.ui.OnboardingManageTokensContent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class PreviewOnboardingManageTokensComponent(
|
||||
private val isLoading: Boolean = false,
|
||||
) : OnboardingManageTokensComponent {
|
||||
|
||||
private val state = OnboardingManageTokensUM(
|
||||
items = initItems(),
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = true,
|
||||
loadMore = { false },
|
||||
onBack = {},
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Later(
|
||||
onClick = {},
|
||||
showProgress = false,
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
OnboardingManageTokensContent(state = state, modifier = modifier)
|
||||
}
|
||||
|
||||
private fun initItems() = List(size = 30) { index ->
|
||||
if (isLoading) {
|
||||
CurrencyItemUM.Loading(index)
|
||||
} else {
|
||||
if (index < 2) {
|
||||
getCustomItem(index)
|
||||
} else {
|
||||
getBasicItem(index)
|
||||
}
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
private fun getCustomItem(index: Int) = CurrencyItemUM.Custom(
|
||||
id = ManagedCryptoCurrency.ID(index.toString()),
|
||||
name = "Custom token $index",
|
||||
symbol = "CT$index",
|
||||
icon = CurrencyIconState.CustomTokenIcon(
|
||||
tint = Color.White,
|
||||
background = Color.Black,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = true,
|
||||
),
|
||||
onRemoveClick = {},
|
||||
)
|
||||
|
||||
private fun getBasicItem(index: Int) = CurrencyItemUM.Basic(
|
||||
id = ManagedCryptoCurrency.ID(index.toString()),
|
||||
name = "Currency $index",
|
||||
symbol = "C$index",
|
||||
icon = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.img_btc_22,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = false,
|
||||
),
|
||||
networks = if (index == 2) {
|
||||
CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks())
|
||||
} else {
|
||||
CurrencyItemUM.Basic.NetworksUM.Collapsed
|
||||
},
|
||||
onExpandClick = {},
|
||||
)
|
||||
|
||||
private fun getCurrencyNetworks() = List(size = 3) { networkIndex ->
|
||||
CurrencyNetworkUM(
|
||||
network = Network(
|
||||
id = Network.ID(networkIndex.toString()),
|
||||
backendId = networkIndex.toString(),
|
||||
name = "Network $networkIndex",
|
||||
currencySymbol = "N$networkIndex",
|
||||
derivationPath = Network.DerivationPath.Card(""),
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
hasFiatFeeRate = false,
|
||||
canHandleTokens = false,
|
||||
),
|
||||
name = "NETWORK$networkIndex",
|
||||
type = "N$networkIndex",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = networkIndex == 0,
|
||||
isSelected = false,
|
||||
onSelectedStateChange = {},
|
||||
onLongClick = {},
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
|
@ -16,6 +16,12 @@ internal interface ComponentModule {
|
|||
@Singleton
|
||||
fun bindManageTokensComponentFactory(factory: DefaultManageTokensComponent.Factory): ManageTokensComponent.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindOnboardingManageTokensComponentFactory(
|
||||
factory: DefaultOnboardingManageTokensComponent.Factory,
|
||||
): OnboardingManageTokensComponent.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindAddCustomTokenComponentFactory(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.features.managetokens.model.CustomTokenFormModel
|
||||
import com.tangem.features.managetokens.model.CustomTokenSelectorModel
|
||||
import com.tangem.features.managetokens.model.ManageTokensModel
|
||||
import com.tangem.features.managetokens.model.OnboardingManageTokensModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -20,6 +21,11 @@ internal interface ModelModule {
|
|||
@ClassKey(ManageTokensModel::class)
|
||||
fun provideManageTokensModel(model: ManageTokensModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnboardingManageTokensModel::class)
|
||||
fun provideOnboardingManageTokensModel(model: OnboardingManageTokensModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(CustomTokenFormModel::class)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ internal sealed class ManageTokensUM {
|
|||
abstract val isInitialBatchLoading: Boolean
|
||||
abstract val isNextBatchLoading: Boolean
|
||||
abstract val items: ImmutableList<CurrencyItemUM>
|
||||
abstract val topBar: ManageTokensTopBarUM
|
||||
abstract val topBar: ManageTokensTopBarUM?
|
||||
abstract val search: SearchBarUM
|
||||
abstract val loadMore: () -> Boolean
|
||||
abstract val scrollToTop: StateEvent<Unit>
|
||||
|
|
@ -24,7 +24,7 @@ internal sealed class ManageTokensUM {
|
|||
override val isInitialBatchLoading: Boolean,
|
||||
override val isNextBatchLoading: Boolean,
|
||||
override val items: ImmutableList<CurrencyItemUM>,
|
||||
override val topBar: ManageTokensTopBarUM,
|
||||
override val topBar: ManageTokensTopBarUM?,
|
||||
override val search: SearchBarUM,
|
||||
override val loadMore: () -> Boolean,
|
||||
override val scrollToTop: StateEvent<Unit> = consumedEvent(),
|
||||
|
|
@ -35,7 +35,7 @@ internal sealed class ManageTokensUM {
|
|||
override val isInitialBatchLoading: Boolean,
|
||||
override val isNextBatchLoading: Boolean,
|
||||
override val items: ImmutableList<CurrencyItemUM>,
|
||||
override val topBar: ManageTokensTopBarUM,
|
||||
override val topBar: ManageTokensTopBarUM?,
|
||||
override val search: SearchBarUM,
|
||||
override val loadMore: () -> Boolean,
|
||||
override val scrollToTop: StateEvent<Unit> = consumedEvent(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.managetokens.entity.managetokens
|
||||
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class OnboardingManageTokensUM(
|
||||
val onBack: () -> Unit,
|
||||
val isInitialBatchLoading: Boolean,
|
||||
val isNextBatchLoading: Boolean,
|
||||
val items: ImmutableList<CurrencyItemUM>,
|
||||
val loadMore: () -> Boolean,
|
||||
val search: SearchBarUM,
|
||||
val scrollToTop: StateEvent<Unit> = consumedEvent(),
|
||||
val actionButtonConfig: ActionButtonConfig,
|
||||
) {
|
||||
sealed class ActionButtonConfig {
|
||||
abstract val onClick: () -> Unit
|
||||
abstract val showProgress: Boolean
|
||||
|
||||
data class Continue(
|
||||
override val onClick: () -> Unit,
|
||||
override val showProgress: Boolean = false,
|
||||
val showTangemIcon: Boolean,
|
||||
) : ActionButtonConfig()
|
||||
|
||||
data class Later(
|
||||
override val onClick: () -> Unit,
|
||||
override val showProgress: Boolean = false,
|
||||
) : ActionButtonConfig()
|
||||
|
||||
fun copySealed(showProgress: Boolean): ActionButtonConfig {
|
||||
return when (this) {
|
||||
is Continue -> copy(showProgress = showProgress)
|
||||
is Later -> copy(showProgress = showProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
observeSearchQueryChanges()
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.launchPagination(params)
|
||||
manageTokensListManager.launchPagination(source = params.source, userWalletId = params.userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,12 +159,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
.sample(periodMillis = 1_000)
|
||||
.onEach { query ->
|
||||
manageTokensListManager.search(
|
||||
userWalletId = params.userWalletId,
|
||||
query = query,
|
||||
)
|
||||
}
|
||||
.onEach { query -> manageTokensListManager.search(userWalletId = params.userWalletId, query = query) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
|
|
@ -272,10 +267,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
if (state.isInitialBatchLoading || state.isNextBatchLoading) return false
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.loadMore(
|
||||
userWalletId = params.userWalletId,
|
||||
query = state.search.query,
|
||||
)
|
||||
manageTokensListManager.loadMore(userWalletId = params.userWalletId, query = state.search.query)
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,297 @@
|
|||
package com.tangem.features.managetokens.model
|
||||
|
||||
import arrow.core.flatten
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.card.HasMissedDerivationsUseCase
|
||||
import com.tangem.domain.managetokens.SaveManagedTokensUseCase
|
||||
import com.tangem.domain.redux.OnboardingManageTokensAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.managetokens.OnboardingManageTokensUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.utils.list.ChangedCurrencies
|
||||
import com.tangem.features.managetokens.utils.list.ManageTokensListManager
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ComponentScoped
|
||||
internal class OnboardingManageTokensModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val manageTokensListManager: ManageTokensListManager,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
|
||||
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params: OnboardingManageTokensComponent.Params = paramsContainer.require()
|
||||
val state: MutableStateFlow<OnboardingManageTokensUM> = MutableStateFlow(getInitialState())
|
||||
|
||||
init {
|
||||
manageTokensListManager.uiItems
|
||||
.onEach { items -> updateItems(items) }
|
||||
.launchIn(modelScope)
|
||||
|
||||
manageTokensListManager.paginationStatus
|
||||
.onEach { status -> updatePaginationStatus(status) }
|
||||
.launchIn(modelScope)
|
||||
|
||||
manageTokensListManager.currenciesToAdd
|
||||
.onEach(::handleNewAddedCurrencies)
|
||||
.launchIn(modelScope)
|
||||
|
||||
observeSearchQueryChanges()
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.launchPagination(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = params.userWalletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState(): OnboardingManageTokensUM {
|
||||
analyticsEventHandler.send(ManageTokensAnalyticEvent.ScreenOpened(source = ManageTokensSource.ONBOARDING))
|
||||
|
||||
return OnboardingManageTokensUM(
|
||||
isInitialBatchLoading = true,
|
||||
isNextBatchLoading = false,
|
||||
items = getLoadingItems(),
|
||||
loadMore = ::loadMoreItems,
|
||||
onBack = {},
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = ::searchCurrencies,
|
||||
isActive = false,
|
||||
onActiveChange = ::toggleSearchBar,
|
||||
),
|
||||
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Later(
|
||||
onClick = ::onLaterClick,
|
||||
showProgress = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun observeSearchQueryChanges() {
|
||||
state
|
||||
.distinctUntilChanged { old, new ->
|
||||
// It's also used to skip search activation to avoid searching an empty query
|
||||
old.search.query == new.search.query &&
|
||||
(old.search.isActive == new.search.isActive || new.search.isActive)
|
||||
}
|
||||
.transform { state ->
|
||||
val query = state.search.query
|
||||
|
||||
if (state.search.isActive) {
|
||||
emit(query)
|
||||
}
|
||||
}
|
||||
.sample(periodMillis = 1_000)
|
||||
.onEach { query -> manageTokensListManager.search(userWalletId = params.userWalletId, query = query) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun updateItems(items: ImmutableList<CurrencyItemUM>) {
|
||||
val updatedState = state.updateAndGet { state -> state.copy(items = items) }
|
||||
|
||||
if (updatedState.items.isEmpty() && updatedState.search.isActive) {
|
||||
val event = ManageTokensAnalyticEvent.TokensIsNotFound(
|
||||
query = updatedState.search.query,
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePaginationStatus(status: PaginationStatus<*>) {
|
||||
state.update { state ->
|
||||
when (status) {
|
||||
is PaginationStatus.None,
|
||||
is PaginationStatus.InitialLoading,
|
||||
-> {
|
||||
if (state.search.isActive) {
|
||||
state.copy(items = getLoadingItems())
|
||||
} else {
|
||||
state.copy(items = getLoadingItems(), isInitialBatchLoading = true)
|
||||
}
|
||||
}
|
||||
is PaginationStatus.NextBatchLoading -> state.copy(isNextBatchLoading = true)
|
||||
is PaginationStatus.InitialLoadingError -> {
|
||||
val message = SnackbarMessage(
|
||||
message = status.throwable.localizedMessage
|
||||
?.let(::stringReference)
|
||||
?: resourceReference(R.string.common_error),
|
||||
)
|
||||
messageSender.send(message)
|
||||
|
||||
state.copy(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
)
|
||||
}
|
||||
is PaginationStatus.Paginating -> {
|
||||
(status.lastResult as? BatchFetchResult.Error)?.let { fetchError ->
|
||||
Timber.e(fetchError.throwable)
|
||||
}
|
||||
|
||||
state.copy(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
scrollToTop = if (state.isInitialBatchLoading && state.items.isNotEmpty()) {
|
||||
triggeredEvent(
|
||||
data = Unit,
|
||||
onConsume = ::consumeScrollToTopEvent,
|
||||
)
|
||||
} else {
|
||||
state.scrollToTop
|
||||
},
|
||||
)
|
||||
}
|
||||
is PaginationStatus.EndOfPagination -> state.copy(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLoadingItems(): ImmutableList<CurrencyItemUM> {
|
||||
return List(size = 10) { index ->
|
||||
CurrencyItemUM.Loading(index)
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
private fun consumeScrollToTopEvent() {
|
||||
state.update { state -> state.copy(scrollToTop = consumedEvent()) }
|
||||
}
|
||||
|
||||
private suspend fun handleNewAddedCurrencies(currenciesToAdd: ChangedCurrencies) {
|
||||
if (currenciesToAdd.isEmpty()) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Later(onClick = ::onLaterClick),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val hasMissedDerivations = hasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = params.userWalletId,
|
||||
networksWithDerivationPath = currenciesToAdd.values
|
||||
.flatten()
|
||||
.toSet()
|
||||
.associate { it.backendId to null },
|
||||
)
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Continue(
|
||||
onClick = ::saveChanges,
|
||||
showTangemIcon = hasMissedDerivations,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMoreItems(): Boolean {
|
||||
val state = state.value
|
||||
if (state.isInitialBatchLoading || state.isNextBatchLoading) return false
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.loadMore(userWalletId = params.userWalletId, query = state.search.query)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun saveChanges() = resource(
|
||||
acquire = {
|
||||
state.update { state ->
|
||||
state.copy(actionButtonConfig = state.actionButtonConfig.copySealed(showProgress = true))
|
||||
}
|
||||
},
|
||||
release = {
|
||||
state.update { state ->
|
||||
state.copy(actionButtonConfig = state.actionButtonConfig.copySealed(showProgress = false))
|
||||
}
|
||||
},
|
||||
) {
|
||||
val event = ManageTokensAnalyticEvent.TokenAdded(
|
||||
tokensCount = manageTokensListManager.currenciesToAdd.value.values.sumOf { it.size },
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
|
||||
saveManagedTokensUseCase(
|
||||
userWalletId = requireNotNull(params.userWalletId),
|
||||
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
|
||||
currenciesToRemove = manageTokensListManager.currenciesToRemove.value,
|
||||
).getOrElse {
|
||||
Timber.e(it, "Failed to save changes")
|
||||
return@resource
|
||||
}
|
||||
|
||||
reduxStateHolder.dispatch(OnboardingManageTokensAction.CurrenciesSaved)
|
||||
}
|
||||
|
||||
private fun onLaterClick() = resource(
|
||||
acquire = {
|
||||
state.update { state ->
|
||||
state.copy(actionButtonConfig = state.actionButtonConfig.copySealed(showProgress = true))
|
||||
}
|
||||
},
|
||||
release = {
|
||||
state.update { state ->
|
||||
state.copy(actionButtonConfig = state.actionButtonConfig.copySealed(showProgress = false))
|
||||
}
|
||||
},
|
||||
) {
|
||||
reduxStateHolder.dispatch(OnboardingManageTokensAction.CurrenciesSaved)
|
||||
}
|
||||
|
||||
private fun searchCurrencies(query: String) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
search = state.search.copy(
|
||||
query = query,
|
||||
isActive = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSearchBar(isActive: Boolean) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
search = state.search.copy(
|
||||
isActive = isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +59,9 @@ import com.tangem.core.ui.res.LocalSnackbarHostState
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
|
||||
|
|
@ -128,19 +130,21 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, search: SearchBarUM, modifier: Modifier = Modifier) {
|
||||
private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM?, search: SearchBarUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.background(TangemTheme.colors.background.primary),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
TangemTopAppBar(
|
||||
title = topBar.title.resolveReference(),
|
||||
startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick),
|
||||
endButton = when (topBar) {
|
||||
is ManageTokensTopBarUM.ManageContent -> topBar.endButton
|
||||
is ManageTokensTopBarUM.ReadContent -> null
|
||||
},
|
||||
)
|
||||
if (topBar != null) {
|
||||
TangemTopAppBar(
|
||||
title = topBar.title.resolveReference(),
|
||||
startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick),
|
||||
endButton = when (topBar) {
|
||||
is ManageTokensTopBarUM.ManageContent -> topBar.endButton
|
||||
is ManageTokensTopBarUM.ReadContent -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing12)
|
||||
|
|
@ -196,7 +200,7 @@ private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Currencies(
|
||||
internal fun Currencies(
|
||||
listState: LazyListState,
|
||||
items: ImmutableList<CurrencyItemUM>,
|
||||
showLoadingItem: Boolean,
|
||||
|
|
@ -466,8 +470,24 @@ private fun Preview_ManageTokens(
|
|||
private class PreviewManageTokensComponentProvider : PreviewParameterProvider<ManageTokensComponent> {
|
||||
override val values: Sequence<ManageTokensComponent>
|
||||
get() = sequenceOf(
|
||||
PreviewManageTokensComponent(),
|
||||
PreviewManageTokensComponent(isLoading = true),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = true,
|
||||
params = ManageTokensComponent.Params(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = UserWalletId("wallet_id"),
|
||||
),
|
||||
),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = false,
|
||||
params = ManageTokensComponent.Params(source = ManageTokensSource.ONBOARDING, userWalletId = null),
|
||||
),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = false,
|
||||
params = ManageTokensComponent.Params(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = UserWalletId("wallet_id"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
package com.tangem.features.managetokens.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.res.LocalSnackbarHostState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.preview.PreviewOnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.managetokens.OnboardingManageTokensUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun OnboardingManageTokensContent(state: OnboardingManageTokensUM, modifier: Modifier = Modifier) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
keyboardController?.hide()
|
||||
|
||||
return super.onPreScroll(available, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.nestedScroll(nestedScrollConnection),
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
topBar = {
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing12, horizontal = TangemTheme.dimens.spacing16),
|
||||
state = state.search,
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
Content(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
state = state,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
TangemSnackbarHost(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
hostState = LocalSnackbarHostState.current,
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
config = state.actionButtonConfig,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: OnboardingManageTokensUM, modifier: Modifier = Modifier) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Currencies(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
listState = listState,
|
||||
items = state.items,
|
||||
showLoadingItem = state.isNextBatchLoading,
|
||||
onLoadMore = state.loadMore,
|
||||
isEditable = true,
|
||||
)
|
||||
|
||||
BottomFade(modifier = Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
|
||||
EventEffect(event = state.scrollToTop) {
|
||||
listState.animateScrollToItem(index = 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingActionButton(config: OnboardingManageTokensUM.ActionButtonConfig, modifier: Modifier = Modifier) {
|
||||
when (config) {
|
||||
is OnboardingManageTokensUM.ActionButtonConfig.Continue -> ContinueButton(config = config, modifier = modifier)
|
||||
is OnboardingManageTokensUM.ActionButtonConfig.Later -> SecondaryButton(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.common_later),
|
||||
showProgress = config.showProgress,
|
||||
onClick = config.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContinueButton(
|
||||
config: OnboardingManageTokensUM.ActionButtonConfig.Continue,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (config.showTangemIcon) {
|
||||
PrimaryButtonIconEnd(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.common_continue),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
showProgress = config.showProgress,
|
||||
onClick = config.onClick,
|
||||
)
|
||||
} else {
|
||||
PrimaryButton(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.common_continue),
|
||||
showProgress = config.showProgress,
|
||||
onClick = config.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_ManageTokens(
|
||||
@PreviewParameter(PreviewOnboardingManageTokensComponentProvider::class) component: OnboardingManageTokensComponent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
component.Content(Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
|
||||
private class PreviewOnboardingManageTokensComponentProvider :
|
||||
CollectionPreviewParameterProvider<PreviewOnboardingManageTokensComponent>(
|
||||
collection = listOf(
|
||||
PreviewOnboardingManageTokensComponent(isLoading = false),
|
||||
PreviewOnboardingManageTokensComponent(isLoading = true),
|
||||
),
|
||||
)
|
||||
// endregion
|
||||
|
|
@ -16,7 +16,6 @@ import com.tangem.domain.managetokens.model.*
|
|||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
|
@ -81,9 +80,9 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
.distinctUntilChanged()
|
||||
val uiItems: Flow<ImmutableList<CurrencyItemUM>> = uiManager.items
|
||||
|
||||
suspend fun launchPagination(params: ManageTokensComponent.Params) = coroutineScope {
|
||||
suspend fun launchPagination(source: ManageTokensSource, userWalletId: UserWalletId?) = coroutineScope {
|
||||
scope = this
|
||||
source = params.source
|
||||
this@ManageTokensListManager.source = source
|
||||
|
||||
val batchFlow = getManagedTokensUseCase(
|
||||
context = ManageTokensListBatchingContext(
|
||||
|
|
@ -93,13 +92,13 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
)
|
||||
|
||||
batchFlow.state
|
||||
.onEach { state -> updateState(state, params.userWalletId) }
|
||||
.onEach { state -> updateState(state, userWalletId) }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(scope = this)
|
||||
.saveIn(jobHolder)
|
||||
|
||||
// Initial load
|
||||
reload(params.userWalletId)
|
||||
reload(userWalletId)
|
||||
}
|
||||
|
||||
suspend fun reload(userWalletId: UserWalletId?) {
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
hasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = selectedWalletId,
|
||||
networksWithDerivationPath = addToPortfolioData.addedNetworks[selectedWalletId].orEmpty()
|
||||
.associate { Network.ID(it.networkId) to null },
|
||||
.associate { it.networkId to null },
|
||||
)
|
||||
} else {
|
||||
false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue