Updated on 2026-08-14
This commit is contained in:
commit
43c376489a
316 changed files with 7586 additions and 3131 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 $"),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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() },
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
@ -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) { "" }),
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.component
|
||||
package com.tangem.features.markets.entry
|
||||
|
||||
enum class BottomSheetState {
|
||||
EXPANDED,
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
|
@ -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 -> {
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue