Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-26 13:14:53 +03:00
commit 43c376489a
316 changed files with 7586 additions and 3131 deletions

View file

@ -25,6 +25,7 @@ dependencies {
implementation(projects.core.navigation)
implementation(projects.core.analytics.models)
implementation(projects.common.routing)
implementation(projects.common.ui)
/* Project - Domain */
implementation(projects.domain.models)

View file

@ -2,6 +2,7 @@ package com.tangem.features.details.component.preview
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -17,7 +18,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
private val previewState = UserWalletListUM(
userWallets = persistentListOf(
UserWalletListUM.UserWalletUM(
UserWalletItemUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
name = stringReference("My Wallet"),
information = getInformation(3, "4 496,75 $"),
@ -25,7 +26,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
isEnabled = true,
onClick = {},
),
UserWalletListUM.UserWalletUM(
UserWalletItemUM(
id = UserWalletId("user_wallet_2".encodeToByteArray()),
name = stringReference("Old wallet"),
information = getInformation(3, "4 496,75 $"),
@ -33,7 +34,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
isEnabled = true,
onClick = {},
),
UserWalletListUM.UserWalletUM(
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = getInformation(3, "4 496,75 $"),

View file

@ -1,25 +1,14 @@
package com.tangem.features.details.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class UserWalletListUM(
val userWallets: ImmutableList<UserWalletUM>,
val userWallets: ImmutableList<UserWalletItemUM>,
val isWalletSavingInProgress: Boolean,
val addNewWalletText: TextReference,
val onAddNewWalletClick: () -> Unit,
) {
@Immutable
data class UserWalletUM(
val id: UserWalletId,
val name: TextReference,
val information: TextReference,
val imageUrl: String,
val isEnabled: Boolean,
val onClick: () -> Unit,
)
}
)

View file

@ -1,12 +1,12 @@
package com.tangem.features.details.model
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.utils.UserWalletSaver
import com.tangem.features.details.utils.UserWalletsFetcher
@ -48,7 +48,7 @@ internal class UserWalletListModel @Inject constructor(
}
private fun updateState(
userWallets: ImmutableList<UserWalletUM>,
userWallets: ImmutableList<UserWalletItemUM>,
shouldSaveUserWallets: Boolean,
isWalletSavingInProgress: Boolean,
) = state.update { value ->

View file

@ -1,7 +1,6 @@
package com.tangem.features.details.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
@ -10,32 +9,25 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.ui.coil.RotationTransformation
@Composable
internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
) {
state.userWallets.forEach { model ->
key(model.id) {
state.userWallets.forEach { state ->
key(state.id) {
UserWalletItem(
modifier = Modifier.fillMaxWidth(),
model = model,
state = state,
)
}
}
@ -47,96 +39,6 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M
}
}
@Composable
private fun UserWalletItem(model: UserWalletListUM.UserWalletUM, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
onClick = model.onClick,
enabled = model.isEnabled,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size68)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Image(imageUrl = model.imageUrl)
NameAndInfo(
name = model.name,
information = model.information,
)
}
}
}
@Composable
private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) {
Column(
modifier = modifier.heightIn(min = TangemTheme.dimens.size40),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceEvenly,
) {
Text(
text = name.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
AnimatedContent(
targetState = information.resolveReference(),
label = "User wallet information",
) { information ->
Text(
text = information,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
private fun Image(imageUrl: String, modifier: Modifier = Modifier) {
val imageModifier = modifier
.width(TangemTheme.dimens.size24)
.height(TangemTheme.dimens.size36)
.clip(TangemTheme.shapes.roundedCornersSmall)
SubcomposeAsyncImage(
modifier = imageModifier,
model = ImageRequest.Builder(LocalContext.current)
.transformations(RotationTransformation(angle = 90f))
.size(
width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() },
height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() },
)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = {
RectangleShimmer(
modifier = imageModifier,
radius = TangemTheme.dimens.size2,
)
},
error = {
Image(
modifier = imageModifier,
painter = painterResource(id = R.drawable.img_card_wallet_2_gray_22_36),
contentDescription = null,
)
},
contentDescription = null,
)
}
@Composable
private fun AddWalletButton(
text: TextReference,

View file

@ -1,13 +1,13 @@
package com.tangem.features.details.utils
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import com.tangem.utils.StringsSigns.STARS
import kotlinx.collections.immutable.ImmutableList
@ -19,7 +19,7 @@ internal fun List<UserWallet>.toUiModels(
balances: Map<UserWalletId, TotalFiatBalance> = emptyMap(),
isLoading: Boolean = true,
isBalancesHidden: Boolean = false,
): ImmutableList<UserWalletUM> = this.map { model ->
): ImmutableList<UserWalletItemUM> = this.map { model ->
val balance = balances[model.walletId]
model.toUiModel(
@ -37,7 +37,7 @@ private fun UserWallet.toUiModel(
isLoading: Boolean,
isBalanceHidden: Boolean,
onClick: () -> Unit,
): UserWalletUM = UserWalletUM(
): UserWalletItemUM = UserWalletItemUM(
id = walletId,
name = stringReference(name),
information = getInfo(
@ -59,7 +59,7 @@ private fun UserWallet.getInfo(
): TextReference {
val dividerRef = stringReference(value = "")
val cardCount = getCardCount()
val cardCount = getCardsCount() ?: 1
val cardCountRef = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
@ -99,12 +99,4 @@ private fun getBalanceInfo(
} else {
combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN))
}
}
private fun UserWallet.getCardCount() = when (val status = scanResponse.card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount.inc()
is CardDTO.BackupStatus.CardLinked -> status.cardCount.inc()
is CardDTO.BackupStatus.NoBackup,
null,
-> 1
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.details.utils
import arrow.core.Either
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
@ -22,7 +23,6 @@ import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -40,7 +40,7 @@ internal class UserWalletsFetcher @Inject constructor(
) {
@OptIn(ExperimentalCoroutinesApi::class)
val userWallets: Flow<ImmutableList<UserWalletUM>> = getWalletsUseCase().transformLatest { wallets ->
val userWallets: Flow<ImmutableList<UserWalletItemUM>> = getWalletsUseCase().transformLatest { wallets ->
emit(wallets.toUiModels(onClick = ::navigateToWalletSettings))
combine(
@ -72,7 +72,7 @@ internal class UserWalletsFetcher @Inject constructor(
maybeAppCurrency: Either<SelectedAppCurrencyError, AppCurrency>,
maybeBalances: Lce<TokenListError, Map<UserWalletId, TotalFiatBalance>>,
balanceHidingSettings: BalanceHidingSettings,
): Lce<Error, ImmutableList<UserWalletUM>> = lce {
): Lce<Error, ImmutableList<UserWalletItemUM>> = lce {
val balances = withError(
transform = { Error.UnableToGetBalances },
block = { maybeBalances.bindOrNull().orEmpty() },

View file

@ -1,18 +1,15 @@
package com.tangem.features.managetokens.component
import androidx.compose.runtime.Composable
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.wallets.models.UserWalletId
interface AddCustomTokenComponent {
@Composable
fun BottomSheet(isVisible: Boolean, onDismiss: () -> Unit)
interface AddCustomTokenComponent : ComposableBottomSheetComponent {
data class Params(
val userWalletId: UserWalletId,
val onDismiss: () -> Unit,
)
interface Factory {
fun create(params: Params): AddCustomTokenComponent
}
interface Factory : ComponentFactory<Params, AddCustomTokenComponent>
}

View file

@ -2,11 +2,11 @@ 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 ManageTokensComponent : ComposableContentComponent {
data class Params(val mode: Mode)
data class Params(val userWalletId: UserWalletId?)
enum class Mode { READ_ONLY, MANAGE, }
interface Factory : ComponentFactory<Params, ManageTokensComponent>
}

View file

@ -22,8 +22,10 @@ dependencies {
implementation(projects.core.featuretoggles)
/* Project - Domain */
implementation(projects.domain.wallets.models)
implementation(projects.domain.manageTokens)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
/* AndroidX */
implementation(deps.androidx.activity.compose)
@ -43,5 +45,6 @@ dependencies {
/* Other */
implementation(deps.kotlin.immutable.collections)
implementation(deps.decompose.ext.compose)
implementation(deps.timber)
}

View file

@ -1,19 +1,22 @@
package com.tangem.features.managetokens.component
import androidx.compose.foundation.lazy.LazyListScope
import com.tangem.domain.tokens.model.Network
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
internal interface CustomTokenFormComponent {
fun content(scope: LazyListScope)
internal interface CustomTokenFormComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val networkId: Network.ID,
val network: SelectedNetwork,
val derivationPath: SelectedDerivationPath,
val formValues: CustomTokenFormValues,
val onSelectNetworkClick: (CustomTokenFormValues) -> Unit,
val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit,
)
interface Factory {
fun create(params: Params): CustomTokenFormComponent
}
interface Factory : ComponentFactory<Params, CustomTokenFormComponent>
}

View file

@ -1,20 +0,0 @@
package com.tangem.features.managetokens.component
import androidx.compose.foundation.lazy.LazyListScope
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.entity.SelectedNetworkUM
internal interface CustomTokenNetworkSelectorComponent {
fun content(scope: LazyListScope)
data class Params(
val userWalletId: UserWalletId,
val selectedNetwork: SelectedNetworkUM?,
val onNetworkSelected: (SelectedNetworkUM) -> Unit,
)
interface Factory {
fun create(params: Params): CustomTokenNetworkSelectorComponent
}
}

View file

@ -0,0 +1,27 @@
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
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
internal interface CustomTokenSelectorComponent : ComposableContentComponent {
sealed class Params {
data class NetworkSelector(
val userWalletId: UserWalletId,
val selectedNetwork: SelectedNetwork?,
val onNetworkSelected: (SelectedNetwork) -> Unit,
) : Params()
data class DerivationPathSelector(
val userWalletId: UserWalletId,
val selectedDerivationPath: SelectedDerivationPath?,
val onDerivationPathSelected: (SelectedDerivationPath) -> Unit,
) : Params()
}
interface Factory : ComponentFactory<Params, CustomTokenSelectorComponent>
}

View file

@ -0,0 +1,179 @@
package com.tangem.features.managetokens.component.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.router.stack.*
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.CustomTokenFormComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: AddCustomTokenComponent.Params,
private val selectorComponentFactory: CustomTokenSelectorComponent.Factory,
private val formComponentFactory: CustomTokenFormComponent.Factory,
) : AddCustomTokenComponent, AppComponentContext by context {
private val navigation = StackNavigation<AddCustomTokenConfig>()
private val contentStack = childStack(
key = "add_custom_token_content_stack",
source = navigation,
initialConfiguration = AddCustomTokenConfig(
userWalletId = params.userWalletId,
step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR,
popBack = ::dismiss,
),
handleBackButton = true,
serializer = AddCustomTokenConfig.serializer(),
childFactory = ::contentChild,
)
override fun dismiss() {
params.onDismiss()
}
@Composable
override fun BottomSheet() {
val config = remember {
TangemBottomSheetConfig(
isShow = true,
onDismissRequest = ::dismiss,
content = contentStack.active.configuration,
)
}
val childStack by contentStack.subscribeAsState()
AddCustomTokenBottomSheet(
config = config.copy(
content = childStack.active.configuration,
),
content = { modifier ->
Children(
stack = childStack,
animation = stackAnimation(),
) { child ->
child.instance.Content(modifier = modifier)
}
},
)
}
private fun contentChild(
config: AddCustomTokenConfig,
componentContext: ComponentContext,
): ComposableContentComponent = when (config.step) {
AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR -> {
selectorComponentFactory.create(
context = childByContext(componentContext),
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = config.userWalletId,
selectedNetwork = null,
onNetworkSelected = { network ->
showForm(network = network)
},
),
)
}
AddCustomTokenConfig.Step.NETWORK_SELECTOR -> {
selectorComponentFactory.create(
context = childByContext(componentContext),
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = config.userWalletId,
selectedNetwork = config.selectedNetwork,
onNetworkSelected = { network ->
showForm(network = network)
},
),
)
}
AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR -> {
selectorComponentFactory.create(
context = childByContext(componentContext),
params = CustomTokenSelectorComponent.Params.DerivationPathSelector(
userWalletId = config.userWalletId,
selectedDerivationPath = config.selectedDerivationPath,
onDerivationPathSelected = { derivationPath ->
showForm(derivationPath = derivationPath)
},
),
)
}
AddCustomTokenConfig.Step.FORM -> {
formComponentFactory.create(
context = childByContext(componentContext),
params = CustomTokenFormComponent.Params(
userWalletId = config.userWalletId,
network = config.selectedNetwork ?: error("Network is not selected"),
derivationPath = config.selectedDerivationPath ?: SelectedDerivationPath(
value = config.selectedNetwork.derivationPath,
name = resourceReference(R.string.custom_token_derivation_path_default),
),
formValues = config.formValues,
onSelectNetworkClick = ::showNetworkSelector,
onSelectDerivationPathClick = ::showDerivationPathSelector,
),
)
}
}
private fun showDerivationPathSelector(formValues: CustomTokenFormValues) {
val currentConfig = contentStack.value.active.configuration
val config = currentConfig.copy(
step = AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR,
formValues = formValues,
popBack = navigation::pop,
)
navigation.push(config)
}
private fun showNetworkSelector(formValues: CustomTokenFormValues) {
val currentConfig = contentStack.value.active.configuration
val config = currentConfig.copy(
step = AddCustomTokenConfig.Step.NETWORK_SELECTOR,
formValues = formValues,
popBack = navigation::pop,
)
navigation.push(config)
}
private fun showForm(network: SelectedNetwork? = null, derivationPath: SelectedDerivationPath? = null) {
val currentConfig = contentStack.value.active.configuration
val config = currentConfig.copy(
step = AddCustomTokenConfig.Step.FORM,
selectedNetwork = network ?: currentConfig.selectedNetwork,
selectedDerivationPath = derivationPath ?: currentConfig.selectedDerivationPath,
popBack = ::dismiss,
)
navigation.replaceAll(config)
}
@AssistedFactory
interface Factory : AddCustomTokenComponent.Factory {
override fun create(
context: AppComponentContext,
params: AddCustomTokenComponent.Params,
): DefaultAddCustomTokenComponent
}
}

View file

@ -0,0 +1,161 @@
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.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.managetokens.component.CustomTokenFormComponent
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.CustomTokenFormContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
internal class DefaultCustomTokenFormComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: CustomTokenFormComponent.Params,
) : CustomTokenFormComponent, AppComponentContext by context {
private val state: MutableStateFlow<CustomTokenFormUM> = MutableStateFlow(
value = getInitialState(),
)
@Composable
override fun Content(modifier: Modifier) {
val state by state.collectAsStateWithLifecycle()
CustomTokenFormContent(
modifier = modifier,
model = state,
)
}
private fun getInitialState(): CustomTokenFormUM {
return CustomTokenFormUM(
networkName = ClickableFieldUM(
label = resourceReference(R.string.custom_token_network_input_title),
value = params.network.name,
onClick = ::selectNetwork,
),
tokenForm = getInitialTokenForm(),
derivationPath = ClickableFieldUM(
label = resourceReference(R.string.custom_token_derivation_path),
value = params.derivationPath.name,
onClick = ::selectDerivationPath,
),
saveToken = {
// TODO: Save token: [REDACTED_JIRA]
},
)
}
private fun getInitialTokenForm(): CustomTokenFormUM.TokenFormUM {
val formValues = params.formValues
val form = CustomTokenFormUM.TokenFormUM(
contractAddress = TextInputFieldUM(
label = resourceReference(R.string.custom_token_contract_address_input_title),
placeholder = stringReference(value = "0x000000000000000000000000000..."),
onValueChange = ::updateContractAddress,
),
name = TextInputFieldUM(
label = resourceReference(R.string.custom_token_name_input_title),
placeholder = resourceReference(R.string.custom_token_name_input_placeholder),
onValueChange = ::updateTokenName,
),
symbol = TextInputFieldUM(
label = resourceReference(R.string.custom_token_token_symbol_input_title),
placeholder = resourceReference(R.string.custom_token_token_symbol_input_placeholder),
onValueChange = ::updateTokenSymbol,
),
decimals = TextInputFieldUM(
label = resourceReference(R.string.custom_token_decimals_input_title),
placeholder = stringReference(value = "0"),
onValueChange = ::updateDecimals,
),
)
return formValues.fillValues(form)
}
private fun updateContractAddress(value: String) {
// TODO: Add field validation: [REDACTED_JIRA]
state.update { state ->
val tokenForm = state.tokenForm ?: return@update state
state.copy(
tokenForm = tokenForm.copy(
contractAddress = tokenForm.contractAddress.copy(value = value),
),
)
}
}
private fun updateTokenName(value: String) {
// TODO: Add field validation: [REDACTED_JIRA]
state.update { state ->
val tokenForm = state.tokenForm ?: return@update state
state.copy(
tokenForm = tokenForm.copy(
name = tokenForm.name.copy(value = value),
),
)
}
}
private fun updateTokenSymbol(value: String) {
// TODO: Add field validation: [REDACTED_JIRA]
state.update { state ->
val tokenForm = state.tokenForm ?: return@update state
state.copy(
tokenForm = tokenForm.copy(
symbol = tokenForm.symbol.copy(value = value),
),
)
}
}
private fun updateDecimals(value: String) {
// TODO: Add field validation: [REDACTED_JIRA]
state.update { state ->
val tokenForm = state.tokenForm ?: return@update state
state.copy(
tokenForm = tokenForm.copy(
decimals = tokenForm.decimals.copy(value = value),
),
)
}
}
private fun selectNetwork() {
params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm))
}
private fun selectDerivationPath() {
params.onSelectDerivationPathClick(CustomTokenFormValues(state.value.tokenForm))
}
@AssistedFactory
interface Factory : CustomTokenFormComponent.Factory {
override fun create(
context: AppComponentContext,
params: CustomTokenFormComponent.Params,
): DefaultCustomTokenFormComponent
}
private companion object {
const val FORM_VALUES_KEY = "form_values"
}
}

View file

@ -0,0 +1,43 @@
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.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM
import com.tangem.features.managetokens.ui.CustomTokenSelectorContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
internal class DefaultCustomTokenSelectorComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: CustomTokenSelectorComponent.Params,
) : CustomTokenSelectorComponent, AppComponentContext by context {
private val state: MutableStateFlow<CustomTokenSelectorUM> = MutableStateFlow(
value = PreviewCustomTokenSelectorComponent(params = params).previewState,
)
@Composable
override fun Content(modifier: Modifier) {
val state by state.collectAsStateWithLifecycle()
CustomTokenSelectorContent(
modifier = modifier,
model = state,
)
}
@AssistedFactory
interface Factory : CustomTokenSelectorComponent.Factory {
override fun create(
context: AppComponentContext,
params: CustomTokenSelectorComponent.Params,
): DefaultCustomTokenSelectorComponent
}
}

View file

@ -1,12 +1,21 @@
package com.tangem.features.managetokens.component.impl
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
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.decompose.ComposableBottomSheetComponent
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.entity.managetokens.BottomSheetConfig
import com.tangem.features.managetokens.model.ManageTokensModel
import com.tangem.features.managetokens.ui.ManageTokensScreen
import dagger.assisted.Assisted
@ -16,18 +25,46 @@ import dagger.assisted.AssistedInject
internal class DefaultManageTokensComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: ManageTokensComponent.Params,
private val addCustomTokenComponentFactory: AddCustomTokenComponent.Factory,
) : ManageTokensComponent, AppComponentContext by context {
private val model: ManageTokensModel = getOrCreateModel(params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = BottomSheetConfig.serializer(),
handleBackButton = false,
childFactory = ::bottomSheetChild,
)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
BackHandler(onBack = state.popBack)
ManageTokensScreen(
modifier = modifier,
state = state,
)
bottomSheet.child?.instance?.BottomSheet()
}
private fun bottomSheetChild(
config: BottomSheetConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent = when (config) {
is BottomSheetConfig.AddCustomToken -> {
addCustomTokenComponentFactory.create(
context = childByContext(componentContext),
params = AddCustomTokenComponent.Params(
userWalletId = config.userWalletId,
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
}
}
@AssistedFactory

View file

@ -4,81 +4,72 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent
import com.tangem.features.managetokens.entity.AddCustomTokenButtonUM
import com.tangem.features.managetokens.entity.AddCustomTokenUM
import com.tangem.features.managetokens.entity.ClickableFieldUM
import com.tangem.features.managetokens.entity.SelectedNetworkUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig
import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
internal class PreviewAddCustomTokenComponent(
initialState: AddCustomTokenUM = AddCustomTokenUM.NetworkSelector(popBack = {}),
initialState: AddCustomTokenConfig = AddCustomTokenConfig(
userWalletId = UserWalletId(stringValue = "321"),
step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR,
popBack = {},
),
) : AddCustomTokenComponent {
private val userWalletId = UserWalletId(stringValue = "321")
private val previewState: MutableStateFlow<AddCustomTokenConfig> = MutableStateFlow(initialState)
private val previewState: MutableStateFlow<AddCustomTokenUM> = MutableStateFlow(initialState)
override fun dismiss() {
/* no-op */
}
@Composable
override fun BottomSheet(isVisible: Boolean, onDismiss: () -> Unit) {
override fun BottomSheet() {
val state by previewState.collectAsStateWithLifecycle()
val config = TangemBottomSheetConfig(
isShow = isVisible,
onDismissRequest = onDismiss,
isShow = true,
onDismissRequest = ::dismiss,
content = state,
)
AddCustomTokenBottomSheet(
config = config,
content = {
when (val s = state) {
is AddCustomTokenUM.Form -> {
PreviewCustomTokenFormComponent(
networkName = ClickableFieldUM(
label = resourceReference(R.string.custom_token_network_input_title),
value = stringReference(s.selectedNetwork.name),
onClick = { showNetworkSelector(s.selectedNetwork) },
content = { modifier ->
when (state.step) {
AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR -> {
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = state.userWalletId,
selectedNetwork = null,
onNetworkSelected = {},
),
).content(this)
).Content(modifier)
}
is AddCustomTokenUM.NetworkSelector -> {
PreviewCustomTokenNetworkSelectorComponent(
params = CustomTokenNetworkSelectorComponent.Params(
userWalletId = userWalletId,
selectedNetwork = s.selectedNetwork,
onNetworkSelected = ::showForm,
AddCustomTokenConfig.Step.NETWORK_SELECTOR -> {
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = state.userWalletId,
selectedNetwork = state.selectedNetwork,
onNetworkSelected = {},
),
networksSize = 20,
).content(this)
).Content(modifier)
}
AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR -> {
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.DerivationPathSelector(
userWalletId = state.userWalletId,
selectedDerivationPath = state.selectedDerivationPath,
onDerivationPathSelected = {},
),
).Content(modifier)
}
AddCustomTokenConfig.Step.FORM -> {
PreviewCustomTokenFormComponent().Content(modifier)
}
}
},
)
}
private fun showNetworkSelector(selectedNetwork: SelectedNetworkUM) {
previewState.update {
AddCustomTokenUM.NetworkSelector(selectedNetwork, popBack = { showForm(selectedNetwork) })
}
}
private fun showForm(network: SelectedNetworkUM) {
previewState.update {
AddCustomTokenUM.Form(
popBack = {},
selectedNetwork = network,
addTokenButton = AddCustomTokenButtonUM.Visible(
isEnabled = false,
onClick = {},
),
)
}
}
}

View file

@ -1,81 +1,87 @@
package com.tangem.features.managetokens.component.preview
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.managetokens.component.CustomTokenFormComponent
import com.tangem.features.managetokens.entity.ClickableFieldUM
import com.tangem.features.managetokens.entity.CustomTokenFormUM
import com.tangem.features.managetokens.entity.TextInputFieldUM
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.customTokenFormContent
import com.tangem.features.managetokens.ui.CustomTokenFormContent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal class PreviewCustomTokenFormComponent(
networkName: ClickableFieldUM = ClickableFieldUM(
label = resourceReference(R.string.custom_token_network_input_title),
value = stringReference(value = "Ethereum"),
onClick = {},
),
derivationPath: ClickableFieldUM = ClickableFieldUM(
label = resourceReference(R.string.custom_token_derivation_path),
value = stringReference(value = "Default"),
onClick = {},
),
networkName: ClickableFieldUM = PreviewCustomTokenFormComponent.networkName,
derivationPath: ClickableFieldUM = PreviewCustomTokenFormComponent.derivationPath,
canAddToken: Boolean = false,
contractAddress: TextInputFieldUM = TextInputFieldUM(
label = resourceReference(R.string.custom_token_contract_address_input_title),
placeholder = stringReference(value = "0x000000000000000000000000000"),
value = "",
onValueChange = {},
),
tokenName: TextInputFieldUM = TextInputFieldUM(
label = resourceReference(R.string.custom_token_name_input_title),
placeholder = stringReference(value = "E.g. USD Coin"),
value = "",
onValueChange = {},
),
tokenSymbol: TextInputFieldUM = TextInputFieldUM(
label = resourceReference(R.string.custom_token_token_symbol_input_title),
placeholder = stringReference(value = "E.g. USDC"),
value = "",
onValueChange = {},
),
tokenDecimals: TextInputFieldUM = TextInputFieldUM(
label = resourceReference(R.string.custom_token_decimals_input_title),
placeholder = stringReference(value = "8"),
value = "",
onValueChange = {},
),
notifications: ImmutableList<CustomTokenFormUM.NotificationUM> = persistentListOf(
CustomTokenFormUM.NotificationUM(
id = "1",
config = NotificationConfig(
title = stringReference(value = "Note that tokens can be created by anyone"),
subtitle = stringReference(value = "Be aware of adding scam tokens, they can cost nothing"),
iconResId = R.drawable.img_attention_20,
),
),
),
tokenForm: CustomTokenFormUM.TokenFormUM? = PreviewCustomTokenFormComponent.tokenForm,
notifications: ImmutableList<CustomTokenFormUM.NotificationUM> = PreviewCustomTokenFormComponent.notifications,
) : CustomTokenFormComponent {
private val previewState = CustomTokenFormUM(
networkName = networkName,
contractAddress = contractAddress,
tokenName = tokenName,
tokenSymbol = tokenSymbol,
tokenDecimals = tokenDecimals,
tokenForm = tokenForm,
derivationPath = derivationPath,
notifications = notifications,
canAddToken = canAddToken,
onDerivationPathClick = {},
onNetworkClick = {},
onAddClick = {},
saveToken = {},
)
override fun content(scope: LazyListScope) {
scope.customTokenFormContent(model = previewState)
@Composable
override fun Content(modifier: Modifier) {
CustomTokenFormContent(modifier = modifier, model = previewState)
}
companion object {
val networkName: ClickableFieldUM = ClickableFieldUM(
label = resourceReference(R.string.custom_token_network_input_title),
value = stringReference(value = "Ethereum"),
onClick = {},
)
val derivationPath: ClickableFieldUM = ClickableFieldUM(
label = resourceReference(R.string.custom_token_derivation_path),
value = stringReference(value = "Default"),
onClick = {},
)
val tokenForm: CustomTokenFormUM.TokenFormUM = CustomTokenFormUM.TokenFormUM(
contractAddress = TextInputFieldUM(
label = resourceReference(R.string.custom_token_contract_address_input_title),
placeholder = stringReference(value = "0x000000000000000000000000000"),
value = "",
onValueChange = {},
),
name = TextInputFieldUM(
label = resourceReference(R.string.custom_token_name_input_title),
placeholder = stringReference(value = "E.g. USD Coin"),
value = "",
onValueChange = {},
),
symbol = TextInputFieldUM(
label = resourceReference(R.string.custom_token_token_symbol_input_title),
placeholder = stringReference(value = "E.g. USDC"),
value = "",
onValueChange = {},
),
decimals = TextInputFieldUM(
label = resourceReference(R.string.custom_token_decimals_input_title),
placeholder = stringReference(value = "8"),
value = "",
onValueChange = {},
),
)
val notifications: ImmutableList<CustomTokenFormUM.NotificationUM> = persistentListOf(
CustomTokenFormUM.NotificationUM(
id = "1",
config = NotificationConfig(
title = stringReference(value = "Note that tokens can be created by anyone"),
subtitle = stringReference(value = "Be aware of adding scam tokens, they can cost nothing"),
iconResId = R.drawable.img_attention_20,
),
),
)
}
}

View file

@ -1,50 +0,0 @@
package com.tangem.features.managetokens.component.preview
import androidx.compose.foundation.lazy.LazyListScope
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
import com.tangem.features.managetokens.entity.CustomTokenNetworkSelectorUM
import com.tangem.features.managetokens.entity.SelectedNetworkUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.customTokenNetworkSelectorContent
import kotlinx.collections.immutable.toImmutableList
internal class PreviewCustomTokenNetworkSelectorComponent(
private val params: CustomTokenNetworkSelectorComponent.Params = CustomTokenNetworkSelectorComponent.Params(
userWalletId = UserWalletId(stringValue = "321"),
selectedNetwork = null,
onNetworkSelected = {},
),
networksSize: Int = 5,
) : CustomTokenNetworkSelectorComponent {
private val previewNetworks = List(size = networksSize) { networkIndex ->
val n = SelectedNetworkUM(
id = Network.ID(networkIndex.toString()),
name = "Network $networkIndex",
)
CurrencyNetworkUM(
id = n.id,
name = n.name,
type = "N$networkIndex",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = false,
isSelected = n.id == params.selectedNetwork?.id,
onSelectedStateChange = { params.onNetworkSelected(n) },
)
}.toImmutableList()
private val previewState = CustomTokenNetworkSelectorUM(
showTitle = params.selectedNetwork == null,
networks = previewNetworks,
)
override fun content(scope: LazyListScope) {
scope.customTokenNetworkSelectorContent(
model = previewState,
)
}
}

View file

@ -0,0 +1,79 @@
package com.tangem.features.managetokens.component.preview
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
import com.tangem.features.managetokens.entity.item.DerivationPathUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.CustomTokenSelectorContent
import kotlinx.collections.immutable.toImmutableList
internal class PreviewCustomTokenSelectorComponent(
private val params: Params = Params.NetworkSelector(
userWalletId = UserWalletId(stringValue = "321"),
selectedNetwork = null,
onNetworkSelected = {},
),
itemsSize: Int = 5,
) : CustomTokenSelectorComponent {
private val previewItems = List(size = itemsSize) { index ->
when (params) {
is Params.DerivationPathSelector -> {
val d = SelectedDerivationPath(
value = "m/44'/0'/0'/0/$index",
name = stringReference(value = "Network $index"),
)
DerivationPathUM(
value = "m/44'/0'/0'/0/$index",
blockchainName = d.name,
isSelected = d.value == params.selectedDerivationPath?.value,
onSelectedStateChange = { params.onDerivationPathSelected(d) },
)
}
is Params.NetworkSelector -> {
val n = SelectedNetwork(
id = Network.ID(index.toString()),
name = stringReference(value = "Network $index"),
derivationPath = "m/44'/0'/0'/0/$index",
)
CurrencyNetworkUM(
networkId = n.id,
name = "Network $index",
type = "N$index",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = false,
isSelected = n.id == params.selectedNetwork?.id,
onSelectedStateChange = { params.onNetworkSelected(n) },
)
}
}
}.toImmutableList()
val previewState = CustomTokenSelectorUM(
header = when (params) {
is Params.DerivationPathSelector -> CustomTokenSelectorUM.HeaderUM.CustomDerivationButton({})
is Params.NetworkSelector -> if (params.selectedNetwork == null) {
CustomTokenSelectorUM.HeaderUM.Description
} else {
CustomTokenSelectorUM.HeaderUM.None
}
},
items = previewItems,
)
@Composable
override fun Content(modifier: Modifier) {
CustomTokenSelectorContent(modifier = modifier, model = previewState)
}
}

View file

@ -9,11 +9,14 @@ 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.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.entity.*
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.ManageTokensScreen
import kotlinx.collections.immutable.mutate
@ -47,8 +50,10 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
onActiveChange = ::toggleSearchBar,
),
hasChanges = false,
isLoading = false,
onSaveClick = {},
isInitialBatchLoading = false,
isNextBatchLoading = true,
loadMore = { false },
saveChanges = {},
),
)
@ -58,7 +63,7 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
initItems()
} else {
items.filter { currency ->
currency.model.name.contains(query, ignoreCase = true)
currency.name.contains(query, ignoreCase = true)
}.toPersistentList()
}
@ -96,34 +101,28 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
}.toPersistentList()
private fun getCustomItem(index: Int) = CurrencyItemUM.Custom(
id = index.toString(),
model = ChainRowUM(
name = "Custom token $index",
type = "CT$index",
icon = CurrencyIconState.CustomTokenIcon(
tint = Color.White,
background = Color.Black,
topBadgeIconResId = R.drawable.img_eth_22,
isGrayscale = false,
showCustomBadge = true,
),
showCustom = true,
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 = index.toString(),
model = ChainRowUM(
name = "Currency $index",
type = "C$index",
icon = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = R.drawable.img_btc_22,
isGrayscale = false,
showCustomBadge = false,
),
showCustom = false,
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(index))
@ -135,7 +134,7 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex ->
CurrencyNetworkUM(
id = Network.ID(networkIndex.toString()),
networkId = Network.ID(networkIndex.toString()),
name = "NETWORK$networkIndex",
type = "N$networkIndex",
iconResId = R.drawable.ic_eth_16,

View file

@ -1,6 +1,12 @@
package com.tangem.features.managetokens.di
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.CustomTokenFormComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.impl.DefaultAddCustomTokenComponent
import com.tangem.features.managetokens.component.impl.DefaultCustomTokenFormComponent
import com.tangem.features.managetokens.component.impl.DefaultCustomTokenSelectorComponent
import com.tangem.features.managetokens.component.impl.DefaultManageTokensComponent
import dagger.Binds
import dagger.Module
@ -15,4 +21,22 @@ internal interface ComponentModule {
@Binds
@Singleton
fun bindManageTokensComponentFactory(factory: DefaultManageTokensComponent.Factory): ManageTokensComponent.Factory
@Binds
@Singleton
fun bindAddCustomTokenComponentFactory(
factory: DefaultAddCustomTokenComponent.Factory,
): AddCustomTokenComponent.Factory
@Binds
@Singleton
fun bindCustomTokenSelectorComponentFactory(
factory: DefaultCustomTokenSelectorComponent.Factory,
): CustomTokenSelectorComponent.Factory
@Binds
@Singleton
fun bindCustomTokenFormComponentFactory(
factory: DefaultCustomTokenFormComponent.Factory,
): CustomTokenFormComponent.Factory
}

View file

@ -1,54 +0,0 @@
package com.tangem.features.managetokens.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.domain.tokens.model.Network
@Immutable
internal sealed class AddCustomTokenUM : TangemBottomSheetConfigContent {
abstract val selectedNetwork: SelectedNetworkUM?
abstract val addTokenButton: AddCustomTokenButtonUM
abstract val popBack: () -> Unit
data class NetworkSelector(
override val selectedNetwork: SelectedNetworkUM? = null,
override val popBack: () -> Unit,
) : AddCustomTokenUM() {
override val addTokenButton: AddCustomTokenButtonUM = AddCustomTokenButtonUM.Hidden
}
data class Form(
override val selectedNetwork: SelectedNetworkUM,
override val addTokenButton: AddCustomTokenButtonUM.Visible,
override val popBack: () -> Unit,
) : AddCustomTokenUM()
}
@Immutable
internal data class SelectedNetworkUM(
val id: Network.ID,
val name: String,
)
@Immutable
internal sealed class AddCustomTokenButtonUM {
open val onClick: () -> Unit = {}
open val isEnabled: Boolean = false
val isVisible: Boolean
get() = this is Visible
data object Hidden : AddCustomTokenButtonUM() {
override val onClick: () -> Unit = {}
}
data class Visible(
override val isEnabled: Boolean,
override val onClick: () -> Unit,
) : AddCustomTokenButtonUM()
}

View file

@ -1,36 +0,0 @@
package com.tangem.features.managetokens.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.rows.model.ChainRowUM
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class CurrencyItemUM {
abstract val id: String
abstract val model: ChainRowUM
data class Basic(
override val id: String,
override val model: ChainRowUM,
val networks: NetworksUM,
val onExpandClick: () -> Unit,
) : CurrencyItemUM() {
@Immutable
sealed class NetworksUM {
data object Collapsed : NetworksUM()
data class Expanded(
val networks: ImmutableList<CurrencyNetworkUM>,
) : NetworksUM()
}
}
data class Custom(
override val id: String,
override val model: ChainRowUM,
val onRemoveClick: () -> Unit,
) : CurrencyItemUM()
}

View file

@ -1,15 +0,0 @@
package com.tangem.features.managetokens.entity
import androidx.compose.runtime.Immutable
import com.tangem.domain.tokens.model.Network
@Immutable
internal data class CurrencyNetworkUM(
val id: Network.ID,
val name: String,
val type: String,
val iconResId: Int,
val isMainNetwork: Boolean,
val isSelected: Boolean,
val onSelectedStateChange: (Boolean) -> Unit,
)

View file

@ -1,10 +0,0 @@
package com.tangem.features.managetokens.entity
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class CustomTokenNetworkSelectorUM(
val showTitle: Boolean,
val networks: ImmutableList<CurrencyNetworkUM>,
)

View file

@ -1,44 +0,0 @@
package com.tangem.features.managetokens.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import kotlinx.collections.immutable.ImmutableList
@Immutable
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

@ -0,0 +1,38 @@
package com.tangem.features.managetokens.entity.customtoken
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
internal data class AddCustomTokenConfig(
val step: Step,
val popBack: () -> Unit,
val userWalletId: UserWalletId,
val selectedNetwork: SelectedNetwork? = null,
val selectedDerivationPath: SelectedDerivationPath? = null,
val formValues: CustomTokenFormValues = CustomTokenFormValues(),
) : TangemBottomSheetConfigContent {
enum class Step {
INITIAL_NETWORK_SELECTOR,
NETWORK_SELECTOR,
DERIVATION_PATH_SELECTOR,
FORM,
}
}
@Serializable
internal data class SelectedNetwork(
val id: Network.ID,
val name: TextReference,
val derivationPath: String,
)
@Serializable
internal data class SelectedDerivationPath(
val value: String,
val name: TextReference,
)

View file

@ -1,42 +1,40 @@
package com.tangem.features.managetokens.entity
package com.tangem.features.managetokens.entity.customtoken
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Immutable
internal data class CustomTokenFormUM(
val networkName: ClickableFieldUM,
val contractAddress: TextInputFieldUM,
val tokenName: TextInputFieldUM,
val tokenSymbol: TextInputFieldUM,
val tokenDecimals: TextInputFieldUM,
val derivationPath: ClickableFieldUM,
val notifications: ImmutableList<NotificationUM>,
val canAddToken: Boolean,
val onNetworkClick: () -> Unit,
val onDerivationPathClick: () -> Unit,
val onAddClick: () -> Unit,
val tokenForm: TokenFormUM?,
val notifications: ImmutableList<NotificationUM> = persistentListOf(),
val canAddToken: Boolean = false,
val saveToken: () -> Unit,
) {
@Immutable
data class TokenFormUM(
val contractAddress: TextInputFieldUM,
val name: TextInputFieldUM,
val symbol: TextInputFieldUM,
val decimals: TextInputFieldUM,
)
data class NotificationUM(
val id: String,
val config: NotificationConfig,
)
}
@Immutable
internal data class TextInputFieldUM(
val label: TextReference,
val placeholder: TextReference,
val value: String,
val onValueChange: (String) -> Unit,
val value: String = "",
val error: TextReference? = null,
val onValueChange: (String) -> Unit,
)
@Immutable
internal data class ClickableFieldUM(
val label: TextReference,
val value: TextReference,

View file

@ -0,0 +1,31 @@
package com.tangem.features.managetokens.entity.customtoken
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM
import kotlinx.serialization.Serializable
@JvmInline
@Serializable
internal value class CustomTokenFormValues private constructor(private val values: List<String>) {
constructor() : this(values = emptyList())
constructor(form: TokenFormUM?) : this(
values = if (form == null) {
emptyList()
} else {
listOf(
form.contractAddress.value,
form.name.value,
form.symbol.value,
form.decimals.value,
)
},
)
fun fillValues(to: TokenFormUM): TokenFormUM = to.copy(
contractAddress = to.contractAddress.copy(value = values.getOrElse(index = 0) { "" }),
name = to.name.copy(value = values.getOrElse(index = 1) { "" }),
symbol = to.symbol.copy(value = values.getOrElse(index = 2) { "" }),
decimals = to.decimals.copy(value = values.getOrElse(index = 3) { "" }),
)
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.managetokens.entity.customtoken
import androidx.compose.runtime.Immutable
import com.tangem.features.managetokens.entity.item.SelectableItemUM
import kotlinx.collections.immutable.ImmutableList
internal data class CustomTokenSelectorUM(
val header: HeaderUM,
val items: ImmutableList<SelectableItemUM>,
) {
@Immutable
sealed class HeaderUM {
data object None : HeaderUM()
data object Description : HeaderUM()
data class CustomDerivationButton(
val onClick: () -> Unit,
) : HeaderUM()
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.features.managetokens.entity.item
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class CurrencyItemUM {
abstract val id: ManagedCryptoCurrency.ID
abstract val name: String
abstract val symbol: String
abstract val icon: CurrencyIconState
data class Basic(
override val id: ManagedCryptoCurrency.ID,
override val name: String,
override val symbol: String,
override val icon: CurrencyIconState,
val networks: NetworksUM,
val onExpandClick: () -> Unit,
) : CurrencyItemUM() {
@Immutable
sealed class NetworksUM {
data object Collapsed : NetworksUM()
data class Expanded(
val networks: ImmutableList<CurrencyNetworkUM>,
) : NetworksUM()
}
}
data class Custom(
override val id: ManagedCryptoCurrency.ID,
override val name: String,
override val symbol: String,
override val icon: CurrencyIconState,
val onRemoveClick: () -> Unit,
) : CurrencyItemUM()
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.managetokens.entity.item
import com.tangem.domain.tokens.model.Network
internal data class CurrencyNetworkUM(
val networkId: Network.ID,
val name: String,
val type: String,
val iconResId: Int,
val isMainNetwork: Boolean,
override val isSelected: Boolean,
override val onSelectedStateChange: (Boolean) -> Unit,
) : SelectableItemUM {
override val id: String = networkId.value
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.managetokens.entity.item
import com.tangem.core.ui.extensions.TextReference
internal data class DerivationPathUM(
val value: String,
val blockchainName: TextReference,
override val isSelected: Boolean,
override val onSelectedStateChange: (Boolean) -> Unit,
) : SelectableItemUM {
override val id: String = value
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.managetokens.entity.item
import androidx.compose.runtime.Immutable
@Immutable
internal sealed interface SelectableItemUM {
val id: String
val isSelected: Boolean
val onSelectedStateChange: (Boolean) -> Unit
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.managetokens.entity.managetokens
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
internal sealed class BottomSheetConfig {
@Serializable
data class AddCustomToken(
val userWalletId: UserWalletId,
) : BottomSheetConfig()
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.managetokens.entity
package com.tangem.features.managetokens.entity.managetokens
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM

View file

@ -0,0 +1,64 @@
package com.tangem.features.managetokens.entity.managetokens
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class ManageTokensUM {
abstract val popBack: () -> Unit
abstract val isInitialBatchLoading: Boolean
abstract val isNextBatchLoading: Boolean
abstract val items: ImmutableList<CurrencyItemUM>
abstract val topBar: ManageTokensTopBarUM
abstract val search: SearchBarUM
abstract val loadMore: () -> Boolean
data class ReadContent(
override val popBack: () -> Unit,
override val isInitialBatchLoading: Boolean,
override val isNextBatchLoading: Boolean,
override val items: ImmutableList<CurrencyItemUM>,
override val topBar: ManageTokensTopBarUM,
override val search: SearchBarUM,
override val loadMore: () -> Boolean,
) : ManageTokensUM()
data class ManageContent(
override val popBack: () -> Unit,
override val isInitialBatchLoading: Boolean,
override val isNextBatchLoading: Boolean,
override val items: ImmutableList<CurrencyItemUM>,
override val topBar: ManageTokensTopBarUM,
override val search: SearchBarUM,
override val loadMore: () -> Boolean,
val saveChanges: () -> Unit,
val hasChanges: Boolean,
) : ManageTokensUM()
fun copySealed(
search: SearchBarUM = this.search,
items: ImmutableList<CurrencyItemUM> = this.items,
hasChanges: Boolean = this is ManageContent && this.hasChanges,
isInitialBatchLoading: Boolean = this.isInitialBatchLoading,
isNextBatchLoading: Boolean = this.isNextBatchLoading,
): ManageTokensUM {
return when (this) {
is ManageContent -> copy(
search = search,
items = items,
hasChanges = hasChanges,
isInitialBatchLoading = isInitialBatchLoading,
isNextBatchLoading = isNextBatchLoading,
)
is ReadContent -> copy(
search = search,
items = items,
isInitialBatchLoading = isInitialBatchLoading,
isNextBatchLoading = isNextBatchLoading,
)
}
}
}

View file

@ -1,53 +1,82 @@
package com.tangem.features.managetokens.model
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.util.fastForEachIndexed
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
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
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.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.entity.*
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.managetokens.BottomSheetConfig
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
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.PaginationStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ComponentScoped
internal class ManageTokensModel @Inject constructor(
paramsContainer: ParamsContainer,
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val manageTokensListManager: ManageTokensListManager,
private val messageSender: UiMessageSender,
paramsContainer: ParamsContainer,
) : Model() {
private val params: ManageTokensComponent.Params = paramsContainer.require()
private val changedItemsIds: MutableSet<String> = mutableSetOf()
private var items = initItems()
val state: MutableStateFlow<ManageTokensUM> = MutableStateFlow(value = getInitialState(mode = params.mode))
val state: MutableStateFlow<ManageTokensUM> = MutableStateFlow(getInitialState(params.userWalletId))
val bottomSheetNavigation: SlotNavigation<BottomSheetConfig> = SlotNavigation()
private fun getInitialState(mode: ManageTokensComponent.Mode): ManageTokensUM {
return when (mode) {
ManageTokensComponent.Mode.READ_ONLY -> createReadContentModel()
ManageTokensComponent.Mode.MANAGE -> createManageContentModel()
init {
manageTokensListManager.uiItems
.onEach { items -> updateItems(items) }
.launchIn(modelScope)
manageTokensListManager.paginationStatus
.onEach { status -> updatePaginationStatus(status) }
.launchIn(modelScope)
combine(
manageTokensListManager.currenciesToAdd,
manageTokensListManager.currenciesToRemove,
::updateChangedItems,
).launchIn(modelScope)
modelScope.launch {
manageTokensListManager.launchPagination(params.userWalletId)
}
}
private fun getInitialState(userWalletId: UserWalletId?): ManageTokensUM {
return if (userWalletId == null) {
createReadContentModel()
} else {
createManageContentModel()
}
}
private fun createReadContentModel(): ManageTokensUM.ReadContent {
return ManageTokensUM.ReadContent(
popBack = router::pop,
isLoading = false,
items = initItems(),
isInitialBatchLoading = true,
isNextBatchLoading = false,
items = getInitialItems(),
topBar = ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_search_tokens),
onBackButtonClick = router::pop,
@ -59,20 +88,22 @@ internal class ManageTokensModel @Inject constructor(
isActive = false,
onActiveChange = ::toggleSearchBar,
),
loadMore = ::loadMoreItems,
)
}
private fun createManageContentModel(): ManageTokensUM.ManageContent {
return ManageTokensUM.ManageContent(
popBack = router::pop,
isLoading = false,
items = initItems(),
isInitialBatchLoading = true,
isNextBatchLoading = false,
items = getInitialItems(),
topBar = ManageTokensTopBarUM.ManageContent(
title = resourceReference(id = R.string.main_manage_tokens),
onBackButtonClick = router::pop,
endButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_plus_24,
onIconClicked = ::onAddCustomToken,
onIconClicked = ::navigateToAddCustomToken,
),
),
search = SearchBarUM(
@ -82,162 +113,125 @@ internal class ManageTokensModel @Inject constructor(
isActive = false,
onActiveChange = ::toggleSearchBar,
),
onSaveClick = ::onSaveClick,
hasChanges = false,
saveChanges = ::saveChanges,
loadMore = ::loadMoreItems,
)
}
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()
}
private fun updateItems(items: ImmutableList<CurrencyItemUM>) {
state.update { state ->
state.copySealed(search = state.search.copy(query = query), items = newItems)
state.copySealed(
items = items,
)
}
}
private fun updatePaginationStatus(status: PaginationStatus<*>) {
state.update { state ->
when (status) {
is PaginationStatus.None,
is PaginationStatus.InitialLoading,
-> {
if (state.search.isActive) {
state
} else {
state.copySealed(
isInitialBatchLoading = true,
)
}
}
is PaginationStatus.NextBatchLoading -> state.copySealed(
isNextBatchLoading = true,
)
is PaginationStatus.InitialLoadingError -> {
val message = SnackbarMessage(
message = status.throwable.localizedMessage
?.let(::stringReference)
?: resourceReference(R.string.common_error),
)
messageSender.send(message)
state.copySealed(
isInitialBatchLoading = false,
isNextBatchLoading = false,
)
}
is PaginationStatus.Paginating,
is PaginationStatus.EndOfPagination,
-> state.copySealed(
isInitialBatchLoading = false,
isNextBatchLoading = false,
)
}
}
}
private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) {
state.update { state ->
state.copySealed(
hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(),
)
}
}
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 getInitialItems(): ImmutableList<CurrencyItemUM> {
return persistentListOf()
}
private fun navigateToAddCustomToken() {
params.userWalletId?.let {
bottomSheetNavigation.activate(BottomSheetConfig.AddCustomToken(it))
}
}
private fun saveChanges() {
// TODO: [REDACTED_JIRA]
}
private fun searchCurrencies(query: String) {
state.update { state ->
state.copySealed(
search = state.search.copy(
query = query,
isActive = true,
),
)
}
modelScope.launch {
manageTokensListManager.search(params.userWalletId, query)
}
}
private fun toggleSearchBar(isActive: Boolean) {
state.update { state ->
state.copySealed(
search = state.search.copy(isActive = isActive),
search = state.search.copy(
query = if (isActive) state.search.query else "",
isActive = isActive,
),
)
}
}
private fun initItems() = List(size = 30) { index ->
if (index < 2) {
getCustomItem(index)
} else {
getBasicItem(index)
}
}.toPersistentList()
private fun getCustomItem(index: Int) = CurrencyItemUM.Custom(
id = index.toString(),
model = ChainRowUM(
name = "Custom token $index",
type = "CT$index",
icon = CurrencyIconState.CustomTokenIcon(
tint = Color.White,
background = Color.Black,
topBadgeIconResId = R.drawable.img_eth_22,
isGrayscale = false,
showCustomBadge = true,
),
showCustom = true,
),
onRemoveClick = {},
)
private fun getBasicItem(index: Int) = CurrencyItemUM.Basic(
id = index.toString(),
model = ChainRowUM(
name = "Currency $index",
type = "C$index",
icon = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = R.drawable.img_btc_22,
isGrayscale = false,
showCustomBadge = false,
),
showCustom = false,
),
networks = if (index == 2) {
CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index))
} else {
CurrencyItemUM.Basic.NetworksUM.Collapsed
},
onExpandClick = { toggleCurrency(index) },
)
private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex ->
CurrencyNetworkUM(
id = Network.ID(networkIndex.toString()),
name = "NETWORK$networkIndex",
type = "N$networkIndex",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = networkIndex == 0,
isSelected = false,
onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) },
)
}.toImmutableList()
private fun toggleCurrency(index: Int) {
val updatedItem = when (val item = items[index]) {
is CurrencyItemUM.Basic -> item.copy(
networks = if (item.networks is CurrencyItemUM.Basic.NetworksUM.Collapsed) {
CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index))
} else {
CurrencyItemUM.Basic.NetworksUM.Collapsed
},
)
is CurrencyItemUM.Custom -> return
}
state.update { state ->
items = items.mutate {
it[index] = updatedItem
modelScope.launch {
if (!isActive) {
manageTokensListManager.reload(params.userWalletId)
}
state.copySealed(items = items)
}
}
private fun toggleNetwork(currencyIndex: Int, networkIndex: Int, isSelected: Boolean) {
val updatedItem = when (val item = items[currencyIndex]) {
is CurrencyItemUM.Basic -> {
val updatedNetworks = (item.networks as? CurrencyItemUM.Basic.NetworksUM.Expanded)
?.copy(
networks = item.networks.networks.toPersistentList().mutate {
it.fastForEachIndexed { index, network ->
if (index == networkIndex) {
it[index] = network.copy(
iconResId = if (isSelected) {
R.drawable.img_eth_22
} else {
R.drawable.ic_eth_16
},
isSelected = isSelected,
)
}
}
},
)
?: return
item.copy(networks = updatedNetworks)
}
is CurrencyItemUM.Custom -> return
}
val id = "${currencyIndex}_$networkIndex"
if (changedItemsIds.contains(id)) {
changedItemsIds.remove(id)
} else {
changedItemsIds.add(id)
}
state.update { state ->
items = items.mutate {
it[currencyIndex] = updatedItem
}
state.copySealed(
items = items,
hasChanges = changedItemsIds.isNotEmpty(),
)
}
}
}

View file

@ -1,146 +1,80 @@
package com.tangem.features.managetokens.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
import com.tangem.core.ui.components.isOpened
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.preview.PreviewAddCustomTokenComponent
import com.tangem.features.managetokens.entity.AddCustomTokenButtonUM
import com.tangem.features.managetokens.entity.AddCustomTokenUM
import com.tangem.features.managetokens.entity.SelectedNetworkUM
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
import com.tangem.features.managetokens.impl.R
@Composable
internal fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig, content: LazyListScope.() -> Unit) {
TangemBottomSheet<AddCustomTokenUM>(
internal fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig, content: @Composable (Modifier) -> Unit) {
TangemBottomSheet<AddCustomTokenConfig>(
config = config,
addBottomInsets = false,
title = { model ->
Title(model)
},
containerColor = TangemTheme.colors.background.secondary,
content = { model ->
Content(
model = model,
content = content,
)
content = {
val contentModifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxSize()
content(contentModifier)
},
)
}
@Composable
private fun Title(model: AddCustomTokenUM, modifier: Modifier = Modifier) {
val showTokenNetworkTitle = model is AddCustomTokenUM.NetworkSelector && model.selectedNetwork != null
if (showTokenNetworkTitle) {
TangemTopAppBar(
modifier = modifier,
title = resourceReference(R.string.custom_token_network_selector_title),
titleAlignment = Alignment.CenterHorizontally,
startButton = TopAppBarButtonUM.Back(model.popBack),
height = TangemTopAppBarHeight.BOTTOM_SHEET,
)
} else {
TangemBottomSheetTitle(
modifier = modifier,
title = resourceReference(R.string.add_custom_token_title),
)
}
}
@Composable
private fun Content(model: AddCustomTokenUM, content: LazyListScope.() -> Unit, modifier: Modifier = Modifier) {
val density = LocalDensity.current
val keyboardState by keyboardAsState()
var fabHeight by remember { mutableStateOf(0.dp) }
Scaffold(
modifier = modifier.imePadding(),
containerColor = TangemTheme.colors.background.secondary,
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
AnimatedVisibility(
modifier = Modifier.onSizeChanged {
fabHeight = with(density) { it.height.toDp() }
},
visible = model.addTokenButton.isVisible && !keyboardState.isOpened,
enter = fadeIn(),
exit = fadeOut(),
label = "Add button visibility",
) {
PrimaryButton(
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing16)
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(id = R.string.custom_token_add_token),
enabled = model.addTokenButton.isEnabled,
onClick = model.addTokenButton.onClick,
)
}
},
) { paddingValues ->
LazyColumn(
modifier = Modifier.padding(paddingValues),
contentPadding = PaddingValues(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing32 + fabHeight,
),
) {
item {
if (model is AddCustomTokenUM.NetworkSelector && model.selectedNetwork != null) {
Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing12))
} else {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = TangemTheme.dimens.spacing16),
contentAlignment = Alignment.Center,
) {
Text(
modifier = Modifier.fillMaxWidth(fraction = 0.7f),
text = stringResource(id = R.string.custom_token_subtitle),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
}
}
content()
private fun Title(model: AddCustomTokenConfig, modifier: Modifier = Modifier) {
when (model.step) {
AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR,
AddCustomTokenConfig.Step.FORM,
-> {
TangemBottomSheetTitle(
modifier = modifier,
title = resourceReference(R.string.add_custom_token_title),
)
}
AddCustomTokenConfig.Step.NETWORK_SELECTOR -> {
TangemTopAppBar(
modifier = modifier,
title = resourceReference(R.string.custom_token_network_selector_title),
titleAlignment = Alignment.CenterHorizontally,
startButton = TopAppBarButtonUM.Back(model.popBack),
height = TangemTopAppBarHeight.BOTTOM_SHEET,
)
}
AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR -> {
TangemTopAppBar(
modifier = modifier,
title = resourceReference(R.string.custom_token_derivation_path),
titleAlignment = Alignment.CenterHorizontally,
startButton = TopAppBarButtonUM.Back(model.popBack),
height = TangemTopAppBarHeight.BOTTOM_SHEET,
)
}
}
}
@ -153,7 +87,7 @@ private fun Preview_AddCustomTokenBottomSheet(
@PreviewParameter(AddCustomTokenComponentPreviewProvider::class) component: AddCustomTokenComponent,
) {
TangemThemePreview {
component.BottomSheet(isVisible = true, onDismiss = {})
component.BottomSheet()
}
}
@ -162,24 +96,37 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<
get() = sequenceOf(
PreviewAddCustomTokenComponent(),
PreviewAddCustomTokenComponent(
initialState = AddCustomTokenUM.NetworkSelector(
initialState = AddCustomTokenConfig(
userWalletId = UserWalletId(stringValue = "321"),
step = AddCustomTokenConfig.Step.FORM,
popBack = {},
selectedNetwork = SelectedNetworkUM(
id = Network.ID(value = "0"),
name = "Ethereum",
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "1"),
name = stringReference("Ethereum"),
derivationPath = "m/44'/0'/0'/0/0",
),
),
),
PreviewAddCustomTokenComponent(
initialState = AddCustomTokenUM.Form(
initialState = AddCustomTokenConfig(
userWalletId = UserWalletId(stringValue = "321"),
step = AddCustomTokenConfig.Step.NETWORK_SELECTOR,
popBack = {},
selectedNetwork = SelectedNetworkUM(
id = Network.ID(value = "1"),
name = "Ethereum",
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "0"),
name = stringReference("Ethereum"),
derivationPath = "m/44'/0'/0'/0/0",
),
addTokenButton = AddCustomTokenButtonUM.Visible(
isEnabled = false,
onClick = {},
),
),
PreviewAddCustomTokenComponent(
initialState = AddCustomTokenConfig(
userWalletId = UserWalletId(stringValue = "321"),
step = AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR,
popBack = {},
selectedDerivationPath = SelectedDerivationPath(
value = "m/44'/0'/0'/0/0",
name = stringReference("Ethereum"),
),
),
),

View file

@ -2,97 +2,152 @@ package com.tangem.features.managetokens.ui
import android.content.res.Configuration
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.components.isOpened
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenFormComponent
import com.tangem.features.managetokens.entity.ClickableFieldUM
import com.tangem.features.managetokens.entity.CustomTokenFormUM
import com.tangem.features.managetokens.entity.TextInputFieldUM
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.component.AddCustomTokenDescription
internal fun LazyListScope.customTokenFormContent(model: CustomTokenFormUM) {
item {
ClickableField(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
model = model.networkName,
)
}
@Composable
internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier = Modifier) {
val keyboard by keyboardAsState()
val bottomBarHeight by animateDpAsState(
label = "Bottom bar height",
targetValue = if (keyboard.isOpened) {
TangemTheme.dimens.spacing0
} else {
with(LocalDensity.current) {
WindowInsets.systemBars.getBottom(density = this).toDp()
}
},
)
Box(
modifier = modifier
.imePadding()
.fillMaxSize()
.background(color = TangemTheme.colors.background.secondary),
) {
val scrollState = rememberScrollState()
item {
Column(
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing12)
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
.verticalScroll(scrollState)
.fillMaxSize()
.padding(bottom = TangemTheme.dimens.spacing76),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
TextField(
model = model.contractAddress,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Next,
),
)
TextField(
model = model.tokenName,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Next,
),
)
TextField(
model = model.tokenSymbol,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Next,
),
)
TextField(
model = model.tokenDecimals,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Next,
),
AddCustomTokenDescription()
FormContent(model)
}
PrimaryButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = TangemTheme.dimens.spacing16 + bottomBarHeight)
.fillMaxWidth(),
text = stringResource(id = R.string.custom_token_add_token),
enabled = model.canAddToken,
onClick = model.saveToken,
)
}
}
@Composable
private fun FormContent(model: CustomTokenFormUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
ClickableField(
model = model.networkName,
)
val tokenForm = model.tokenForm
if (tokenForm != null) {
TokenForm(tokenForm)
}
ClickableField(
model = model.derivationPath,
)
model.notifications.fastForEach { notification ->
Notification(
config = notification.config,
containerColor = TangemTheme.colors.button.disabled,
)
}
}
}
item {
ClickableField(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
model = model.derivationPath,
@Composable
private fun TokenForm(tokenForm: CustomTokenFormUM.TokenFormUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
) {
TextField(
model = tokenForm.contractAddress,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Next,
),
)
}
items(
items = model.notifications,
key = { it.id },
) { notification ->
Notification(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
config = notification.config,
containerColor = TangemTheme.colors.button.disabled,
TextField(
model = tokenForm.name,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Next,
),
)
TextField(
model = tokenForm.symbol,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Next,
),
)
TextField(
model = tokenForm.decimals,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Next,
),
)
}
}
@ -124,7 +179,9 @@ private fun TextField(
},
content = {
SimpleTextField(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing12)
.fillMaxWidth(),
value = model.value,
onValueChange = model.onValueChange,
readOnly = false,
@ -170,9 +227,7 @@ private fun Preview_CustomTokenFormContent(
component: PreviewCustomTokenFormComponent,
) {
TangemThemePreview {
LazyColumn(
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
) { component.content(scope = this) }
component.Content(modifier = Modifier)
}
}
@ -183,14 +238,19 @@ private class PreviewCustomTokenFormComponentProvider :
get() = sequenceOf(
PreviewCustomTokenFormComponent(),
PreviewCustomTokenFormComponent(
contractAddress = TextInputFieldUM(
label = stringReference("Contract address"),
value = "0x1234567890",
error = stringReference("Contract address is invalid"),
placeholder = stringReference("0x1234567890"),
onValueChange = {},
tokenForm = PreviewCustomTokenFormComponent.tokenForm.copy(
contractAddress = TextInputFieldUM(
label = stringReference("Contract address"),
value = "0x1234567890",
error = stringReference("Contract address is invalid"),
placeholder = stringReference("0x1234567890"),
onValueChange = {},
),
),
),
PreviewCustomTokenFormComponent(
tokenForm = null,
),
)
}
// endregion Preview

View file

@ -1,158 +0,0 @@
package com.tangem.features.managetokens.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.painterResource
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.PreviewParameterProvider
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.rows.ChainRow
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenNetworkSelectorComponent
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
import com.tangem.features.managetokens.entity.CustomTokenNetworkSelectorUM
import com.tangem.features.managetokens.entity.SelectedNetworkUM
import com.tangem.features.managetokens.impl.R
internal fun LazyListScope.customTokenNetworkSelectorContent(model: CustomTokenNetworkSelectorUM) {
val lastIndex = model.networks.lastIndex
if (model.showTitle) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size36)
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.bottomSheet,
),
) {
Text(
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing6,
)
.padding(horizontal = TangemTheme.dimens.spacing12),
text = stringResource(R.string.add_custom_token_choose_network),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
itemsIndexed(
items = model.networks,
key = { _, item -> item.id.value },
) { index, item ->
NetworkItem(
modifier = Modifier
.fillMaxWidth()
.clip(
shape = when {
!model.showTitle && index == 0 -> RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
)
index == lastIndex -> RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
)
else -> RectangleShape
},
)
.background(color = TangemTheme.colors.background.primary)
.clickable(onClick = { item.onSelectedStateChange(true) })
.padding(horizontal = TangemTheme.dimens.spacing4),
model = item,
)
}
}
@Composable
private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) {
ChainRow(
modifier = modifier,
model = with(model) {
ChainRowUM(
name = name,
type = type,
icon = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = model.iconResId,
isGrayscale = false,
showCustomBadge = false,
),
showCustom = false,
)
},
action = {
AnimatedVisibility(
modifier = Modifier.size(TangemTheme.dimens.size24),
visible = model.isSelected,
) {
Icon(
painter = painterResource(id = R.drawable.ic_check_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
},
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_CustomTokenNetworkSelectorContent(
@PreviewParameter(CustomTokenNetworkSelectorComponentPreviewProvider::class)
component: CustomTokenNetworkSelectorComponent,
) {
TangemThemePreview {
LazyColumn {
component.content(this)
}
}
}
private class CustomTokenNetworkSelectorComponentPreviewProvider :
PreviewParameterProvider<CustomTokenNetworkSelectorComponent> {
override val values: Sequence<CustomTokenNetworkSelectorComponent>
get() = sequenceOf(
PreviewCustomTokenNetworkSelectorComponent(),
PreviewCustomTokenNetworkSelectorComponent(
params = CustomTokenNetworkSelectorComponent.Params(
userWalletId = UserWalletId(stringValue = "321"),
selectedNetwork = SelectedNetworkUM(
id = Network.ID(value = "0"),
name = "",
),
onNetworkSelected = {},
),
),
)
}
// endregion Preview

View file

@ -0,0 +1,255 @@
package com.tangem.features.managetokens.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
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.PreviewParameterProvider
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.rows.ChainRow
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
import com.tangem.features.managetokens.entity.item.DerivationPathUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.component.AddCustomTokenDescription
@Composable
internal fun CustomTokenSelectorContent(model: CustomTokenSelectorUM, modifier: Modifier = Modifier) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val lastIndex = model.items.lastIndex
LazyColumn(
modifier = modifier,
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing16 + bottomBarHeight,
),
) {
item {
Header(model.header)
}
itemsIndexed(
items = model.items,
key = { _, item -> item.id },
) { index, item ->
val itemModifier = Modifier
.fillMaxWidth()
.clip(
shape = when {
model.header !is CustomTokenSelectorUM.HeaderUM.Description && index == 0 -> {
RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
)
}
index == lastIndex -> {
RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
)
}
else -> {
RectangleShape
}
},
)
.background(color = TangemTheme.colors.background.primary)
.clickable(onClick = { item.onSelectedStateChange(true) })
.padding(horizontal = TangemTheme.dimens.spacing4)
when (item) {
is CurrencyNetworkUM -> {
NetworkItem(
modifier = itemModifier,
model = item,
)
}
is DerivationPathUM -> {
DerivationPathItem(
modifier = itemModifier,
model = item,
)
}
}
}
}
}
@Composable
private fun Header(header: CustomTokenSelectorUM.HeaderUM, modifier: Modifier = Modifier) {
when (header) {
is CustomTokenSelectorUM.HeaderUM.CustomDerivationButton -> {
CustomDerivationButton(
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing16),
onClick = header.onClick,
)
}
is CustomTokenSelectorUM.HeaderUM.Description -> {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AddCustomTokenDescription()
Box(
modifier = modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size36)
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.bottomSheet,
),
) {
Text(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing12),
text = stringResource(R.string.add_custom_token_choose_network),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
is CustomTokenSelectorUM.HeaderUM.None -> {
Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing16))
}
}
}
@Composable
private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) {
ChainRow(
modifier = modifier,
model = with(model) {
ChainRowUM(
name = name,
type = type,
icon = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = model.iconResId,
isGrayscale = !model.isSelected,
showCustomBadge = false,
),
showCustom = false,
)
},
action = {
AnimatedVisibility(
modifier = Modifier.size(TangemTheme.dimens.size24),
visible = model.isSelected,
) {
Icon(
painter = painterResource(id = R.drawable.ic_check_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
},
)
}
@Composable
private fun CustomDerivationButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
// TODO: Implement in [REDACTED_JIRA]
Box(
modifier = modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size56)
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
.background(color = TangemTheme.colors.background.primary)
.clickable(onClick = onClick)
.padding(all = TangemTheme.dimens.spacing12),
contentAlignment = Alignment.CenterStart,
) {
Text(
text = "Custom",
color = TangemTheme.colors.text.primary1,
)
}
}
@Composable
private fun DerivationPathItem(model: DerivationPathUM, modifier: Modifier = Modifier) {
// TODO: Implement in [REDACTED_JIRA]
Box(
modifier = modifier
.heightIn(TangemTheme.dimens.size56)
.padding(all = TangemTheme.dimens.spacing12),
contentAlignment = Alignment.CenterStart,
) {
Text(
text = "${model.value} ${if (model.isSelected) "- selected" else ""}",
color = TangemTheme.colors.text.primary1,
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_CustomTokenNetworkSelectorContent(
@PreviewParameter(CustomTokenNetworkSelectorComponentPreviewProvider::class)
component: CustomTokenSelectorComponent,
) {
TangemThemePreview {
component.Content(modifier = Modifier)
}
}
private class CustomTokenNetworkSelectorComponentPreviewProvider :
PreviewParameterProvider<CustomTokenSelectorComponent> {
override val values: Sequence<CustomTokenSelectorComponent>
get() = sequenceOf(
PreviewCustomTokenSelectorComponent(),
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = UserWalletId(stringValue = "321"),
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "0"),
name = stringReference(""),
derivationPath = "m/44'/0'/0'/0/0",
),
onNetworkSelected = {},
),
),
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.DerivationPathSelector(
userWalletId = UserWalletId(stringValue = "321"),
selectedDerivationPath = SelectedDerivationPath(
value = "m/44'/0'/0'/0/0",
name = stringReference(""),
),
onDerivationPathSelected = {},
),
),
)
}
// endregion Preview

View file

@ -1,15 +1,14 @@
package com.tangem.features.managetokens.ui
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.*
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
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.foundation.lazy.rememberLazyListState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
@ -20,6 +19,12 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
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.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@ -33,36 +38,51 @@ import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.list.InfiniteListHandler
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.components.rows.model.ChainRowUM
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.core.ui.utils.WindowInsetsZero
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.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import kotlinx.collections.immutable.ImmutableList
private const val CHEVRON_ROTATION_EXPANDED = 180f
private const val CHEVRON_ROTATION_COLLAPSED = 0f
private const val LOAD_ITEMS_BUFFER = 10
@Composable
internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) {
BackHandler(onBack = state.popBack)
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,
modifier = modifier.nestedScroll(nestedScrollConnection),
containerColor = TangemTheme.colors.background.primary,
contentWindowInsets = WindowInsetsZero,
topBar = {
ManageTokensTopBar(
modifier = Modifier.statusBarsPadding(),
topBar = state.topBar,
search = state.search,
)
},
content = { innerPadding ->
@ -70,10 +90,7 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
search = state.search,
items = state.items,
isLoading = state.isLoading,
hasChanges = state is ManageTokensUM.ManageContent && state.hasChanges,
state = state,
)
},
floatingActionButtonPosition = FabPosition.Center,
@ -81,10 +98,11 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
if (state is ManageTokensUM.ManageContent) {
SaveChangesButton(
modifier = Modifier
.navigationBarsPadding()
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
isVisible = state.hasChanges,
onClick = state.onSaveClick,
onClick = state.saveChanges,
)
}
},
@ -92,16 +110,26 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
}
@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
},
)
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
},
)
SearchBar(
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing16),
state = search,
)
}
}
@Composable
@ -122,80 +150,58 @@ private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier:
}
@Composable
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,
) {
private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) {
Box(modifier = modifier) {
Currencies(
modifier = Modifier.fillMaxSize(),
items = items,
search = search,
items = state.items,
showLoadingItem = state.isNextBatchLoading,
onLoadMore = state.loadMore,
isEditable = state is ManageTokensUM.ManageContent,
)
AnimatedVisibility(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(),
visible = hasChanges,
label = "bottom_fade_visibility",
) {
BottomFade()
}
BottomFade(modifier = Modifier.align(Alignment.BottomCenter))
}
Crossfade(targetState = isLoading, label = "ManageTokensLoadingContent") {
if (it) {
LoadingContent()
Crossfade(targetState = state.isInitialBatchLoading, label = "ManageTokensLoadingContent") { isVisible ->
if (isVisible) {
ProgressIndicator(
modifier = Modifier.fillMaxSize(),
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun Currencies(items: ImmutableList<CurrencyItemUM>, search: SearchBarUM, modifier: Modifier = Modifier) {
private fun Currencies(
items: ImmutableList<CurrencyItemUM>,
showLoadingItem: Boolean,
isEditable: Boolean,
onLoadMore: () -> Boolean,
modifier: Modifier = Modifier,
) {
val bottomBarHeight = with(LocalDensity.current) {
WindowInsets.systemBars.getBottom(density = this).toDp()
}
val listState = rememberLazyListState()
LazyColumn(
modifier = modifier,
state = listState,
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing76 + bottomBarHeight,
),
) {
stickyHeader(key = "search") {
Column(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.padding(
top = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing12,
)
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
) {
SearchBar(state = search)
}
}
items(
items = items,
key = CurrencyItemUM::id,
key = { it.id.value },
) { item ->
when (item) {
is CurrencyItemUM.Basic -> {
BasicCurrencyItem(
modifier = Modifier.fillMaxWidth(),
item = item,
isEditable = isEditable,
)
}
is CurrencyItemUM.Custom -> {
@ -206,6 +212,32 @@ private fun Currencies(items: ImmutableList<CurrencyItemUM>, search: SearchBarUM
}
}
}
if (showLoadingItem) {
item(key = "loading_item") {
ProgressIndicator(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
}
}
}
InfiniteListHandler(
listState = listState,
buffer = LOAD_ITEMS_BUFFER,
onLoadMore = onLoadMore,
)
}
@Composable
private fun ProgressIndicator(modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(color = TangemTheme.colors.background.primary),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(color = TangemTheme.colors.icon.informative)
}
}
@ -213,7 +245,14 @@ private fun Currencies(items: ImmutableList<CurrencyItemUM>, search: SearchBarUM
private fun CustomCurrencyItem(item: CurrencyItemUM.Custom, modifier: Modifier = Modifier) {
ChainRow(
modifier = modifier,
model = item.model,
model = with(item) {
ChainRowUM(
name = name,
type = symbol,
icon = icon,
showCustom = true,
)
},
action = {
SecondarySmallButton(
config = SmallButtonConfig(
@ -226,13 +265,20 @@ private fun CustomCurrencyItem(item: CurrencyItemUM.Custom, modifier: Modifier =
}
@Composable
private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = Modifier) {
private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, isEditable: Boolean, modifier: Modifier = Modifier) {
val isExpanded = item.networks is NetworksUM.Expanded
Column(modifier = modifier) {
ChainRow(
modifier = Modifier.clickable(onClick = item.onExpandClick),
model = item.model,
model = with(item) {
ChainRowUM(
name = name,
type = symbol,
icon = icon,
showCustom = false,
)
},
action = {
val rotation by animateFloatAsState(
targetValue = if (isExpanded) {
@ -260,13 +306,19 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = M
end = TangemTheme.dimens.spacing8,
),
networks = item.networks,
currencyId = item.id,
currencyId = item.id.value,
isEditable = isEditable,
)
}
}
@Composable
private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Modifier = Modifier) {
private fun NetworksList(
networks: NetworksUM,
currencyId: String,
isEditable: Boolean,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
modifier = modifier,
visible = networks is NetworksUM.Expanded,
@ -286,6 +338,7 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod
isLastItem = index == currentItems.lastIndex,
content = {
BlockchainRow(
modifier = Modifier.padding(end = TangemTheme.dimens.spacing8),
model = with(network) {
BlockchainRowUM(
name = name,
@ -296,10 +349,12 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod
)
},
action = {
TangemSwitch(
checked = network.isSelected,
onCheckedChange = network.onSelectedStateChange,
)
if (isEditable) {
TangemSwitch(
checked = network.isSelected,
onCheckedChange = network.onSelectedStateChange,
)
}
},
)
},

View file

@ -0,0 +1,21 @@
package com.tangem.features.managetokens.ui.component
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.managetokens.impl.R
@Composable
internal fun AddCustomTokenDescription(modifier: Modifier = Modifier) {
Text(
modifier = modifier.fillMaxWidth(fraction = 0.7f),
text = stringResource(id = R.string.custom_token_subtitle),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}

View file

@ -0,0 +1,30 @@
package com.tangem.features.managetokens.ui.dialog
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.impl.R
@Composable
internal fun HasLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network, onDismiss: () -> Unit) {
BasicDialog(
title = stringResource(
R.string.token_details_unable_hide_alert_title,
currency.name,
),
message = stringResource(
R.string.token_details_unable_hide_alert_message,
currency.name,
currency.symbol,
network.name,
),
confirmButton = DialogButtonUM(
title = stringResource(R.string.common_ok),
onClick = onDismiss,
),
onDismissDialog = onDismiss,
)
}

View file

@ -0,0 +1,29 @@
package com.tangem.features.managetokens.ui.dialog
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.features.managetokens.impl.R
@Composable
internal fun HideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit, onDismiss: () -> Unit) {
BasicDialog(
title = stringResource(
R.string.token_details_hide_alert_title,
currency.name,
),
message = stringResource(R.string.token_details_hide_alert_message),
confirmButton = DialogButtonUM(
title = stringResource(R.string.token_details_hide_alert_hide),
warning = true,
onClick = onConfirm,
),
dismissButton = DialogButtonUM(
title = stringResource(R.string.common_cancel),
onClick = onDismiss,
),
onDismissDialog = onDismiss,
)
}

View file

@ -0,0 +1,59 @@
package com.tangem.features.managetokens.utils.list
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.model.Network
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
internal typealias ChangedCurrencies = Map<ManagedCryptoCurrency.ID, Set<Network.ID>>
internal class ChangedCurrenciesManager {
val currenciesToAdd: MutableStateFlow<ChangedCurrencies> = MutableStateFlow(emptyMap())
val currenciesToRemove: MutableStateFlow<ChangedCurrencies> = MutableStateFlow(emptyMap())
fun addCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd)
}
fun removeCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove)
}
fun containsCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean {
return networkId in currenciesToAdd.value[currencyId].orEmpty() ||
networkId in currenciesToRemove.value[currencyId].orEmpty()
}
private fun updateChangedItems(
currencyId: ManagedCryptoCurrency.ID,
networkId: Network.ID,
removeFromIfPresent: MutableStateFlow<ChangedCurrencies>,
addToIfNotPresent: MutableStateFlow<ChangedCurrencies>,
) {
val present = removeFromIfPresent.value[currencyId].orEmpty()
if (networkId in present) {
removeFromIfPresent.update { items ->
items.toMutableMap().apply {
val ids = present - networkId
if (ids.isEmpty()) {
remove(currencyId)
} else {
set(currencyId, ids)
}
}
}
} else {
addToIfNotPresent.update { items ->
val alreadyAdded = items[currencyId] ?: emptySet()
if (networkId in alreadyAdded) {
return@update items
}
items + (currencyId to alreadyAdded + networkId)
}
}
}
}

View file

@ -0,0 +1,209 @@
package com.tangem.features.managetokens.utils.list
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.ui.UiMessageSender
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.managetokens.GetManagedTokensUseCase
import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext
import com.tangem.domain.managetokens.model.ManageTokensListConfig
import com.tangem.domain.managetokens.model.ManageTokensUpdateAction
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.CheckHasLinkedTokensUseCase
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.impl.R
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchListState
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject
@ComponentScoped
internal class ManageTokensListManager @Inject constructor(
private val getManagedTokensUseCase: GetManagedTokensUseCase,
private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase,
private val messageSender: UiMessageSender,
private val dispatchers: CoroutineDispatcherProvider,
) : ManageTokensUiActions {
private lateinit var scope: CoroutineScope
private val jobHolder = JobHolder()
private val actionsFlow: MutableSharedFlow<ManageTokensBatchAction> = MutableSharedFlow(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
private val state: MutableStateFlow<ManageTokensListState> = MutableStateFlow(ManageTokensListState())
private val changedCurrenciesManager = ChangedCurrenciesManager()
private val uiManager = ManageTokensUiManager(
state = state,
messageSender = messageSender,
dispatchers = dispatchers,
actions = this,
scopeProvider = Provider { scope },
)
val currenciesToAdd: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToAdd
val currenciesToRemove: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToRemove
@OptIn(ExperimentalCoroutinesApi::class)
val paginationStatus: Flow<PaginationStatus<*>> = state
.mapLatest { it.status }
.distinctUntilChanged()
val uiItems: Flow<ImmutableList<CurrencyItemUM>> = uiManager.items
suspend fun launchPagination(userWalletId: UserWalletId?) = coroutineScope {
scope = this
val batchFlow = getManagedTokensUseCase(
context = ManageTokensListBatchingContext(
actionsFlow = actionsFlow,
coroutineScope = this,
),
)
batchFlow.state
.onEach { state -> updateState(state, userWalletId) }
.flowOn(dispatchers.default)
.launchIn(scope = this)
.saveIn(jobHolder)
// Initial load
reload(userWalletId)
}
suspend fun reload(userWalletId: UserWalletId?) {
actionsFlow.emit(
BatchAction.Reload(
requestParams = ManageTokensListConfig(userWalletId, searchText = null),
),
)
}
suspend fun loadMore(userWalletId: UserWalletId?, query: String) {
actionsFlow.emit(
BatchAction.LoadMore(
requestParams = ManageTokensListConfig(userWalletId, query),
),
)
}
suspend fun search(userWalletId: UserWalletId?, query: String) {
state.value = ManageTokensListState()
actionsFlow.emit(
BatchAction.Reload(
requestParams = ManageTokensListConfig(
userWalletId = userWalletId,
searchText = query,
),
),
)
}
private fun updateState(
batchListState: BatchListState<Int, List<ManagedCryptoCurrency>>,
userWalletId: UserWalletId?,
) {
state.update { state ->
state.copy(
status = batchListState.status,
)
}
state.update { state ->
val newBatches = batchListState.data
val currentBatches = state.currencyBatches
// Distinct until changed
if (newBatches.size == currentBatches.size &&
newBatches.map { it.key } == currentBatches.map { it.key } &&
newBatches.flatMap { it.data } == currentBatches.flatMap { it.data }
) {
return
}
val canEditItems = userWalletId != null
state.copy(
userWalletId = userWalletId,
currencyBatches = newBatches,
uiBatches = uiManager.createOrUpdateUiBatches(newBatches, canEditItems),
canEditItems = canEditItems,
)
}
}
override fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
changedCurrenciesManager.addCurrency(currencyId, networkId)
sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = true)
}
override fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
changedCurrenciesManager.removeCurrency(currencyId, networkId)
sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = false)
}
override fun checkNeedToShowRemoveNetworkWarning(
currencyId: ManagedCryptoCurrency.ID,
networkId: Network.ID,
): Boolean = !changedCurrenciesManager.containsCurrency(currencyId, networkId)
private fun sendSelectCurrencyAction(
batchKey: Int,
currencyId: ManagedCryptoCurrency.ID,
networkId: Network.ID,
isSelected: Boolean,
) {
val request = ManageTokensUpdateAction.AddCurrency(
currencyId = currencyId,
networkId = networkId,
isSelected = isSelected,
)
val action = BatchAction.UpdateBatches(
keys = setOf(batchKey),
async = true,
updateRequest = request,
)
actionsFlow.tryEmit(action)
}
override suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean {
return checkHasLinkedTokensUseCase(userWalletId, network).getOrElse {
Timber.e(
it,
"""
Failed to check linked tokens
|- User wallet ID: $userWalletId
|- Network ID: ${network.id}
""".trimIndent(),
)
val message = SnackbarMessage(
message = it.localizedMessage
?.let(::stringReference)
?: resourceReference(R.string.common_error),
)
messageSender.send(message)
false
}
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.features.managetokens.utils.list
import com.tangem.domain.managetokens.model.ManageTokensListConfig
import com.tangem.domain.managetokens.model.ManageTokensUpdateAction
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.PaginationStatus
internal typealias ManageTokensBatchAction = BatchAction<Int, ManageTokensListConfig, ManageTokensUpdateAction>
internal data class ManageTokensListState(
val status: PaginationStatus<*> = PaginationStatus.None,
val userWalletId: UserWalletId? = null,
val uiBatches: List<Batch<Int, List<CurrencyItemUM>>> = mutableListOf(),
val currencyBatches: List<Batch<Int, List<ManagedCryptoCurrency>>> = mutableListOf(),
val canEditItems: Boolean = true,
) {
fun batchIndexByCurrencyId(currencyId: ManagedCryptoCurrency.ID): Int {
return currencyBatches
.indexOfFirst { batch -> batch.data.any { it.id == currencyId } }
.takeIf { it != -1 }
?: error("Batch with currency '$currencyId' not found")
}
fun updateUiBatchesItem(
indexToBatch: Pair<Int, Batch<Int, List<CurrencyItemUM>>>,
indexToItem: Pair<Int, CurrencyItemUM>,
): ManageTokensListState {
val updatedUiBatch = indexToBatch.second.copy(
data = indexToBatch.second.data.toMutableList().apply {
set(indexToItem.first, indexToItem.second)
},
)
return copy(
uiBatches = uiBatches.toMutableList().apply {
set(indexToBatch.first, updatedUiBatch)
},
)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.managetokens.utils.list
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
internal interface ManageTokensUiActions {
fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID)
fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID)
fun checkNeedToShowRemoveNetworkWarning(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean
suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean
}

View file

@ -0,0 +1,203 @@
package com.tangem.features.managetokens.utils.list
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.message.ContentMessage
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.ui.dialog.HasLinkedTokensWarning
import com.tangem.features.managetokens.ui.dialog.HideTokenWarning
import com.tangem.features.managetokens.utils.mapper.toUiModel
import com.tangem.features.managetokens.utils.ui.toggleExpanded
import com.tangem.features.managetokens.utils.ui.update
import com.tangem.pagination.Batch
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
internal class ManageTokensUiManager(
private val state: MutableStateFlow<ManageTokensListState>,
private val messageSender: UiMessageSender,
private val dispatchers: CoroutineDispatcherProvider,
private val scopeProvider: Provider<CoroutineScope>,
private val actions: ManageTokensUiActions,
) {
private val scope: CoroutineScope
get() = scopeProvider()
@OptIn(ExperimentalCoroutinesApi::class)
val items: Flow<ImmutableList<CurrencyItemUM>> = state
.mapLatest { state ->
state.uiBatches.asSequence()
.flatMap { it.data }
.toImmutableList()
}
.distinctUntilChanged()
fun createOrUpdateUiBatches(
newCurrencyBatches: List<Batch<Int, List<ManagedCryptoCurrency>>>,
canEditItems: Boolean,
): List<Batch<Int, List<CurrencyItemUM>>> {
val currentUiBatches = state.value.uiBatches
val batches = currentUiBatches.toMutableList()
newCurrencyBatches.forEach { (key, data) ->
val indexToUpdate = currentUiBatches.indexOfFirst { it.key == key }
if (indexToUpdate == -1) {
val newBatch = Batch(
key = key,
data = data.map { item ->
item.toUiModel(
isEditable = canEditItems,
onRemoveCustomCurrencyClick = ::removeCustomCurrency,
onExpandNetworksClick = ::toggleCurrencyNetworksVisibility,
)
},
)
batches.add(newBatch)
} else {
val uiBatchToUpdate = currentUiBatches[indexToUpdate]
if (uiBatchToUpdate.data == data) {
return@forEach
}
val currentCurrencyBatches = state.value.currencyBatches
val currencyBatch = currentCurrencyBatches[indexToUpdate]
val updatedBatch = uiBatchToUpdate.copy(
data = data.mapIndexed { index, item ->
if (item == currencyBatch.data[index]) {
return@mapIndexed uiBatchToUpdate.data[index]
}
val previousUiItem = uiBatchToUpdate.data.getOrNull(index)
if (previousUiItem == null || previousUiItem.id != item.id) {
item.toUiModel(
isEditable = canEditItems,
onRemoveCustomCurrencyClick = ::removeCustomCurrency,
onExpandNetworksClick = ::toggleCurrencyNetworksVisibility,
)
} else {
previousUiItem.update(item)
}
},
)
batches[indexToUpdate] = updatedBatch
}
}
return batches
}
private fun removeCustomCurrency(currency: ManagedCryptoCurrency.Custom) = scope.launch(dispatchers.default) {
showRemoveNetworkWarning(
currency = currency,
network = currency.network,
isCoin = currency is ManagedCryptoCurrency.Custom.Coin,
onConfirm = {
// TODO: [REDACTED_JIRA]
},
)
}
private fun toggleCurrencyNetworksVisibility(currency: ManagedCryptoCurrency.Token) = scope.launch(
dispatchers.default,
) {
state.update { batches ->
val batchIndex = batches.batchIndexByCurrencyId(currency.id)
val currencyBatch = batches.currencyBatches[batchIndex]
val currencyIndex = currencyBatch.currencyIndexById(currency.id)
val uiBatch = batches.uiBatches[batchIndex]
val updatedUiItem = uiBatch.data[currencyIndex].toggleExpanded(
currency = currencyBatch.data[currencyIndex],
isEditable = batches.canEditItems,
onSelectCurrencyNetwork = { networkId, isSelected ->
selectNetwork(currencyBatch.key, currency, networkId, isSelected)
},
)
batches.updateUiBatchesItem(
indexToBatch = batchIndex to uiBatch,
indexToItem = currencyIndex to updatedUiItem,
)
}
}
private fun selectNetwork(
batchKey: Int,
currency: ManagedCryptoCurrency,
source: ManagedCryptoCurrency.SourceNetwork,
isSelected: Boolean,
) = scope.launch(dispatchers.default) {
if (currency !is ManagedCryptoCurrency.Token) return@launch
if (isSelected) {
actions.addCurrency(batchKey, currency.id, source.id)
} else {
if (actions.checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) {
showRemoveNetworkWarning(
currency = currency,
network = source.network,
isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main,
onConfirm = {
actions.removeCurrency(batchKey, currency.id, source.id)
},
)
} else {
actions.removeCurrency(batchKey, currency.id, source.id)
}
}
}
private suspend fun showRemoveNetworkWarning(
currency: ManagedCryptoCurrency,
network: Network,
isCoin: Boolean,
onConfirm: () -> Unit,
) {
val userWalletId = state.value.userWalletId
val hasLinkedTokens = if (userWalletId == null || !isCoin) {
false
} else {
actions.checkHasLinkedTokens(userWalletId, network)
}
val message = ContentMessage { onDismiss ->
if (hasLinkedTokens) {
HasLinkedTokensWarning(
currency = currency,
network = network,
onDismiss = onDismiss,
)
} else {
HideTokenWarning(
currency = currency,
onConfirm = {
onConfirm()
onDismiss()
},
onDismiss = onDismiss,
)
}
}
messageSender.send(message)
}
private fun Batch<Int, List<ManagedCryptoCurrency>>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int {
return data
.indexOfFirst { it.id == id }
.takeIf { it != -1 }
?: error("Currency with currency '$id' not found in batch #$key")
}
}

View file

@ -0,0 +1,77 @@
package com.tangem.features.managetokens.utils.mapper
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.getTintForTokenIcon
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
import com.tangem.features.managetokens.utils.ui.getIconRes
internal fun ManagedCryptoCurrency.toUiModel(
isEditable: Boolean,
onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit,
onRemoveCustomCurrencyClick: (ManagedCryptoCurrency.Custom) -> Unit,
): CurrencyItemUM = when (this) {
is ManagedCryptoCurrency.Custom -> toUiModel(onRemoveCustomCurrencyClick)
is ManagedCryptoCurrency.Token -> toUiModel(isEditable, onExpandNetworksClick)
}
private fun ManagedCryptoCurrency.Custom.toUiModel(
onRemoveCustomCurrency: (ManagedCryptoCurrency.Custom) -> Unit,
): CurrencyItemUM = CurrencyItemUM.Custom(
id = id,
name = name,
symbol = symbol,
icon = when (this) {
is ManagedCryptoCurrency.Custom.Coin -> {
CurrencyIconState.CoinIcon(
url = iconUrl,
fallbackResId = network.id.getIconRes(isColored = true),
isGrayscale = false,
showCustomBadge = true,
)
}
is ManagedCryptoCurrency.Custom.Token -> {
val background = tryGetBackgroundForTokenIcon(contractAddress)
CurrencyIconState.TokenIcon(
url = iconUrl,
fallbackBackground = background,
fallbackTint = getTintForTokenIcon(background),
topBadgeIconResId = network.id.getIconRes(isColored = true),
isGrayscale = false,
showCustomBadge = true,
)
}
},
onRemoveClick = {
onRemoveCustomCurrency(this)
},
)
private fun ManagedCryptoCurrency.Token.toUiModel(
isEditable: Boolean,
onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit,
): CurrencyItemUM {
val background = TangemColorPalette.Black
return CurrencyItemUM.Basic(
id = id,
name = name,
symbol = symbol,
icon = CurrencyIconState.TokenIcon(
url = iconUrl,
topBadgeIconResId = null,
isGrayscale = if (isEditable) !isAdded else false,
showCustomBadge = false,
fallbackTint = getTintForTokenIcon(background),
fallbackBackground = background,
),
networks = NetworksUM.Collapsed,
onExpandClick = {
onExpandNetworksClick(this)
},
)
}

View file

@ -0,0 +1,46 @@
package com.tangem.features.managetokens.utils.mapper
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
import com.tangem.features.managetokens.utils.ui.getIconRes
import kotlinx.collections.immutable.toImmutableList
internal fun ManagedCryptoCurrency.Token.toUiNetworksModel(
isExpanded: Boolean,
isItemsEditable: Boolean,
onSelectedStateChange: (SourceNetwork, Boolean) -> Unit,
): NetworksUM {
return if (isExpanded) {
NetworksUM.Expanded(
networks = availableNetworks.map {
it.toUiModel(
isSelected = it.id in addedIn,
isEditable = isItemsEditable,
onSelectedStateChange = onSelectedStateChange,
)
}.toImmutableList(),
)
} else {
NetworksUM.Collapsed
}
}
private fun SourceNetwork.toUiModel(
isSelected: Boolean,
isEditable: Boolean,
onSelectedStateChange: (SourceNetwork, Boolean) -> Unit,
): CurrencyNetworkUM {
return CurrencyNetworkUM(
networkId = id,
name = network.name.uppercase(),
iconResId = id.getIconRes(isColored = isSelected || !isEditable),
isSelected = isSelected || !isEditable,
type = typeName,
isMainNetwork = this is SourceNetwork.Main,
onSelectedStateChange = { selected ->
onSelectedStateChange(this, selected)
},
)
}

View file

@ -0,0 +1,66 @@
package com.tangem.features.managetokens.utils.ui
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
import com.tangem.features.managetokens.utils.mapper.toUiNetworksModel
import kotlinx.collections.immutable.toImmutableList
internal fun CurrencyItemUM.toggleExpanded(
currency: ManagedCryptoCurrency,
isEditable: Boolean,
onSelectCurrencyNetwork: (SourceNetwork, Boolean) -> Unit,
): CurrencyItemUM {
if (currency !is ManagedCryptoCurrency.Token) return this
return when (this) {
is CurrencyItemUM.Custom -> this
is CurrencyItemUM.Basic -> {
val isExpanded = networks !is NetworksUM.Expanded
copy(
icon = icon.copySealed(
isGrayscale = if (isEditable) !currency.isAdded && !isExpanded else false,
),
networks = currency.toUiNetworksModel(
isExpanded = isExpanded,
isItemsEditable = isEditable,
onSelectedStateChange = onSelectCurrencyNetwork,
),
)
}
}
}
internal fun CurrencyItemUM.update(currency: ManagedCryptoCurrency): CurrencyItemUM {
return when (this) {
is CurrencyItemUM.Custom -> this
is CurrencyItemUM.Basic -> {
if (currency !is ManagedCryptoCurrency.Token) {
return this
}
copy(
icon = icon.copySealed(
isGrayscale = networks is NetworksUM.Collapsed && !currency.isAdded,
),
networks = updateNetworks(currency),
)
}
}
}
private fun CurrencyItemUM.Basic.updateNetworks(currency: ManagedCryptoCurrency.Token): NetworksUM = when (networks) {
is NetworksUM.Collapsed -> networks
is NetworksUM.Expanded -> networks.copy(
networks = networks.networks.map { network ->
val isSelected = network.networkId in currency.addedIn
network.copy(
iconResId = network.networkId.getIconRes(isSelected),
isSelected = isSelected,
)
}.toImmutableList(),
)
}

View file

@ -0,0 +1,21 @@
package com.tangem.features.managetokens.utils.ui
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.getGreyedOutIconRes
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
internal fun CurrencyNetworkUM.select(isSelected: Boolean): CurrencyNetworkUM {
return copy(
iconResId = networkId.getIconRes(isSelected),
isSelected = isSelected,
)
}
@DrawableRes
internal fun Network.ID.getIconRes(isColored: Boolean): Int = if (isColored) {
getActiveIconRes(value)
} else {
getGreyedOutIconRes(value)
}

View file

@ -1,6 +1,7 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.serialization)
id("kotlin-parcelize")
id("configuration")
}
@ -15,4 +16,10 @@ dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Project - Domain */
implementation(projects.domain.core)
implementation(projects.domain.tokens.models)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.markets.models)
}

View file

@ -1,21 +1,23 @@
package com.tangem.features.markets.details.api
package com.tangem.features.markets.details
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.markets.component.BottomSheetState
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.markets.entry.BottomSheetState
import kotlinx.serialization.Serializable
@Stable
interface MarketsTokenDetailsComponent {
interface MarketsTokenDetailsComponent : ComposableContentComponent {
@Serializable
data class Params(
val token: TokenMarketSerializable,
val token: TokenMarketParams,
val appCurrency: AppCurrency,
)
@ -26,7 +28,5 @@ interface MarketsTokenDetailsComponent {
modifier: Modifier,
)
interface Factory {
fun create(context: AppComponentContext, params: Params, onBack: () -> Unit): MarketsTokenDetailsComponent
}
interface Factory : ComponentFactory<Params, MarketsTokenDetailsComponent>
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.markets.component
package com.tangem.features.markets.entry
enum class BottomSheetState {
EXPANDED,

View file

@ -1,4 +1,4 @@
package com.tangem.features.markets.component
package com.tangem.features.markets.entry
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable

View file

@ -0,0 +1,22 @@
package com.tangem.features.markets.token.block
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import kotlinx.serialization.Serializable
@Stable
interface TokenMarketBlockComponent : ComposableContentComponent {
@Serializable
data class Params(
val tokenId: String,
val tokenName: String,
val tokenSymbol: String,
val tokenImageUrl: String?,
)
interface Factory {
fun create(appComponentContext: AppComponentContext, params: Params): TokenMarketBlockComponent
}
}

View file

@ -20,6 +20,8 @@ dependencies {
implementation(projects.domain.markets)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens.models)
/* Compose */
implementation(deps.compose.coil)
@ -46,6 +48,8 @@ dependencies {
implementation(projects.core.ui)
implementation(projects.core.featuretoggles)
/* Common */
implementation(projects.common.ui)
implementation(projects.common.uiCharts)
implementation(projects.common.routing)
}

View file

@ -1,164 +0,0 @@
package com.tangem.features.markets
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.*
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.value.Value
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.features.markets.component.BottomSheetState
import com.tangem.features.markets.component.MarketsEntryComponent
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.api.toSerializable
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Stable
internal class DefaultMarketsEntryComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
private val marketsEntryChildFactory: MarketsEntryChildFactory,
) : MarketsEntryComponent, AppComponentContext by context {
private val stackNavigation = StackNavigation<MarketsEntryChildFactory.Child>()
val stack: Value<ChildStack<MarketsEntryChildFactory.Child, Any>> = childStack(
key = "main",
source = stackNavigation,
serializer = MarketsEntryChildFactory.Child.serializer(),
initialConfiguration = MarketsEntryChildFactory.Child.TokenList,
handleBackButton = true,
childFactory = { configuration, componentContext ->
marketsEntryChildFactory.createChild(
child = configuration,
appComponentContext = childByContext(componentContext),
onTokenSelected = ::marketsListTokenSelected,
onDetailsBack = ::onDetailsBack,
)
},
)
@Suppress("LongMethod")
@Composable
override fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
) {
val primary = TangemTheme.colors.background.primary
val secondary = TangemTheme.colors.background.secondary
val backgroundColor = remember { Animatable(primary) }
val stackState = stack.subscribeAsState()
LocalMainBottomSheetColor.current.value = backgroundColor.value
Children(
stack = stackState.value,
animation = stackAnimation(slide()),
) {
when (it.configuration) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
(it.instance as MarketsTokenDetailsComponent).BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)
}
MarketsEntryChildFactory.Child.TokenList -> {
(it.instance as MarketsTokenListComponent).BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)
}
}
}
// order of LaunchedEffects is important here
val activeChild = stackState.value.active.configuration
LaunchedEffect(activeChild) {
when (activeChild) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.animateTo(
secondary,
animationSpec = tween(durationMillis = 500),
)
}
MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.animateTo(
primary,
animationSpec = tween(durationMillis = 500),
)
}
}
}
LaunchedEffect(bottomSheetState.value) {
if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) {
when (bottomSheetState.value) {
BottomSheetState.EXPANDED -> {
backgroundColor.animateTo(
secondary,
animationSpec = tween(durationMillis = 100),
)
}
BottomSheetState.COLLAPSED -> {
backgroundColor.animateTo(
primary,
animationSpec = tween(durationMillis = 100),
)
}
}
}
}
LaunchedEffect(primary, secondary) {
if (backgroundColor.isRunning) return@LaunchedEffect
when (activeChild) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.snapTo(secondary)
}
MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.snapTo(primary)
}
}
}
}
@OptIn(ExperimentalDecomposeApi::class)
private fun marketsListTokenSelected(token: TokenMarket, appCurrency: AppCurrency) {
stackNavigation.pushNew(
configuration = MarketsEntryChildFactory.Child.TokenDetails(
params = MarketsTokenDetailsComponent.Params(
token = token.toSerializable(),
appCurrency = appCurrency,
),
),
)
}
private fun onDetailsBack() {
stackNavigation.popWhile { it != MarketsEntryChildFactory.Child.TokenList }
}
@AssistedFactory
interface Factory : MarketsEntryComponent.Factory {
override fun create(context: AppComponentContext): DefaultMarketsEntryComponent
}
}

