Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-07 12:59:41 +05:00
parent 0f44f21d6f
commit 4f237f898b
11 changed files with 218 additions and 52 deletions

View file

@ -98,7 +98,7 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
viewModel.onSearchClick()
} else {
Analytics.send(IntroductionProcess.ButtonTokensList())
store.dispatchNavigationAction { push(AppRoute.ManageTokens) }
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) }
store.dispatch(TokensAction.SetArgs.ReadAccess)
}
},

View file

@ -61,7 +61,7 @@ internal class HomeViewModel @Inject constructor(
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
store.dispatch(TokensAction.SetArgs.ReadAccess)
store.dispatchNavigationAction { push(AppRoute.ManageTokens) }
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) }
}
private fun scanCard() {

View file

@ -126,7 +126,13 @@ internal class ChildFactory @Inject constructor(
if (manageTokensToggles.isFeatureEnabled) {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ManageTokensComponent.Params(),
params = ManageTokensComponent.Params(
mode = if (route.readOnlyContent) {
ManageTokensComponent.Mode.READ_ONLY
} else {
ManageTokensComponent.Mode.MANAGE
},
),
componentFactory = manageTokensComponentFactory,
)
} else {

View file

@ -189,7 +189,11 @@ sealed class AppRoute(val path: String) : Route {
}
@Serializable
data object ManageTokens : AppRoute(path = "/manage_tokens")
data class ManageTokens(
val readOnlyContent: Boolean,
) : AppRoute(path = "/manage_tokens/$readOnlyContent"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
@Serializable
data object AddCustomToken : AppRoute(path = "/add_custom_token")

View file

@ -5,7 +5,8 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
interface ManageTokensComponent : ComposableContentComponent {
class Params
data class Params(val mode: Mode)
enum class Mode { READ_ONLY, MANAGE, }
interface Factory : ComponentFactory<Params, ManageTokensComponent>
}

View file

@ -6,15 +6,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.entity.CurrencyItemUM
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
import com.tangem.features.managetokens.entity.ManageTokensUM
import com.tangem.features.managetokens.entity.*
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.ManageTokensScreen
import kotlinx.collections.immutable.mutate
@ -28,11 +27,18 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
private val changedItemsIds: MutableSet<String> = mutableSetOf()
private var items = initItems()
private val previewState = MutableStateFlow(
value = ManageTokensUM(
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 = {},
),
),
search = SearchBarUM(
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
query = "",
@ -41,8 +47,8 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
onActiveChange = ::toggleSearchBar,
),
hasChanges = false,
isLoading = false,
onSaveClick = {},
onAddCustomToken = {},
),
)

View file

@ -0,0 +1,23 @@
package com.tangem.features.managetokens.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed class ManageTokensTopBarUM {
abstract val title: TextReference
abstract val onBackButtonClick: () -> Unit
data class ReadContent(
override val title: TextReference,
override val onBackButtonClick: () -> Unit,
) : ManageTokensTopBarUM()
data class ManageContent(
override val title: TextReference,
override val onBackButtonClick: () -> Unit,
val endButton: TopAppBarButtonUM,
) : ManageTokensTopBarUM()
}

View file

@ -5,11 +5,40 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class ManageTokensUM(
val popBack: () -> Unit,
val items: ImmutableList<CurrencyItemUM>,
val search: SearchBarUM,
val hasChanges: Boolean,
val onAddCustomToken: () -> Unit,
val onSaveClick: () -> Unit,
)
internal sealed class ManageTokensUM {
abstract val popBack: () -> Unit
abstract val isLoading: Boolean
abstract val items: ImmutableList<CurrencyItemUM>
abstract val topBar: ManageTokensTopBarUM
abstract val search: SearchBarUM
data class ReadContent(
override val popBack: () -> Unit,
override val isLoading: Boolean,
override val items: ImmutableList<CurrencyItemUM>,
override val topBar: ManageTokensTopBarUM,
override val search: SearchBarUM,
) : ManageTokensUM()
data class ManageContent(
override val popBack: () -> Unit,
override val isLoading: Boolean,
override val items: ImmutableList<CurrencyItemUM>,
override val topBar: ManageTokensTopBarUM,
override val search: SearchBarUM,
val onSaveClick: () -> Unit,
val hasChanges: Boolean,
) : ManageTokensUM()
fun copySealed(
search: SearchBarUM = this.search,
items: ImmutableList<CurrencyItemUM> = this.items,
hasChanges: Boolean = this is ManageContent && this.hasChanges,
): ManageTokensUM {
return when (this) {
is ManageContent -> copy(search = search, items = items, hasChanges = hasChanges)
is ReadContent -> copy(search = search, items = items)
}
}
}

View file

@ -4,15 +4,16 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.util.fastForEachIndexed
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.navigation.Router
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.entity.CurrencyItemUM
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
import com.tangem.features.managetokens.entity.ManageTokensUM
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.entity.*
import com.tangem.features.managetokens.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.mutate
@ -24,17 +25,33 @@ import javax.inject.Inject
@ComponentScoped
internal class ManageTokensModel @Inject constructor(
paramsContainer: ParamsContainer,
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params: ManageTokensComponent.Params = paramsContainer.require()
private val changedItemsIds: MutableSet<String> = mutableSetOf()
private var items = initItems()
val state: MutableStateFlow<ManageTokensUM> = MutableStateFlow(
value = ManageTokensUM(
val state: MutableStateFlow<ManageTokensUM> = MutableStateFlow(value = getInitialState(mode = params.mode))
private fun getInitialState(mode: ManageTokensComponent.Mode): ManageTokensUM {
return when (mode) {
ManageTokensComponent.Mode.READ_ONLY -> createReadContentModel()
ManageTokensComponent.Mode.MANAGE -> createManageContentModel()
}
}
private fun createReadContentModel(): ManageTokensUM.ReadContent {
return ManageTokensUM.ReadContent(
popBack = router::pop,
items = items,
isLoading = false,
items = initItems(),
topBar = ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_search_tokens),
onBackButtonClick = router::pop,
),
search = SearchBarUM(
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
query = "",
@ -42,20 +59,60 @@ internal class ManageTokensModel @Inject constructor(
isActive = false,
onActiveChange = ::toggleSearchBar,
),
)
}
private fun createManageContentModel(): ManageTokensUM.ManageContent {
return ManageTokensUM.ManageContent(
popBack = router::pop,
isLoading = false,
items = initItems(),
topBar = ManageTokensTopBarUM.ManageContent(
title = resourceReference(id = R.string.main_manage_tokens),
onBackButtonClick = router::pop,
endButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_plus_24,
onIconClicked = ::onAddCustomToken,
),
),
search = SearchBarUM(
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
query = "",
onQueryChange = ::searchCurrencies,
isActive = false,
onActiveChange = ::toggleSearchBar,
),
onSaveClick = ::onSaveClick,
hasChanges = false,
onSaveClick = {},
onAddCustomToken = {},
),
)
)
}
private fun onAddCustomToken() {
// TODO: [REDACTED_JIRA]
}
private fun onSaveClick() {
// TODO: [REDACTED_JIRA]
}
@Suppress("UnusedPrivateMember")
private fun searchCurrencies(query: String) {
// TODO: [REDACTED_JIRA]
val newItems = if (query.isBlank()) {
initItems()
} else {
state.value.items.filter { currency ->
currency.model.name.contains(query, ignoreCase = true)
}.toPersistentList()
}
state.update { state ->
state.copySealed(search = state.search.copy(query = query), items = newItems)
}
}
private fun toggleSearchBar(isActive: Boolean) {
state.update { state ->
state.copy(
state.copySealed(
search = state.search.copy(isActive = isActive),
)
}
@ -135,7 +192,7 @@ internal class ManageTokensModel @Inject constructor(
items = items.mutate {
it[index] = updatedItem
}
state.copy(items = items)
state.copySealed(items = items)
}
}
@ -177,7 +234,7 @@ internal class ManageTokensModel @Inject constructor(
items = items.mutate {
it[currencyIndex] = updatedItem
}
state.copy(
state.copySealed(
items = items,
hasChanges = changedItemsIds.isNotEmpty(),
)

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
@ -36,12 +37,14 @@ import com.tangem.core.ui.components.rows.ArrowRow
import com.tangem.core.ui.components.rows.BlockchainRow
import com.tangem.core.ui.components.rows.ChainRow
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent
import com.tangem.features.managetokens.entity.CurrencyItemUM
import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM
import com.tangem.features.managetokens.entity.ManageTokensTopBarUM
import com.tangem.features.managetokens.entity.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import kotlinx.collections.immutable.ImmutableList
@ -57,14 +60,9 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
modifier = modifier,
containerColor = TangemTheme.colors.background.primary,
topBar = {
TangemTopAppBar(
ManageTokensTopBar(
modifier = Modifier.statusBarsPadding(),
title = stringResource(id = R.string.main_manage_tokens),
startButton = TopAppBarButtonUM.Back(state.popBack),
endButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_plus_24,
onIconClicked = state.onAddCustomToken,
),
topBar = state.topBar,
)
},
content = { innerPadding ->
@ -72,18 +70,36 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
state = state,
search = state.search,
items = state.items,
isLoading = state.isLoading,
hasChanges = state is ManageTokensUM.ManageContent && state.hasChanges,
)
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
SaveChangesButton(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
isVisible = state.hasChanges,
onClick = state.onSaveClick,
)
if (state is ManageTokensUM.ManageContent) {
SaveChangesButton(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
isVisible = state.hasChanges,
onClick = state.onSaveClick,
)
}
},
)
}
@Composable
private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, modifier: Modifier = Modifier) {
TangemTopAppBar(
modifier = modifier,
title = topBar.title.resolveReference(),
startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick),
endButton = when (topBar) {
is ManageTokensTopBarUM.ManageContent -> topBar.endButton
is ManageTokensTopBarUM.ReadContent -> null
},
)
}
@ -106,24 +122,48 @@ private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier:
}
@Composable
private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) {
private fun LoadingContent() {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = TangemTheme.colors.background.primary),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(color = TangemTheme.colors.icon.accent)
}
}
@Composable
private fun Content(
search: SearchBarUM,
items: ImmutableList<CurrencyItemUM>,
isLoading: Boolean,
hasChanges: Boolean,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
Currencies(
modifier = Modifier.fillMaxSize(),
items = state.items,
search = state.search,
items = items,
search = search,
)
AnimatedVisibility(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(),
visible = state.hasChanges,
visible = hasChanges,
label = "bottom_fade_visibility",
) {
BottomFade()
}
}
Crossfade(targetState = isLoading, label = "ManageTokensLoadingContent") {
if (it) {
LoadingContent()
}
}
}
@OptIn(ExperimentalFoundationApi::class)

View file

@ -140,7 +140,7 @@ internal class DefaultWalletRouter(
}
override fun openManageTokensScreen() {
router.push(AppRoute.ManageTokens)
router.push(AppRoute.ManageTokens(readOnlyContent = false))
}
override fun openScanFailedDialog(onTryAgain: () -> Unit) {