View file

@ -1,40 +0,0 @@
package com.tangem.features.markets.details.api
import com.tangem.domain.core.serialization.SerializedBigDecimal
import com.tangem.domain.markets.TokenMarket
import kotlinx.serialization.Serializable
@Serializable
data class TokenMarketSerializable(
val id: String,
val name: String,
val symbol: String,
val marketCap: SerializedBigDecimal?,
val tokenQuotes: Quotes,
val imageUrl: String,
) {
@Serializable
data class Quotes(
val currentPrice: SerializedBigDecimal,
val h24Percent: SerializedBigDecimal,
val weekPercent: SerializedBigDecimal,
val monthPercent: SerializedBigDecimal,
)
}
fun TokenMarket.toSerializable(): TokenMarketSerializable {
return TokenMarketSerializable(
id = id,
name = name,
symbol = symbol,
marketCap = marketCap,
tokenQuotes = TokenMarketSerializable.Quotes(
currentPrice = tokenQuotesShort.currentPrice,
h24Percent = tokenQuotesShort.h24ChangePercent,
weekPercent = tokenQuotesShort.weekChangePercent,
monthPercent = tokenQuotesShort.monthChangePercent,
),
imageUrl = imageUrlLarge,
)
}

View file

@ -6,24 +6,49 @@ import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.markets.component.BottomSheetState
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params
import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@Stable
internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: MarketsTokenDetailsComponent.Params,
@Assisted private val onBack: () -> Unit,
@Assisted params: Params,
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent {
private val model: MarketsTokenDetailsModel = getOrCreateModel(params)
private val portfolioComponent = portfolioComponentFactory.create(
context = child("my_portfolio"),
params = MarketsPortfolioComponent.Params(params.token.id),
)
init {
componentScope.launch {
model.networksState.collectLatest {
when (it) {
is TokenNetworksState.NetworksAvailable -> portfolioComponent.setTokenNetworks(it.networks)
TokenNetworksState.NoNetworksAvailable -> portfolioComponent.setNoNetworksAvailable()
else -> {}
}
}
}
}
@Composable
override fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
@ -41,23 +66,50 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
val bsState by bottomSheetState
LaunchedEffect(bsState) {
model.containerBottomSheetState.value = bsState
model.isVisibleOnScreen.value = bsState == BottomSheetState.EXPANDED
}
MarketsTokenDetailsContent(
state = state,
onBackClick = onBack,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
backgroundColor = LocalMainBottomSheetColor.current.value,
addTopBarStatusBarPadding = false,
state = state,
onBackClick = ::navigateBack,
onHeaderSizeChange = onHeaderSizeChange,
portfolioBlock = { blockModifier ->
portfolioComponent.Content(blockModifier)
},
)
}
@Composable
override fun Content(modifier: Modifier) {
LifecycleStartEffect(Unit) {
model.isVisibleOnScreen.value = true
onStopOrDispose {
model.isVisibleOnScreen.value = false
}
}
val state by model.state.collectAsStateWithLifecycle()
MarketsTokenDetailsContent(
modifier = modifier,
backgroundColor = TangemTheme.colors.background.tertiary,
addTopBarStatusBarPadding = true,
state = state,
onBackClick = ::navigateBack,
onHeaderSizeChange = {},
portfolioBlock = { blockModifier ->
portfolioComponent.Content(blockModifier)
},
)
}
private fun navigateBack() = router.pop()
@AssistedFactory
interface Factory : MarketsTokenDetailsComponent.Factory {
override fun create(
context: AppComponentContext,
params: MarketsTokenDetailsComponent.Params,
onBack: () -> Unit,
): DefaultMarketsTokenDetailsComponent
override fun create(context: AppComponentContext, params: Params): DefaultMarketsTokenDetailsComponent
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.features.markets.details.impl.di
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.impl.DefaultMarketsTokenDetailsComponent
import dagger.Binds
import dagger.Module

View file

@ -3,6 +3,7 @@ package com.tangem.features.markets.details.impl.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.ui.charts.state.*
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.navigation.url.UrlOpener
@ -17,14 +18,14 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.*
import com.tangem.features.markets.component.BottomSheetState
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter
import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter
import com.tangem.features.markets.details.impl.model.formatter.*
import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice
import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween
import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.impl.R
@ -43,13 +44,14 @@ import javax.inject.Inject
@Suppress("LargeClass", "LongParameterList")
@Stable
@ComponentScoped
internal class MarketsTokenDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
private val getTokenQuotesUseCase: GetTokenQuotesUseCase,
private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase,
private val urlOpener: UrlOpener,
) : Model() {
@ -115,8 +117,8 @@ internal class MarketsTokenDetailsModel @Inject constructor(
private var lastUpdatedTimestamp: Long = DateTime.now().millis
val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED)
val isVisibleOnScreen = MutableStateFlow(false)
val networksState = MutableStateFlow<TokenNetworksState>(TokenNetworksState.Loading)
val state = MutableStateFlow(
MarketsTokenDetailsUM(
@ -131,11 +133,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
percent = params.token.tokenQuotes.h24Percent,
useAbsoluteValue = true,
),
priceChangeType = if (params.token.tokenQuotes.h24Percent < BigDecimal.ZERO) {
PriceChangeType.DOWN
} else {
PriceChangeType.UP
},
priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(),
iconUrl = params.token.imageUrl,
chartState = MarketsTokenDetailsUM.ChartState(
dataProducer = chartDataProducer,
@ -180,7 +178,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
private fun loadQuotes() {
modelScope.launch {
val result = getTokenQuotesUseCase(
val result = getTokenFullQuotesUseCase(
tokenId = params.token.id,
appCurrency = currentAppCurrency.value,
)
@ -209,6 +207,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
appCurrency = currentAppCurrency.value,
interval = interval,
tokenId = params.token.id,
preview = false,
)
state.update {
@ -242,6 +241,11 @@ internal class MarketsTokenDetailsModel @Inject constructor(
chartState = it.chartState.copy(
status = MarketsTokenDetailsUM.ChartState.Status.DATA,
),
body = if (it.body is MarketsTokenDetailsUM.Body.Nothing) {
MarketsTokenDetailsUM.Body.Error(onLoadRetryClick = ::onLoadRetryClicked)
} else {
it.body
},
)
}
}.onLeft {
@ -292,6 +296,14 @@ internal class MarketsTokenDetailsModel @Inject constructor(
)
}
val networks = result.networks
networksState.value = if (networks.isNullOrEmpty()) {
TokenNetworksState.NoNetworksAvailable
} else {
TokenNetworksState.NetworksAvailable(networks)
}
chartDataProducer.runTransaction {
updateLook {
it.copy(
@ -476,7 +488,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
while (true) {
delay(timeMillis)
// Update quotes only when the container bottom sheet is in the expanded state
containerBottomSheetState.first { it == BottomSheetState.EXPANDED }
// and is visible on the screen
isVisibleOnScreen.first { it }

View file

@ -48,11 +48,13 @@ internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): Bi
}
}
@Suppress("MagicNumber")
internal fun BigDecimal?.percentChangeType(): PriceChangeType {
val scaled = this?.setScale(4, RoundingMode.HALF_UP)
return when {
this == null -> PriceChangeType.NEUTRAL
this > BigDecimal.ZERO -> PriceChangeType.UP
this < BigDecimal.ZERO -> PriceChangeType.DOWN
scaled == null -> PriceChangeType.NEUTRAL
scaled > BigDecimal.ZERO -> PriceChangeType.UP
scaled < BigDecimal.ZERO -> PriceChangeType.DOWN
else -> PriceChangeType.NEUTRAL
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.markets.details.impl.model.state
import com.tangem.domain.markets.TokenMarketInfo
internal sealed class TokenNetworksState {
data object Loading : TokenNetworksState()
data object NoNetworksAvailable : TokenNetworksState()
data class NetworksAvailable(val networks: List<TokenMarketInfo.Network>) : TokenNetworksState()
}

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
@ -33,7 +34,6 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.disableNestedScroll
@ -45,49 +45,59 @@ import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.persistentListOf
@Suppress("UnusedPrivateMember")
@Suppress("LongParameterList")
@Composable
internal fun MarketsTokenDetailsContent(
state: MarketsTokenDetailsUM,
backgroundColor: Color,
addTopBarStatusBarPadding: Boolean,
onBackClick: () -> Unit,
onHeaderSizeChange: (Dp) -> Unit,
portfolioBlock: @Composable (Modifier) -> Unit,
modifier: Modifier = Modifier,
) {
Content(
modifier = modifier,
backgroundColor = backgroundColor,
state = state,
onBackClick = onBackClick,
onHeaderSizeChange = onHeaderSizeChange,
portfolioBlock = portfolioBlock,
addTopBarStatusBarInsets = addTopBarStatusBarPadding,
)
InfoBottomSheet(config = state.infoBottomSheet)
}
@Suppress("UnusedPrivateMember")
@Suppress("LongParameterList")
@Composable
private fun Content(
state: MarketsTokenDetailsUM,
backgroundColor: Color,
addTopBarStatusBarInsets: Boolean,
onBackClick: () -> Unit,
onHeaderSizeChange: (Dp) -> Unit,
portfolioBlock: @Composable (Modifier) -> Unit,
modifier: Modifier = Modifier,
) {
val backgroundColor = LocalMainBottomSheetColor.current.value
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
Column(
modifier = modifier
.drawBehind { drawRect(backgroundColor) }
.let { if (addTopBarStatusBarInsets) it.statusBarsPadding() else it }
.fillMaxSize(),
) {
TangemTopAppBar(
modifier = Modifier.onGloballyPositioned {
if (it.size.height > 0) {
with(density) {
onHeaderSizeChange(it.size.height.toDp())
modifier = Modifier
.onGloballyPositioned {
if (it.size.height > 0) {
with(density) {
onHeaderSizeChange(it.size.height.toDp())
}
}
}
},
},
title = state.tokenName,
startButton = TopAppBarButtonUM.Back(onBackClick),
)
@ -122,6 +132,7 @@ private fun Content(
) {
MarketTokenDetailsChart(
modifier = Modifier.fillMaxWidth(),
backgroundColor = backgroundColor,
state = state.chartState,
)
}
@ -129,6 +140,7 @@ private fun Content(
tokenMarketDetailsBody(
state = state.body,
portfolioBlock = portfolioBlock,
)
}
}
@ -261,6 +273,7 @@ private fun Preview() {
TangemThemePreview {
Content(
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
addTopBarStatusBarInsets = false,
state = MarketsTokenDetailsUM(
tokenName = "Token Name",
priceText = "$0.00000000324",
@ -288,6 +301,8 @@ private fun Preview() {
),
onHeaderSizeChange = {},
onBackClick = {},
backgroundColor = TangemTheme.colors.background.tertiary,
portfolioBlock = {},
)
}
}

View file

@ -9,17 +9,21 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import com.tangem.common.ui.charts.MarketChart
import com.tangem.common.ui.charts.getMarketChartBottomAxisHeight
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.common.ui.charts.state.rememberMarketChartState
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
@Composable
internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) {
internal fun MarketTokenDetailsChart(
state: MarketsTokenDetailsUM.ChartState,
backgroundColor: Color,
modifier: Modifier = Modifier,
) {
val growingColor = TangemTheme.colors.icon.accent
val fallingColor = TangemTheme.colors.icon.warning
val neutralColor = TangemTheme.colors.icon.informative
@ -36,7 +40,6 @@ internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, mo
onMarkerShown = state.onMarkerPointSelected,
)
val backgroundColor = LocalMainBottomSheetColor.current.value
val bottomChartAxisHeight = getMarketChartBottomAxisHeight()
Box(modifier) {

View file

@ -11,16 +11,31 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
internal fun LazyListScope.tokenMarketDetailsBody(state: MarketsTokenDetailsUM.Body) {
internal fun LazyListScope.tokenMarketDetailsBody(
state: MarketsTokenDetailsUM.Body,
portfolioBlock: @Composable (Modifier) -> Unit,
) {
when (state) {
MarketsTokenDetailsUM.Body.Loading -> {
loading()
item("description-loading") {
DescriptionPlaceholder(modifier = Modifier.blockPaddings())
}
item(key = "portfolio") {
portfolioBlock(Modifier.blockPaddings())
}
loadingInfoBlocks()
}
is MarketsTokenDetailsUM.Body.Content -> {
if (state.description != null) {
description(state.description)
}
item(key = "portfolio") {
portfolioBlock(Modifier.blockPaddings())
}
infoBlocksList(state.infoBlocks)
}
is MarketsTokenDetailsUM.Body.Error -> {
@ -106,11 +121,7 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati
}
}
private fun LazyListScope.loading() {
item("description-loading") {
DescriptionPlaceholder(modifier = Modifier.blockPaddings())
}
private fun LazyListScope.loadingInfoBlocks() {
item("insights-loading") {
InsightsBlockPlaceholder(modifier = Modifier.blockPaddings())
}

View file

@ -13,7 +13,7 @@ import java.math.BigDecimal
internal data class MarketsTokenDetailsUM(
val tokenName: String,
val priceText: String,
val iconUrl: String,
val iconUrl: String?,
val dateTimeText: TextReference,
val priceChangePercentText: String,
val priceChangeType: PriceChangeType,

View file

@ -1,18 +0,0 @@
package com.tangem.features.markets.di
import com.tangem.features.markets.component.MarketsEntryComponent
import com.tangem.features.markets.DefaultMarketsEntryComponent
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 ComponentModule {
@Binds
@Singleton
fun bindMarketsListComponent(factory: DefaultMarketsEntryComponent.Factory): MarketsEntryComponent.Factory
}

View file

@ -0,0 +1,94 @@
package com.tangem.features.markets.entry.impl
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.value.Value
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.entry.MarketsEntryComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.domain.markets.toSerializableParam
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child
import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Stable
internal class DefaultMarketsEntryComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
private val marketsEntryChildFactory: MarketsEntryChildFactory,
) : MarketsEntryComponent, AppComponentContext by context {
private val stackNavigation = StackNavigation<Child>()
val stack: Value<ChildStack<Child, Any>> = childStack(
key = "main",
source = stackNavigation,
serializer = Child.serializer(),
initialConfiguration = Child.TokenList,
handleBackButton = true,
childFactory = { configuration, factoryContext ->
marketsEntryChildFactory.createChild(
child = configuration,
appComponentContext = childByContext(
componentContext = factoryContext,
router = createRouter(configuration),
),
onTokenSelected = ::marketsListTokenSelected,
)
},
)
@Suppress("LongMethod")
@Composable
override fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
) {
EntryBottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
stackState = stack.subscribeAsState(),
modifier = modifier,
)
}
@OptIn(ExperimentalDecomposeApi::class)
private fun marketsListTokenSelected(token: TokenMarket, appCurrency: AppCurrency) {
stackNavigation.pushNew(
configuration = Child.TokenDetails(
params = MarketsTokenDetailsComponent.Params(
token = token.toSerializableParam(),
appCurrency = appCurrency,
),
),
)
}
private fun AppComponentContext.createRouter(child: Child): Router {
return when (child) {
is Child.TokenDetails -> {
MarketTokenDetailsRouter(
contextRouter = this.router,
stackNavigation = stackNavigation,
)
}
else -> this.router
}
}
@AssistedFactory
interface Factory : MarketsEntryComponent.Factory {
override fun create(context: AppComponentContext): DefaultMarketsEntryComponent
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.features.markets.entry.impl
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.popWhile
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.decompose.navigation.Router
import kotlin.reflect.KClass
internal class MarketTokenDetailsRouter(
private val contextRouter: Router,
private val stackNavigation: StackNavigation<MarketsEntryChildFactory.Child>,
) : Router by contextRouter {
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
stackNavigation.popWhile({ it != MarketsEntryChildFactory.Child.TokenList }, onComplete)
}
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
/** Not allowed */
}
override fun popTo(routeClass: KClass<out Route>, onComplete: (isSuccess: Boolean) -> Unit) {
/** Not allowed */
}
}

View file

@ -1,10 +1,11 @@
package com.tangem.features.markets
package com.tangem.features.markets.entry.impl
import androidx.compose.runtime.Immutable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
import kotlinx.serialization.Serializable
import javax.inject.Inject
@ -31,14 +32,12 @@ internal class MarketsEntryChildFactory @Inject constructor(
child: Child,
appComponentContext: AppComponentContext,
onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
onDetailsBack: () -> Unit,
): Any {
return when (child) {
is Child.TokenDetails -> {
tokenDetailsComponentFactory.create(
context = appComponentContext,
params = child.params,
onBack = onDetailsBack,
)
}
is Child.TokenList -> {

View file

@ -0,0 +1,18 @@
package com.tangem.features.markets.entry.impl.di
import com.tangem.features.markets.entry.MarketsEntryComponent
import com.tangem.features.markets.entry.impl.DefaultMarketsEntryComponent
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 ComponentModule {
@Binds
@Singleton
fun bindMarketsEntryComponent(factory: DefaultMarketsEntryComponent.Factory): MarketsEntryComponent.Factory
}

View file

@ -0,0 +1,128 @@
package com.tangem.features.markets.entry.impl.ui
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector4D
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
@Composable
internal fun EntryBottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
stackState: State<ChildStack<MarketsEntryChildFactory.Child, Any>>,
modifier: Modifier = Modifier,
) {
val primary = TangemTheme.colors.background.primary
val secondary = TangemTheme.colors.background.secondary
val backgroundColor = remember { Animatable(primary) }
LocalMainBottomSheetColor.current.value = backgroundColor.value
Children(
stack = stackState.value,
animation = stackAnimation(slide()),
) {
when (it.configuration) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
(it.instance as MarketsTokenDetailsComponent).BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)
}
MarketsEntryChildFactory.Child.TokenList -> {
(it.instance as MarketsTokenListComponent).BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)
}
}
}
val activeChild = stackState.value.active.configuration
BackgroundColorEffects(
activeChild = activeChild,
backgroundColor = backgroundColor,
bottomSheetState = bottomSheetState,
)
}
@Composable
private fun BackgroundColorEffects(
activeChild: MarketsEntryChildFactory.Child,
backgroundColor: Animatable<Color, AnimationVector4D>,
bottomSheetState: State<BottomSheetState>,
) {
val primary = TangemTheme.colors.background.primary
val secondary = TangemTheme.colors.background.secondary
// Order of LaunchedEffects is important here
LaunchedEffect(activeChild) {
when (activeChild) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.animateTo(
secondary,
animationSpec = tween(durationMillis = 500),
)
}
MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.animateTo(
primary,
animationSpec = tween(durationMillis = 500),
)
}
}
}
LaunchedEffect(bottomSheetState.value) {
if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) {
when (bottomSheetState.value) {
BottomSheetState.EXPANDED -> {
backgroundColor.animateTo(
secondary,
animationSpec = tween(durationMillis = 100),
)
}
BottomSheetState.COLLAPSED -> {
backgroundColor.animateTo(
primary,
animationSpec = tween(durationMillis = 100),
)
}
}
}
}
LaunchedEffect(primary, secondary) {
if (backgroundColor.isRunning) return@LaunchedEffect
when (activeChild) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.snapTo(secondary)
}
MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.snapTo(primary)
}
}
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.markets.portfolio.api
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.markets.TokenMarketInfo
import kotlinx.serialization.Serializable
@Stable
interface MarketsPortfolioComponent : ComposableContentComponent {
@Serializable
data class Params(val tokenId: String)
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>)
fun setNoNetworksAvailable()
interface Factory : ComponentFactory<Params, MarketsPortfolioComponent>
}

View file

@ -0,0 +1,43 @@
package com.tangem.features.markets.portfolio.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel
import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Stable
internal class DefaultMarketsPortfolioComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: MarketsPortfolioComponent.Params,
) : AppComponentContext by context, MarketsPortfolioComponent {
private val model: MarketsPortfolioModel = getOrCreateModel(params)
override fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) = model.setTokenNetworks(networks)
override fun setNoNetworksAvailable() = model.setNoNetworksAvailable()
@Composable
override fun Content(modifier: Modifier) {
MyPortfolio(
modifier = modifier,
state = MyPortfolioUM.Loading,
)
}
@AssistedFactory
interface Factory : MarketsPortfolioComponent.Factory {
override fun create(
context: AppComponentContext,
params: MarketsPortfolioComponent.Params,
): DefaultMarketsPortfolioComponent
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.markets.portfolio.impl.di
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.markets.portfolio.impl.DefaultMarketsPortfolioComponent
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 ComponentModule {
@Binds
@Singleton
fun bindMarketsPortfolioComponent(
factory: DefaultMarketsPortfolioComponent.Factory,
): MarketsPortfolioComponent.Factory
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.markets.portfolio.impl.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(MarketsPortfolioModel::class)
fun provideMarketsPortfolioModel(model: MarketsPortfolioModel): Model
}

View file

@ -0,0 +1,30 @@
package com.tangem.features.markets.portfolio.impl.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject
@Stable
@ComponentScoped
internal class MarketsPortfolioModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
@Suppress("UnusedPrivateMember")
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>()
@Suppress("UnusedPrivateMember")
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
// TODO [REDACTED_TASK_KEY]
}
fun setNoNetworksAvailable() {
// TODO [REDACTED_TASK_KEY]
}
}

View file

@ -0,0 +1,362 @@
package com.tangem.features.markets.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.currency.icon.CoinIcon
import com.tangem.core.ui.components.rows.ArrowRow
import com.tangem.core.ui.components.rows.BlockchainRow
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
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.markets.impl.R
import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider
import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM
import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM
import kotlinx.coroutines.delay
@Composable
internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<AddToPortfolioBSContentUM>(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
addBottomInsets = false,
titleText = resourceReference(R.string.markets_add_to_portfolio_button),
) {
Content(
modifier = Modifier.fillMaxWidth(),
state = config.content as AddToPortfolioBSContentUM,
)
}
}
@Composable
private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) {
var continueButtonAreaHeight by remember { mutableIntStateOf(0) }
val density = LocalDensity.current
val scrollState = rememberScrollState()
Box(modifier = modifier) {
Column(
modifier = Modifier
.verticalScroll(state = scrollState)
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
UserWalletItem(
state = state.selectedWallet,
blockColors = TangemBlockCardColors.copy(
containerColor = TangemTheme.colors.background.action,
disabledContainerColor = TangemTheme.colors.background.action,
),
)
SpacerH12()
NetworkSelection(
modifier = Modifier.fillMaxWidth(),
state = state.selectNetworkUM,
)
SpacerH12()
AnimatedVisibility(
visible = state.isScanCardNotificationVisible,
modifier = Modifier.fillMaxWidth(),
) {
Column {
ScanWalletWarning(modifier = Modifier.fillMaxWidth())
SpacerH12()
}
// Scroll to the bottom when the notification appears and the scroll is at the bottom
LaunchedEffect(Unit) {
if (scrollState.canScrollForward.not()) {
delay(timeMillis = 500)
scrollState.animateScrollTo(scrollState.maxValue)
}
}
}
SpacerH(with(density) { continueButtonAreaHeight.toDp() })
}
AnimatedVisibility(
visible = scrollState.canScrollForward,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier.align(Alignment.BottomCenter),
) {
BottomFade(Modifier.align(Alignment.BottomCenter))
}
ContinueButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.onGloballyPositioned {
continueButtonAreaHeight = it.size.height
},
enabled = state.continueButtonEnabled,
onClick = state.onContinueButtonClick,
)
}
}
@Composable
private fun ContinueButton(enabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
TangemButton(
enabled = enabled,
modifier = modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
)
.navigationBarsPadding()
.fillMaxWidth(),
text = stringResource(R.string.common_continue),
icon = if (enabled) {
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
} else {
TangemButtonIconPosition.None
},
showProgress = false,
size = TangemButtonSize.Default,
colors = TangemButtonsDefaults.primaryButtonColors,
onClick = onClick,
animateContentChange = true,
)
}
@Suppress("LongMethod")
@Composable
private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
title = {
Text(
text = stringResource(R.string.markets_select_network),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
) {
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing14),
verticalAlignment = Alignment.CenterVertically,
) {
CoinIcon(
modifier = Modifier.size(TangemTheme.dimens.size36),
url = state.iconUrl,
alpha = 1f,
colorFilter = null,
fallbackResId = R.drawable.ic_custom_token_44,
)
SpacerW12()
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.alignByBaseline(),
text = state.tokenName,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
SpacerW6()
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.alignByBaseline(),
text = state.tokenCurrencySymbol,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
)
}
state.networks.fastForEachIndexed { index, network ->
ArrowRow(
isLastItem = index == state.networks.lastIndex,
content = {
BlockchainRow(
modifier = Modifier.padding(
end = TangemTheme.dimens.spacing4,
),
model = with(network) {
BlockchainRowUM(
name = name,
type = type,
iconResId = iconResId,
isMainNetwork = isMainNetwork,
isSelected = isSelected,
)
},
action = {
TangemSwitch(
checked = network.isSelected,
onCheckedChange = {
state.onNetworkSwitchClick(network, it)
},
)
},
)
},
)
}
}
}
}
@Composable
private fun ScanWalletWarning(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.background(
color = TangemTheme.colors.button.disabled,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10),
) {
Icon(
modifier = Modifier.requiredSize(TangemTheme.dimens.size20),
imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
Text(
text = stringResource(R.string.markets_generate_addresses_notification),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview(
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
) {
TangemThemePreview {
AddToPortfolioBottomSheet(
config = TangemBottomSheetConfig(
isShow = true,
content = content,
onDismissRequest = {},
),
)
}
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun PreviewContent(
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
) {
TangemThemePreview {
Content(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.fillMaxWidth(),
state = content,
)
}
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun PreviewContentRtl(
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
) {
TangemThemePreview(rtl = true) {
Content(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.fillMaxWidth(),
state = content,
)
}
}
// For on device testing
@Composable
@Preview
private fun PreviewContentTestOnDevice(
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
) {
TangemThemePreview(
alwaysShowBottomSheets = false,
) {
var isShow by remember { mutableStateOf(false) }
var contentState by remember {
mutableStateOf(content)
}
LaunchedEffect(Unit) {
contentState = content.copy(
onContinueButtonClick = {
contentState = contentState.copy(
isScanCardNotificationVisible = !contentState.isScanCardNotificationVisible,
)
},
continueButtonEnabled = true,
selectedWallet = content.selectedWallet.copy(
onClick = {
contentState = contentState.copy(
continueButtonEnabled = !contentState.continueButtonEnabled,
)
},
),
)
}
AddToPortfolioBottomSheet(
config = TangemBottomSheetConfig(
isShow = isShow,
content = contentState,
onDismissRequest = { isShow = false },
),
)
Button(
onClick = { isShow = !isShow },
) {
Text(text = "Toggle")
}
}
}

View file

@ -0,0 +1,174 @@
package com.tangem.features.markets.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SmallButtonShimmer
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
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.markets.impl.R
import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
@Composable
internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
contentHorizontalPadding = 0.dp,
title = {
Text(
text = stringResource(R.string.markets_common_my_portfolio),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
action = {
if (state !is MyPortfolioUM.Tokens) return@InformationBlock
when (state.buttonState) {
MyPortfolioUM.Tokens.AddButtonState.Loading -> {
SmallButtonShimmer(
modifier = Modifier.size(width = 63.dp, height = TangemTheme.dimens.size18),
shape = RoundedCornerShape(TangemTheme.dimens.radius3),
)
}
else -> {
SecondarySmallButton(
config = SmallButtonConfig(
text = resourceReference(R.string.markets_add_token),
icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24),
onClick = state.onAddClick,
enabled = state.buttonState == MyPortfolioUM.Tokens.AddButtonState.Available,
),
)
}
}
},
) {
when (state) {
is MyPortfolioUM.Tokens -> TokenList(state = state)
is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state)
MyPortfolioUM.Loading -> LoadingPlaceholder()
MyPortfolioUM.Unavailable -> UnavailableContent()
}
}
}
@Composable
private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier) {
Column(modifier) {
state.tokens.fastForEachIndexed { index, token ->
PortfolioItem(
state = token,
lastInList = index == state.tokens.size - 1,
)
}
}
}
@Composable
private fun UnavailableContent(modifier: Modifier = Modifier) {
Text(
modifier = modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
text = stringResource(R.string.markets_add_to_my_portfolio_unavailable_description),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
@Composable
private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Text(
text = "To start buying, exchanging or receiving this asset, add this token to at least 1 network",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.markets_add_to_portfolio_button),
onClick = state.onAddClick,
)
}
}
@Composable
private fun LoadingPlaceholder(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
) {
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
TextShimmer(
modifier = Modifier.fillMaxWidth(fraction = 0.7f),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) {
TangemThemePreview {
Box(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing8),
) {
MyPortfolio(state)
}
}
}
@Preview
@Composable
private fun PreviewRtl(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) {
TangemThemePreview(rtl = true) {
Box(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing8),
) {
MyPortfolio(state)
}
}
}

View file

@ -0,0 +1,112 @@
package com.tangem.features.markets.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
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.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState
@Composable
internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) {
Column(modifier) {
TokenItem(state = state.tokenItemState, isBalanceHidden = state.isBalanceHidden)
PortfolioQuickActions(
modifier = Modifier.padding(
bottom = if (lastInList) {
TangemTheme.dimens.spacing12
} else {
TangemTheme.dimens.spacing24
},
),
isVisible = state.isQuickActionsShown,
onActionClick = state.onQuickActionClick,
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview(@PreviewParameter(PortfolioTokenUMProvider::class) tokenUM: PortfolioTokenUM) {
TangemThemePreview {
var quickActionsShown by remember { mutableStateOf(value = false) }
val onItemClick = {
quickActionsShown = quickActionsShown.not()
}
PortfolioItem(
state = tokenUM.copy(
tokenItemState = when (tokenUM.tokenItemState) {
is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = onItemClick)
is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = onItemClick)
else -> tokenUM.tokenItemState
},
isQuickActionsShown = quickActionsShown,
),
lastInList = true,
)
}
}
private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider<PortfolioTokenUM>(
collection = listOf(
tokenUM.copy(
tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy(
fiatAmountState = contentFiatAmount.copy(hasStaked = true),
),
),
tokenUM.copy(
tokenItemState = tokenUM.tokenItemState.copy(
fiatAmountState = contentFiatAmount.copy(text = DASH_SIGN),
cryptoAmountState = tokenUM.tokenItemState.cryptoAmountState.copy(text = DASH_SIGN),
),
),
tokenUM.copy(isBalanceHidden = true),
tokenUM.copy(
tokenItemState = TokenItemState.Unreachable(
id = tokenUM.tokenItemState.id,
iconState = tokenUM.tokenItemState.iconState,
titleState = tokenUM.tokenItemState.titleState,
subtitleState = tokenUM.tokenItemState.subtitleState,
onItemClick = {},
onItemLongClick = {},
),
),
tokenUM.copy(
tokenItemState = TokenItemState.NoAddress(
id = tokenUM.tokenItemState.id,
iconState = tokenUM.tokenItemState.iconState,
titleState = tokenUM.tokenItemState.titleState,
subtitleState = tokenUM.tokenItemState.subtitleState,
onItemLongClick = {},
),
),
tokenUM.copy(
tokenItemState = TokenItemState.Loading(
id = tokenUM.tokenItemState.id,
iconState = tokenUM.tokenItemState.iconState,
titleState = tokenUM.tokenItemState.titleState as TokenItemState.TitleState.Content,
subtitleState = tokenUM.tokenItemState.subtitleState,
),
),
),
) {
companion object {
val tokenUM = PreviewMyPortfolioUMProvider().sampleToken
val contentFiatAmount = tokenUM.tokenItemState.fiatAmountState as TokenFiatAmountState.Content
}
}

View file

@ -0,0 +1,214 @@
package com.tangem.features.markets.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM
@Composable
internal fun PortfolioQuickActions(
isVisible: Boolean,
onActionClick: (QuickActionUM) -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = isVisible,
enter = expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top),
) {
Column(modifier = modifier) {
LineSeparator()
QuickActionItem(
state = QuickActionUM.Buy,
onClick = { onActionClick(QuickActionUM.Buy) },
)
LineSeparator()
QuickActionItem(
state = QuickActionUM.Exchange,
onClick = { onActionClick(QuickActionUM.Exchange) },
)
LineSeparator()
QuickActionItem(
state = QuickActionUM.Receive,
onClick = { onActionClick(QuickActionUM.Receive) },
)
}
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) {
val lineColor = TangemTheme.colors.stroke.primary
val strokeWidth = TangemTheme.dimens.size1
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
val verticalPadding = TangemTheme.dimens.spacing2
val startPadding = TangemTheme.dimens.spacing28
val height = TangemTheme.dimens.size16 + verticalPadding * 2
Canvas(
modifier = modifier
.animateEnterExit(
enter = expandVertically(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
),
expandFrom = Alignment.Top,
) + fadeIn(),
exit = shrinkVertically(
spring(
stiffness = Spring.StiffnessLow,
),
shrinkTowards = Alignment.Top,
) + fadeOut(),
)
.fillMaxWidth()
.height(height),
) {
val x = if (isLtr) startPadding.toPx() else size.width - startPadding.toPx()
drawLine(
color = lineColor,
start = Offset(x, verticalPadding.toPx()),
end = Offset(x, size.height - verticalPadding.toPx()),
strokeWidth = strokeWidth.toPx(),
)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun AnimatedVisibilityScope.QuickActionItem(
state: QuickActionUM,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val hapticManager = LocalHapticManager.current
Row(
modifier = modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersMedium)
.clickable {
hapticManager.perform(TangemHapticEffect.View.SegmentTick)
onClick()
}
.padding(
vertical = TangemTheme.dimens.spacing2,
horizontal = TangemTheme.dimens.spacing12,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18),
) {
Box(
Modifier
.animateEnterExit(
enter = scaleIn(),
exit = scaleOut(),
)
.background(
color = TangemTheme.colors.button.secondary,
shape = CircleShape,
)
.size(TangemTheme.dimens.size32),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier
.requiredSize(TangemTheme.dimens.size16),
imageVector = ImageVector.vectorResource(id = state.icon),
contentDescription = null,
tint = TangemTheme.colors.button.primary,
)
}
Column(
modifier = Modifier
.animateEnterExit(
enter = fadeIn(),
exit = fadeOut(),
),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
) {
Text(
text = state.title.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = state.description.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
var isVisible by remember { mutableStateOf(true) }
Column(
modifier = Modifier
.fillMaxWidth()
.height(680.dp),
) {
Button(
onClick = { isVisible = !isVisible },
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
) {
Text(text = "Toggle")
}
SpacerH4()
Box(
modifier = Modifier.background(color = TangemTheme.colors.background.action),
) {
PortfolioQuickActions(
isVisible = isVisible,
onActionClick = {},
)
}
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewRtl() {
TangemThemePreview(rtl = true) {
Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) {
PortfolioQuickActions(
isVisible = true,
onActionClick = {},
)
}
}
}

View file

@ -0,0 +1,90 @@
package com.tangem.features.markets.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
import com.tangem.core.ui.components.inputrow.InputRowChecked
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.rows.CornersToRound
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContent
import kotlinx.collections.immutable.toImmutableList
@Composable
fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<TokenActionsBSContent>(
config = config,
title = { content ->
TangemBottomSheetTitle(content.title)
},
containerColor = TangemTheme.colors.background.tertiary,
content = { Content(it) },
)
}
@Composable
private fun Content(content: TokenActionsBSContent) {
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
content.actions.forEachIndexed { index, action ->
val cornersToRound = when (index) {
0 -> CornersToRound.TOP_2
content.actions.lastIndex -> CornersToRound.BOTTOM_2
else -> CornersToRound.ZERO
}
DividerContainer(
modifier = Modifier
.clip(cornersToRound.getShape())
.background(TangemTheme.colors.background.action)
.clickable { content.onActionClick(action) },
showDivider = index != content.actions.lastIndex,
) {
InputRowChecked(
text = action.text,
checked = false,
)
}
}
}
}
@Preview(widthDp = 360, heightDp = 640)
@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview(
alwaysShowBottomSheets = true,
) {
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
TokenActionsBottomSheet(
TangemBottomSheetConfig(
isShow = true,
onDismissRequest = {},
content = TokenActionsBSContent(
title = "Wallet 1",
actions = TokenActionsBSContent.Action.entries.toImmutableList(),
onActionClick = {},
),
),
)
}
}
}

View file

@ -0,0 +1,86 @@
package com.tangem.features.markets.portfolio.impl.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM
import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM
import kotlinx.collections.immutable.persistentListOf
internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider<AddToPortfolioBSContentUM> {
private val blockchainRow = BlockchainRowUM(
name = "Etherium 3",
type = "TEST",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = false,
isSelected = false,
)
override val values: Sequence<AddToPortfolioBSContentUM>
get() = sequenceOf(
AddToPortfolioBSContentUM(
selectedWallet = UserWalletItemUM(
id = UserWalletId("1"),
name = stringReference("Wallet 1"),
information = stringReference("3 cards, 10,123$"),
imageUrl = "",
isEnabled = true,
endIcon = UserWalletItemUM.EndIcon.Arrow,
onClick = {},
),
selectNetworkUM = SelectNetworkUM(
tokenId = "etherium",
tokenName = "Etherium",
tokenCurrencySymbol = "ETH",
networks = persistentListOf(
blockchainRow.copy(
type = "MAIN",
isMainNetwork = true,
isSelected = true,
),
blockchainRow,
blockchainRow,
),
onNetworkSwitchClick = { _, _ -> },
iconUrl = null,
),
isScanCardNotificationVisible = true,
continueButtonEnabled = true,
onContinueButtonClick = {},
),
AddToPortfolioBSContentUM(
selectedWallet = UserWalletItemUM(
id = UserWalletId("1"),
name = stringReference("Wallet 1"),
information = stringReference("3 cards, 10,123$"),
imageUrl = "",
isEnabled = true,
endIcon = UserWalletItemUM.EndIcon.Arrow,
onClick = {},
),
selectNetworkUM = SelectNetworkUM(
tokenId = "etherium",
tokenName = "Etherium",
tokenCurrencySymbol = "ETH",
networks = persistentListOf(
blockchainRow.copy(
type = "MAIN",
isMainNetwork = true,
isSelected = true,
),
*Array(25) { blockchainRow },
),
onNetworkSwitchClick = { _, _ -> },
iconUrl = null,
),
isScanCardNotificationVisible = true,
continueButtonEnabled = false,
onContinueButtonClick = {},
),
)
}

View file

@ -0,0 +1,51 @@
package com.tangem.features.markets.portfolio.impl.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM
import kotlinx.collections.immutable.persistentListOf
internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfolioUM> {
override val values: Sequence<MyPortfolioUM>
get() = sequenceOf(
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken, sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Available,
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable,
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading,
onAddClick = {},
),
MyPortfolioUM.AddFirstToken(
onAddClick = {},
),
MyPortfolioUM.Loading,
MyPortfolioUM.Unavailable,
)
val sampleToken = PortfolioTokenUM(
tokenItemState = TokenItemState.Content(
id = "",
iconState = CurrencyIconState.Locked,
titleState = TokenItemState.TitleState.Content(text = "My wallet"),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "486,65 \$"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "733,71097 MATIC"),
subtitleState = TokenItemState.SubtitleState.TextContent(value = "XRP Ledger token"),
onItemClick = {},
onItemLongClick = {},
),
isQuickActionsShown = false,
onQuickActionClick = {},
isBalanceHidden = false,
)
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
internal data class AddToPortfolioBSContentUM(
val selectedWallet: UserWalletItemUM,
val selectNetworkUM: SelectNetworkUM,
val isScanCardNotificationVisible: Boolean,
val continueButtonEnabled: Boolean,
val onContinueButtonClick: () -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,29 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class MyPortfolioUM {
data class Tokens(
val tokens: ImmutableList<PortfolioTokenUM>,
val buttonState: AddButtonState,
val onAddClick: () -> Unit,
) : MyPortfolioUM() {
enum class AddButtonState {
Loading,
Available,
Unavailable,
}
}
data class AddFirstToken(
val onAddClick: () -> Unit,
) : MyPortfolioUM()
data object Loading : MyPortfolioUM()
data object Unavailable : MyPortfolioUM()
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import com.tangem.core.ui.components.token.state.TokenItemState
internal data class PortfolioTokenUM(
val tokenItemState: TokenItemState,
val isBalanceHidden: Boolean,
val isQuickActionsShown: Boolean,
val onQuickActionClick: (QuickActionUM) -> Unit,
)

View file

@ -0,0 +1,30 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.markets.impl.R
@Immutable
internal enum class QuickActionUM(
val title: TextReference,
val description: TextReference,
@DrawableRes val icon: Int,
) {
Buy(
title = resourceReference(R.string.common_buy),
description = resourceReference(R.string.buy_token_description),
icon = R.drawable.ic_plus_24,
),
Exchange(
title = resourceReference(R.string.common_exchange),
description = resourceReference(R.string.exсhange_token_description),
icon = R.drawable.ic_exchange_vertical_24,
),
Receive(
title = resourceReference(R.string.common_receive),
description = resourceReference(R.string.receive_token_description),
icon = R.drawable.ic_arrow_down_24,
),
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import kotlinx.collections.immutable.ImmutableList
internal data class SelectNetworkUM(
val tokenId: String,
val iconUrl: String?,
val tokenName: String,
val tokenCurrencySymbol: String,
val networks: ImmutableList<BlockchainRowUM>,
val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit,
)

View file

@ -0,0 +1,28 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.ImmutableList
internal data class TokenActionsBSContent(
val title: String,
val actions: ImmutableList<Action>,
val onActionClick: (Action) -> Unit,
) : TangemBottomSheetConfigContent {
@Immutable
enum class Action(
val text: TextReference,
) {
CopyAddress(text = resourceReference(R.string.common_copy_address)),
Receive(text = resourceReference(R.string.common_receive)),
Sell(text = resourceReference(R.string.common_sell)),
Buy(text = resourceReference(R.string.common_buy)),
Send(text = resourceReference(R.string.common_send)),
Exchange(text = resourceReference(R.string.common_exchange)),
Stake(text = resourceReference(R.string.common_stake)),
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.features.markets.token.block.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
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.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.markets.token.block.TokenMarketBlockComponent.Params
import com.tangem.features.markets.token.block.impl.model.TokenMarketBlockModel
import com.tangem.features.markets.token.block.impl.ui.TokenMarketBlock
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Stable
internal class DefaultTokenMarketBlockComponent @AssistedInject constructor(
@Assisted componentContext: AppComponentContext,
@Assisted params: Params,
) : TokenMarketBlockComponent, AppComponentContext by componentContext {
private val model: TokenMarketBlockModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
TokenMarketBlock(
modifier = modifier,
state = state,
)
}
@AssistedFactory
interface Factory : TokenMarketBlockComponent.Factory {
override fun create(appComponentContext: AppComponentContext, params: Params): DefaultTokenMarketBlockComponent
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.markets.token.block.impl.di
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.markets.token.block.impl.DefaultTokenMarketBlockComponent
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 ComponentModule {
@Binds
@Singleton
fun bindMarketsPortfolioComponent(
factory: DefaultTokenMarketBlockComponent.Factory,
): TokenMarketBlockComponent.Factory
}

Some files were not shown because too many files have changed in this diff Show more