Updated on 2026-08-14
This commit is contained in:
commit
afff7892bb
721 changed files with 24513 additions and 7275 deletions
|
|
@ -23,8 +23,10 @@ dependencies {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ internal class PreviewDetailsComponent : DetailsComponent {
|
|||
private val previewBlocks = runBlocking {
|
||||
ItemsBuilder(
|
||||
router = DummyRouter(),
|
||||
urlOpener = DummyUrlOpener(),
|
||||
).buildAll(isWalletConnectAvailable = true, onSupportClick = {})
|
||||
).buildAll(isWalletConnectAvailable = true, onSupportClick = {}, onBuyClick = {})
|
||||
}
|
||||
|
||||
private val previewFooter = DetailsFooterUM(
|
||||
|
|
|
|||
|
|
@ -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,10 +1,12 @@
|
|||
package com.tangem.features.details.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.AppInstanceIdProvider
|
||||
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.navigation.url.UrlOpener
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
|
||||
|
|
@ -35,6 +37,8 @@ internal class DetailsModel @Inject constructor(
|
|||
private val appVersionProvider: AppVersionProvider,
|
||||
private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase,
|
||||
private val router: Router,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val appInstanceIdProvider: AppInstanceIdProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val appStateHolder: ReduxStateHolder,
|
||||
|
|
@ -74,6 +78,7 @@ internal class DetailsModel @Inject constructor(
|
|||
items.value = itemsBuilder.buildAll(
|
||||
isWalletConnectAvailable = isWalletConnectAvailable,
|
||||
onSupportClick = ::sendFeedback,
|
||||
onBuyClick = ::onBuyClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +91,12 @@ internal class DetailsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onBuyClick() {
|
||||
modelScope.launch {
|
||||
urlOpener.openUrl(buildBuyLink())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateState(items: ImmutableList<DetailsItemUM>) {
|
||||
state.update { prevState ->
|
||||
prevState.copy(items = items)
|
||||
|
|
@ -93,4 +104,14 @@ internal class DetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getAppVersion(): String = "${appVersionProvider.versionName} (${appVersionProvider.versionCode})"
|
||||
|
||||
private suspend fun buildBuyLink(): String {
|
||||
return appInstanceIdProvider.getAppInstanceId()?.let {
|
||||
"$BUY_TANGEM_URL&app_instance_id=$it"
|
||||
} ?: BUY_TANGEM_URL
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val BUY_TANGEM_URL = "https://buy.tangem.com/?utm_source=tangem&utm_medium=app"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -27,6 +28,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
|
|||
import com.tangem.core.ui.res.LocalSnackbarHostState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.TestTags
|
||||
import com.tangem.features.details.component.preview.PreviewDetailsComponent
|
||||
import com.tangem.features.details.entity.DetailsFooterUM
|
||||
import com.tangem.features.details.entity.DetailsItemUM
|
||||
|
|
@ -74,7 +76,7 @@ private fun Content(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
modifier = modifier.testTag(TestTags.DETAILS_SCREEN),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
contentPadding = PaddingValues(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
|
|
@ -125,7 +127,7 @@ private fun Block(
|
|||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
) {
|
||||
val itemModifier = Modifier.fillMaxWidth()
|
||||
val itemModifier = Modifier.fillMaxWidth().testTag(TestTags.DETAILS_SCREEN_ITEM)
|
||||
|
||||
when (model) {
|
||||
is DetailsItemUM.Basic -> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.features.details.utils
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.components.block.model.BlockUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
|
|
@ -16,19 +15,19 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class ItemsBuilder @Inject constructor(
|
||||
private val router: Router,
|
||||
private val urlOpener: UrlOpener,
|
||||
) {
|
||||
internal class ItemsBuilder @Inject constructor(private val router: Router) {
|
||||
|
||||
fun buildAll(isWalletConnectAvailable: Boolean, onSupportClick: () -> Unit): ImmutableList<DetailsItemUM> =
|
||||
buildList {
|
||||
buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add)
|
||||
buildUserWalletListBlock().let(::add)
|
||||
buildShopBlock().let(::add)
|
||||
buildSettingsBlock().let(::add)
|
||||
buildSupportBlock(onSupportClick).let(::add)
|
||||
}.toImmutableList()
|
||||
fun buildAll(
|
||||
isWalletConnectAvailable: Boolean,
|
||||
onSupportClick: () -> Unit,
|
||||
onBuyClick: () -> Unit,
|
||||
): ImmutableList<DetailsItemUM> = buildList {
|
||||
buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add)
|
||||
buildUserWalletListBlock().let(::add)
|
||||
buildShopBlock(onBuyClick).let(::add)
|
||||
buildSettingsBlock().let(::add)
|
||||
buildSupportBlock(onSupportClick).let(::add)
|
||||
}.toImmutableList()
|
||||
|
||||
private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? {
|
||||
return if (isWalletConnectAvailable) {
|
||||
|
|
@ -42,7 +41,7 @@ internal class ItemsBuilder @Inject constructor(
|
|||
|
||||
private fun buildUserWalletListBlock(): DetailsItemUM = DetailsItemUM.UserWalletList
|
||||
|
||||
private fun buildShopBlock(): DetailsItemUM = DetailsItemUM.Basic(
|
||||
private fun buildShopBlock(onBuyClick: () -> Unit): DetailsItemUM = DetailsItemUM.Basic(
|
||||
id = "shop",
|
||||
items = persistentListOf(
|
||||
DetailsItemUM.Basic.Item(
|
||||
|
|
@ -50,7 +49,7 @@ internal class ItemsBuilder @Inject constructor(
|
|||
block = BlockUM(
|
||||
text = resourceReference(R.string.details_buy_wallet),
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
onClick = { urlOpener.openUrl(BUY_TANGEM_URL) },
|
||||
onClick = onBuyClick,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -102,8 +101,4 @@ internal class ItemsBuilder @Inject constructor(
|
|||
),
|
||||
),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val BUY_TANGEM_URL = "https://buy.tangem.com/?utm_source=tangem&utm_medium=app"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
package com.tangem.features.details.utils
|
||||
|
||||
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.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
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal fun List<UserWallet>.toUiModels(
|
||||
onClick: (UserWalletId) -> Unit,
|
||||
appCurrency: AppCurrency? = null,
|
||||
balances: Map<UserWalletId, TotalFiatBalance> = emptyMap(),
|
||||
isLoading: Boolean = true,
|
||||
isBalancesHidden: Boolean = false,
|
||||
): ImmutableList<UserWalletUM> = this.map { model ->
|
||||
val balance = balances[model.walletId]
|
||||
|
||||
model.toUiModel(
|
||||
balance = balance,
|
||||
appCurrency = appCurrency,
|
||||
isLoading = isLoading,
|
||||
isBalanceHidden = isBalancesHidden,
|
||||
onClick = { onClick(model.walletId) },
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
private fun UserWallet.toUiModel(
|
||||
balance: TotalFiatBalance?,
|
||||
appCurrency: AppCurrency?,
|
||||
isLoading: Boolean,
|
||||
isBalanceHidden: Boolean,
|
||||
onClick: () -> Unit,
|
||||
): UserWalletUM = UserWalletUM(
|
||||
id = walletId,
|
||||
name = stringReference(name),
|
||||
information = getInfo(
|
||||
appCurrency = appCurrency,
|
||||
balance = balance,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
isLoading = isLoading,
|
||||
),
|
||||
imageUrl = artworkUrl,
|
||||
isEnabled = !isLocked,
|
||||
onClick = onClick,
|
||||
)
|
||||
|
||||
private fun UserWallet.getInfo(
|
||||
appCurrency: AppCurrency?,
|
||||
balance: TotalFiatBalance?,
|
||||
isBalanceHidden: Boolean,
|
||||
isLoading: Boolean,
|
||||
): TextReference {
|
||||
val dividerRef = stringReference(value = " • ")
|
||||
|
||||
val cardCount = getCardCount()
|
||||
val cardCountRef = TextReference.PluralRes(
|
||||
id = R.plurals.card_label_card_count,
|
||||
count = cardCount,
|
||||
formatArgs = wrappedList(cardCount),
|
||||
)
|
||||
|
||||
return when {
|
||||
isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS))
|
||||
isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked))
|
||||
isLoading -> cardCountRef
|
||||
else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBalanceInfo(
|
||||
balance: TotalFiatBalance?,
|
||||
appCurrency: AppCurrency?,
|
||||
cardCountRef: TextReference,
|
||||
dividerRef: TextReference,
|
||||
): TextReference {
|
||||
val amount = when (balance) {
|
||||
is TotalFiatBalance.Loaded -> balance.amount
|
||||
is TotalFiatBalance.Failed,
|
||||
is TotalFiatBalance.Loading,
|
||||
null,
|
||||
-> null
|
||||
}
|
||||
|
||||
return if (amount != null && appCurrency != null) {
|
||||
val formattedAmount = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = amount,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
val amountRef = stringReference(formattedAmount)
|
||||
combinedReference(cardCountRef, dividerRef, amountRef)
|
||||
} 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,8 @@ package com.tangem.features.details.utils
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter
|
||||
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,9 +24,9 @@ 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.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
|
|
@ -40,8 +42,11 @@ internal class UserWalletsFetcher @Inject constructor(
|
|||
) {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val userWallets: Flow<ImmutableList<UserWalletUM>> = getWalletsUseCase().transformLatest { wallets ->
|
||||
emit(wallets.toUiModels(onClick = ::navigateToWalletSettings))
|
||||
val userWallets: Flow<ImmutableList<UserWalletItemUM>> = getWalletsUseCase().transformLatest { wallets ->
|
||||
val uiModels = UserWalletItemUMConverter(onClick = ::navigateToWalletSettings).convertList(wallets)
|
||||
.toImmutableList()
|
||||
|
||||
emit(uiModels)
|
||||
|
||||
combine(
|
||||
getSelectedAppCurrencyUseCase().distinctUntilChanged(),
|
||||
|
|
@ -72,23 +77,33 @@ 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() },
|
||||
block = {
|
||||
maybeBalances.bindOrNull().orEmpty()
|
||||
.filterKeys { userWalletId -> wallets.any { it.walletId == userWalletId } }
|
||||
.mapKeys { entry -> wallets.first { it.walletId == entry.key } }
|
||||
},
|
||||
)
|
||||
|
||||
val appCurrency = withError(
|
||||
transform = { Error.UnableToGetAppCurrency },
|
||||
block = { maybeAppCurrency.toLce().bind() },
|
||||
)
|
||||
|
||||
wallets.toUiModels(
|
||||
appCurrency = appCurrency,
|
||||
balances = balances,
|
||||
onClick = ::navigateToWalletSettings,
|
||||
isBalancesHidden = balanceHidingSettings.isBalanceHidden,
|
||||
isLoading = maybeBalances.isLoading(),
|
||||
)
|
||||
balances
|
||||
.map { (userWallet, balance) ->
|
||||
UserWalletItemUMConverter(
|
||||
onClick = ::navigateToWalletSettings,
|
||||
appCurrency = appCurrency,
|
||||
balance = balance,
|
||||
isBalanceHidden = balanceHidingSettings.isBalanceHidden,
|
||||
isLoading = maybeBalances.isLoading(),
|
||||
)
|
||||
.convert(userWallet)
|
||||
}
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
private fun navigateToWalletSettings(userWalletId: UserWalletId) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
|
@ -34,6 +35,8 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.TestTags.DISCLAIMER_SCREEN_ACCEPT_BUTTON
|
||||
import com.tangem.core.ui.test.TestTags.DISCLAIMER_SCREEN_CONTAINER
|
||||
import com.tangem.features.disclaimer.impl.R
|
||||
import com.tangem.features.disclaimer.impl.entity.DisclaimerUM
|
||||
import com.tangem.features.disclaimer.impl.entity.DummyDisclaimer
|
||||
|
|
@ -57,7 +60,8 @@ internal fun DisclaimerScreen(state: DisclaimerUM) {
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.background(backgroundColor)
|
||||
.statusBarsPadding(),
|
||||
.statusBarsPadding()
|
||||
.testTag(DISCLAIMER_SCREEN_CONTAINER),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -158,6 +162,7 @@ private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) {
|
|||
disabledContentColor = TangemColorPalette.Dark6,
|
||||
),
|
||||
modifier = Modifier
|
||||
.testTag(DISCLAIMER_SCREEN_ACCEPT_BUTTON)
|
||||
.align(Alignment.BottomCenter)
|
||||
.navigationBarsPadding()
|
||||
.padding(
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
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,
|
||||
val onCurrencyAdded: () -> 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,11 @@ dependencies {
|
|||
implementation(projects.core.featuretoggles)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/* AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
|
@ -43,5 +46,6 @@ dependencies {
|
|||
|
||||
/* Other */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.timber)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.managetokens.component
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableDialogComponent
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
|
||||
|
||||
internal interface CustomTokenDerivationInputComponent : ComposableDialogComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val onConfirm: (SelectedDerivationPath) -> Unit,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, CustomTokenDerivationInputComponent>
|
||||
}
|
||||
|
|
@ -1,19 +1,23 @@
|
|||
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,
|
||||
val onCurrencyAdded: () -> 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,28 @@
|
|||
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 selectedNetwork: SelectedNetwork,
|
||||
val selectedDerivationPath: SelectedDerivationPath?,
|
||||
val onDerivationPathSelected: (SelectedDerivationPath) -> Unit,
|
||||
) : Params()
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, CustomTokenSelectorComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
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.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.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,
|
||||
selectedNetwork = requireNotNull(config.selectedNetwork) {
|
||||
"Network is not selected"
|
||||
},
|
||||
selectedDerivationPath = config.selectedDerivationPath,
|
||||
onDerivationPathSelected = { derivationPath ->
|
||||
showForm(derivationPath = derivationPath)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
AddCustomTokenConfig.Step.FORM -> {
|
||||
formComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = CustomTokenFormComponent.Params(
|
||||
userWalletId = config.userWalletId,
|
||||
network = requireNotNull(config.selectedNetwork) {
|
||||
"Network is not selected"
|
||||
},
|
||||
derivationPath = config.selectedDerivationPath,
|
||||
formValues = config.formValues,
|
||||
onSelectNetworkClick = ::showNetworkSelector,
|
||||
onSelectDerivationPathClick = ::showDerivationPathSelector,
|
||||
onCurrencyAdded = ::dismissAndNotify,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private fun dismissAndNotify() {
|
||||
dismiss()
|
||||
params.onCurrencyAdded()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : AddCustomTokenComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: AddCustomTokenComponent.Params,
|
||||
): DefaultAddCustomTokenComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
package com.tangem.features.managetokens.component.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.managetokens.ValidateDerivationPathUseCase
|
||||
import com.tangem.domain.managetokens.model.exceptoin.DerivationPathValidationException
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomDerivationInputUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.ui.dialog.CustomDerivationInputDialog
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal class DefaultCustomTokenDerivationInputComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: CustomTokenDerivationInputComponent.Params,
|
||||
private val validateDerivationPathUseCase: ValidateDerivationPathUseCase,
|
||||
) : CustomTokenDerivationInputComponent, AppComponentContext by context {
|
||||
|
||||
private val state: MutableStateFlow<CustomDerivationInputUM> = MutableStateFlow(
|
||||
value = getInitialState(),
|
||||
)
|
||||
|
||||
init {
|
||||
observeValueUpdates()
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
params.onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Dialog() {
|
||||
val state by state.collectAsStateWithLifecycle()
|
||||
|
||||
CustomDerivationInputDialog(
|
||||
model = state,
|
||||
onDismiss = ::dismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun observeValueUpdates() {
|
||||
state
|
||||
.map { it.value.text }
|
||||
.distinctUntilChanged()
|
||||
.sample(periodMillis = 1_000)
|
||||
.onEach(::validateValue)
|
||||
.launchIn(componentScope)
|
||||
}
|
||||
|
||||
private fun getInitialState(): CustomDerivationInputUM = CustomDerivationInputUM(
|
||||
value = TextFieldValue(),
|
||||
error = null,
|
||||
updateValue = ::updateValue,
|
||||
isConfirmEnabled = false,
|
||||
onConfirm = ::confirm,
|
||||
)
|
||||
|
||||
private fun validateValue(value: String) {
|
||||
validateDerivationPathUseCase(value).getOrElse { e ->
|
||||
updateWithValidationError(e)
|
||||
return
|
||||
}
|
||||
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
error = null,
|
||||
isConfirmEnabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWithValidationError(e: DerivationPathValidationException) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
error = when (e) {
|
||||
DerivationPathValidationException.Empty -> null
|
||||
DerivationPathValidationException.Invalid -> {
|
||||
resourceReference(R.string.custom_token_invalid_derivation_path)
|
||||
}
|
||||
},
|
||||
isConfirmEnabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateValue(value: TextFieldValue) {
|
||||
state.update { state ->
|
||||
state.copy(value = value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirm() {
|
||||
if (state.value.error != null && !state.value.isConfirmEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
val value = state.value.value.text
|
||||
val model = SelectedDerivationPath(
|
||||
id = null,
|
||||
value = Network.DerivationPath.Custom(value),
|
||||
networkName = stringReference(value = value),
|
||||
)
|
||||
|
||||
params.onConfirm(model)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : CustomTokenDerivationInputComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: CustomTokenDerivationInputComponent.Params,
|
||||
): DefaultCustomTokenDerivationInputComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.managetokens.component.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.managetokens.component.CustomTokenFormComponent
|
||||
import com.tangem.features.managetokens.model.CustomTokenFormModel
|
||||
import com.tangem.features.managetokens.ui.CustomTokenFormContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultCustomTokenFormComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: CustomTokenFormComponent.Params,
|
||||
) : CustomTokenFormComponent, AppComponentContext by context {
|
||||
|
||||
private val model: CustomTokenFormModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
CustomTokenFormContent(
|
||||
modifier = modifier,
|
||||
model = state,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : CustomTokenFormComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: CustomTokenFormComponent.Params,
|
||||
): DefaultCustomTokenFormComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
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.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.ComposableDialogComponent
|
||||
import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent
|
||||
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorDialogConfig
|
||||
import com.tangem.features.managetokens.model.CustomTokenSelectorModel
|
||||
import com.tangem.features.managetokens.ui.CustomTokenSelectorContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultCustomTokenSelectorComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted params: CustomTokenSelectorComponent.Params,
|
||||
private val customTokenDerivationInputComponentFactory: CustomTokenDerivationInputComponent.Factory,
|
||||
) : CustomTokenSelectorComponent, AppComponentContext by context {
|
||||
|
||||
private val model: CustomTokenSelectorModel = getOrCreateModel(params)
|
||||
private val dialogSlot = childSlot(
|
||||
source = model.dialogNavigation,
|
||||
serializer = CustomTokenSelectorDialogConfig.serializer(),
|
||||
childFactory = ::createDialog,
|
||||
)
|
||||
|
||||
private fun createDialog(
|
||||
config: CustomTokenSelectorDialogConfig,
|
||||
context: ComponentContext,
|
||||
): ComposableDialogComponent = when (config) {
|
||||
is CustomTokenSelectorDialogConfig.CustomDerivationInput -> customTokenDerivationInputComponentFactory.create(
|
||||
context = childByContext(context),
|
||||
params = CustomTokenDerivationInputComponent.Params(
|
||||
userWalletId = config.userWalletId,
|
||||
onConfirm = model::selectCustomDerivationPath,
|
||||
onDismiss = model.dialogNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val dialog by dialogSlot.subscribeAsState()
|
||||
|
||||
CustomTokenSelectorContent(
|
||||
modifier = modifier,
|
||||
model = state,
|
||||
)
|
||||
|
||||
dialog.child?.instance?.Dialog()
|
||||
}
|
||||
|
||||
@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.ManageTokensBottomSheetConfig
|
||||
import com.tangem.features.managetokens.model.ManageTokensModel
|
||||
import com.tangem.features.managetokens.ui.ManageTokensScreen
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -16,18 +25,47 @@ 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 = ManageTokensBottomSheetConfig.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: ManageTokensBottomSheetConfig,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = when (config) {
|
||||
is ManageTokensBottomSheetConfig.AddCustomToken -> {
|
||||
addCustomTokenComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddCustomTokenComponent.Params(
|
||||
userWalletId = config.userWalletId,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
onCurrencyAdded = model::reloadList,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -4,81 +4,73 @@ 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,
|
||||
selectedNetwork = state.selectedNetwork!!,
|
||||
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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.managetokens.component.preview
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomDerivationInputUM
|
||||
import com.tangem.features.managetokens.ui.dialog.CustomDerivationInputDialog
|
||||
|
||||
internal class PreviewCustomTokenDerivationInputComponent(
|
||||
private val value: String = "",
|
||||
private val error: String? = null,
|
||||
) : CustomTokenDerivationInputComponent {
|
||||
|
||||
override fun dismiss() {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Dialog() {
|
||||
val model = CustomDerivationInputUM(
|
||||
value = TextFieldValue(value),
|
||||
error = error?.let(::stringReference),
|
||||
updateValue = {},
|
||||
isConfirmEnabled = false,
|
||||
onConfirm = {},
|
||||
)
|
||||
|
||||
CustomDerivationInputDialog(
|
||||
model = model,
|
||||
onDismiss = ::dismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 kotlinx.collections.immutable.ImmutableList
|
||||
import com.tangem.features.managetokens.ui.CustomTokenFormContent
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
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: PersistentList<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: PersistentList<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,96 @@
|
|||
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(
|
||||
id = Network.ID(index.toString()),
|
||||
value = Network.DerivationPath.Card("m/44'/0'/0'/0/$index"),
|
||||
networkName = stringReference(value = "Network $index"),
|
||||
)
|
||||
|
||||
DerivationPathUM(
|
||||
id = d.id?.value ?: "",
|
||||
value = d.value.value.orEmpty(),
|
||||
networkName = d.networkName,
|
||||
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 = Network.DerivationPath.Card("m/44'/0'/0'/0/$index"),
|
||||
canHandleTokens = false,
|
||||
)
|
||||
|
||||
CurrencyNetworkUM(
|
||||
network = Network(
|
||||
id = n.id,
|
||||
backendId = n.id.value,
|
||||
name = "",
|
||||
currencySymbol = "",
|
||||
derivationPath = Network.DerivationPath.Card(""),
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
hasFiatFeeRate = false,
|
||||
canHandleTokens = false,
|
||||
),
|
||||
name = "Network $index",
|
||||
type = "N$index",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = false,
|
||||
isSelected = n.id == params.selectedNetwork?.id,
|
||||
onSelectedStateChange = { params.onNetworkSelected(n) },
|
||||
onLongClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
|
||||
val previewState = CustomTokenSelectorUM(
|
||||
header = when (params) {
|
||||
is Params.DerivationPathSelector -> CustomTokenSelectorUM.HeaderUM.CustomDerivationButton(
|
||||
value = null,
|
||||
onClick = {},
|
||||
)
|
||||
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
|
||||
|
|
@ -22,7 +25,9 @@ import kotlinx.collections.immutable.toPersistentList
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class PreviewManageTokensComponent : ManageTokensComponent {
|
||||
internal class PreviewManageTokensComponent(
|
||||
private val isLoading: Boolean = false,
|
||||
) : ManageTokensComponent {
|
||||
|
||||
private val changedItemsIds: MutableSet<String> = mutableSetOf()
|
||||
|
||||
|
|
@ -47,8 +52,11 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
onActiveChange = ::toggleSearchBar,
|
||||
),
|
||||
hasChanges = false,
|
||||
isLoading = false,
|
||||
onSaveClick = {},
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = true,
|
||||
loadMore = { false },
|
||||
saveChanges = {},
|
||||
isSavingInProgress = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -58,7 +66,7 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
initItems()
|
||||
} else {
|
||||
items.filter { currency ->
|
||||
currency.model.name.contains(query, ignoreCase = true)
|
||||
currency.name.contains(query, ignoreCase = true)
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
|
|
@ -88,42 +96,40 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
}
|
||||
|
||||
private fun initItems() = List(size = 30) { index ->
|
||||
if (index < 2) {
|
||||
getCustomItem(index)
|
||||
if (isLoading) {
|
||||
CurrencyItemUM.Loading(index)
|
||||
} else {
|
||||
getBasicItem(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,
|
||||
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,13 +141,24 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
|
||||
private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex ->
|
||||
CurrencyNetworkUM(
|
||||
id = Network.ID(networkIndex.toString()),
|
||||
network = Network(
|
||||
id = Network.ID(networkIndex.toString()),
|
||||
backendId = networkIndex.toString(),
|
||||
name = "Network $networkIndex",
|
||||
currencySymbol = "N$networkIndex",
|
||||
derivationPath = Network.DerivationPath.Card(""),
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
hasFiatFeeRate = false,
|
||||
canHandleTokens = false,
|
||||
),
|
||||
name = "NETWORK$networkIndex",
|
||||
type = "N$networkIndex",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = networkIndex == 0,
|
||||
isSelected = false,
|
||||
onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) },
|
||||
onLongClick = {},
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
|
|
@ -154,7 +171,9 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
CurrencyItemUM.Basic.NetworksUM.Collapsed
|
||||
},
|
||||
)
|
||||
is CurrencyItemUM.Custom -> return
|
||||
is CurrencyItemUM.Custom,
|
||||
is CurrencyItemUM.Loading,
|
||||
-> return
|
||||
}
|
||||
|
||||
previewState.update { state ->
|
||||
|
|
@ -189,7 +208,9 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
|
||||
item.copy(networks = updatedNetworks)
|
||||
}
|
||||
is CurrencyItemUM.Custom -> return
|
||||
is CurrencyItemUM.Custom,
|
||||
is CurrencyItemUM.Loading,
|
||||
-> return
|
||||
}
|
||||
|
||||
val id = "${currencyIndex}_$networkIndex"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.managetokens.di
|
||||
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.impl.DefaultManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.*
|
||||
import com.tangem.features.managetokens.component.impl.*
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -15,4 +15,28 @@ 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
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindCustomTokenDerivationInputComponentFactory(
|
||||
factory: DefaultCustomTokenDerivationInputComponent.Factory,
|
||||
): CustomTokenDerivationInputComponent.Factory
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package com.tangem.features.managetokens.di
|
|||
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.managetokens.model.CustomTokenFormModel
|
||||
import com.tangem.features.managetokens.model.CustomTokenSelectorModel
|
||||
import com.tangem.features.managetokens.model.ManageTokensModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -17,4 +19,14 @@ internal interface ModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(ManageTokensModel::class)
|
||||
fun provideManageTokensModel(model: ManageTokensModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(CustomTokenFormModel::class)
|
||||
fun provideCustomTokenFormModel(model: CustomTokenFormModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(CustomTokenSelectorModel::class)
|
||||
fun provideCustomTokenSelectorModel(model: CustomTokenSelectorModel): Model
|
||||
}
|
||||
|
|
@ -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,44 +0,0 @@
|
|||
package com.tangem.features.managetokens.entity
|
||||
|
||||
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
|
||||
|
||||
@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,
|
||||
) {
|
||||
|
||||
@Immutable
|
||||
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 error: TextReference? = null,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal data class ClickableFieldUM(
|
||||
val label: TextReference,
|
||||
val value: TextReference,
|
||||
val onClick: () -> 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,40 @@
|
|||
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: Network.DerivationPath,
|
||||
val canHandleTokens: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class SelectedDerivationPath(
|
||||
val id: Network.ID?,
|
||||
val value: Network.DerivationPath,
|
||||
val networkName: TextReference,
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.managetokens.entity.customtoken
|
||||
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class CustomDerivationInputUM(
|
||||
val value: TextFieldValue,
|
||||
val error: TextReference? = null,
|
||||
val updateValue: (value: TextFieldValue) -> Unit,
|
||||
val isConfirmEnabled: Boolean,
|
||||
val onConfirm: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.features.managetokens.entity.customtoken
|
||||
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal data class CustomTokenFormUM(
|
||||
val networkName: ClickableFieldUM,
|
||||
val derivationPath: ClickableFieldUM,
|
||||
val tokenForm: TokenFormUM?,
|
||||
val notifications: PersistentList<NotificationUM> = persistentListOf(),
|
||||
val canAddToken: Boolean = false,
|
||||
val isValidating: Boolean = false,
|
||||
val saveToken: () -> Unit,
|
||||
) {
|
||||
|
||||
data class TokenFormUM(
|
||||
val contractAddress: TextInputFieldUM,
|
||||
val name: TextInputFieldUM,
|
||||
val symbol: TextInputFieldUM,
|
||||
val decimals: TextInputFieldUM,
|
||||
val wasFilled: Boolean = false,
|
||||
)
|
||||
|
||||
data class NotificationUM(
|
||||
val id: String,
|
||||
val config: NotificationConfig,
|
||||
)
|
||||
}
|
||||
|
||||
internal data class TextInputFieldUM(
|
||||
val label: TextReference,
|
||||
val placeholder: TextReference,
|
||||
val value: String = "",
|
||||
val error: TextReference? = null,
|
||||
val isEnabled: Boolean = true,
|
||||
val onValueChange: (String) -> Unit,
|
||||
)
|
||||
|
||||
internal data class ClickableFieldUM(
|
||||
val label: TextReference,
|
||||
val value: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.features.managetokens.entity.customtoken
|
||||
|
||||
import com.tangem.domain.managetokens.model.AddCustomTokenForm
|
||||
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) { "" }),
|
||||
)
|
||||
|
||||
fun toDomainModel(): AddCustomTokenForm.Raw? {
|
||||
return if (values.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
AddCustomTokenForm.Raw(
|
||||
contractAddress = values.getOrElse(index = 0) { "" },
|
||||
name = values.getOrElse(index = 1) { "" },
|
||||
symbol = values.getOrElse(index = 2) { "" },
|
||||
decimals = values.getOrElse(index = 3) { "" },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.managetokens.entity.customtoken
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal sealed class CustomTokenSelectorDialogConfig {
|
||||
|
||||
@Serializable
|
||||
data class CustomDerivationInput(
|
||||
val userWalletId: UserWalletId,
|
||||
) : CustomTokenSelectorDialogConfig()
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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 value: String?,
|
||||
val onClick: () -> Unit,
|
||||
) : HeaderUM()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
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()
|
||||
|
||||
class Loading(val index: Int) : CurrencyItemUM() {
|
||||
|
||||
override val id: ManagedCryptoCurrency.ID = ManagedCryptoCurrency.ID(value = "loading_$index")
|
||||
override val name: String = "loading"
|
||||
override val symbol: String = "loading"
|
||||
override val icon: CurrencyIconState = CurrencyIconState.Loading
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.managetokens.entity.item
|
||||
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
|
||||
internal data class CurrencyNetworkUM(
|
||||
val network: Network,
|
||||
val name: String,
|
||||
val type: String,
|
||||
val iconResId: Int,
|
||||
val isMainNetwork: Boolean,
|
||||
val onLongClick: () -> Unit,
|
||||
override val isSelected: Boolean,
|
||||
override val onSelectedStateChange: (Boolean) -> Unit,
|
||||
) : SelectableItemUM {
|
||||
|
||||
override val id: String = network.id.value
|
||||
|
||||
data class LongTapConfig(val contractAddress: String, val onLongTap: () -> Unit)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.managetokens.entity.item
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class DerivationPathUM(
|
||||
override val id: String,
|
||||
val value: String,
|
||||
val networkName: TextReference,
|
||||
override val isSelected: Boolean,
|
||||
override val onSelectedStateChange: (Boolean) -> Unit,
|
||||
) : SelectableItemUM
|
||||
|
|
@ -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 ManageTokensBottomSheetConfig {
|
||||
|
||||
@Serializable
|
||||
data class AddCustomToken(
|
||||
val userWalletId: UserWalletId,
|
||||
) : ManageTokensBottomSheetConfig()
|
||||
}
|
||||
|
|
@ -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,75 @@
|
|||
package com.tangem.features.managetokens.entity.managetokens
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@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
|
||||
abstract val scrollToTop: StateEvent<Unit>
|
||||
|
||||
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,
|
||||
override val scrollToTop: StateEvent<Unit> = consumedEvent(),
|
||||
) : 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,
|
||||
override val scrollToTop: StateEvent<Unit> = consumedEvent(),
|
||||
val saveChanges: () -> Unit,
|
||||
val hasChanges: Boolean,
|
||||
val isSavingInProgress: 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,
|
||||
isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress,
|
||||
scrollToTop: StateEvent<Unit> = this.scrollToTop,
|
||||
): ManageTokensUM {
|
||||
return when (this) {
|
||||
is ManageContent -> copy(
|
||||
search = search,
|
||||
items = items,
|
||||
hasChanges = hasChanges,
|
||||
isInitialBatchLoading = isInitialBatchLoading,
|
||||
isNextBatchLoading = isNextBatchLoading,
|
||||
isSavingInProgress = isSavingInProgress,
|
||||
scrollToTop = scrollToTop,
|
||||
)
|
||||
is ReadContent -> copy(
|
||||
search = search,
|
||||
items = items,
|
||||
isInitialBatchLoading = isInitialBatchLoading,
|
||||
isNextBatchLoading = isNextBatchLoading,
|
||||
scrollToTop = scrollToTop,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
package com.tangem.features.managetokens.model
|
||||
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.components.SimpleOkDialog
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.ContentMessage
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
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.utils.CustomCurrencyValidator
|
||||
import com.tangem.features.managetokens.utils.mapper.mapToDomainModel
|
||||
import com.tangem.features.managetokens.utils.ui.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class CustomTokenFormModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val customCurrencyValidator: CustomCurrencyValidator,
|
||||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params: CustomTokenFormComponent.Params = paramsContainer.require()
|
||||
private var createdCurrency: CryptoCurrency? = null
|
||||
|
||||
val state: MutableStateFlow<CustomTokenFormUM> = MutableStateFlow(
|
||||
value = getInitialState(),
|
||||
)
|
||||
|
||||
init {
|
||||
observeValidatorUpdates()
|
||||
|
||||
if (params.network.canHandleTokens) {
|
||||
observeTokenFormUpdates()
|
||||
} else {
|
||||
modelScope.launch {
|
||||
customCurrencyValidator.createCoin(
|
||||
userWalletId = params.userWalletId,
|
||||
networkId = params.network.id,
|
||||
derivationPath = getDerivationPath(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState(): CustomTokenFormUM {
|
||||
return CustomTokenFormUM(
|
||||
networkName = ClickableFieldUM(
|
||||
label = resourceReference(R.string.custom_token_network_input_title),
|
||||
value = params.network.name,
|
||||
onClick = ::selectNetwork,
|
||||
),
|
||||
tokenForm = if (params.network.canHandleTokens) {
|
||||
getInitialTokenForm()
|
||||
} else {
|
||||
null
|
||||
},
|
||||
derivationPath = ClickableFieldUM(
|
||||
label = resourceReference(R.string.custom_token_derivation_path),
|
||||
value = if (params.derivationPath == null || params.derivationPath.id == params.network.id) {
|
||||
resourceReference(R.string.custom_token_derivation_path_default)
|
||||
} else {
|
||||
params.derivationPath.networkName
|
||||
},
|
||||
onClick = ::selectDerivationPath,
|
||||
),
|
||||
saveToken = ::addCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun observeTokenFormUpdates() {
|
||||
state
|
||||
.transform { state ->
|
||||
val form = state.tokenForm
|
||||
|
||||
if (form != null && !form.wasFilled) {
|
||||
emit(form.mapToDomainModel())
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.sample(periodMillis = 1_000)
|
||||
.onEach { formValues ->
|
||||
customCurrencyValidator.validateForm(
|
||||
userWalletId = params.userWalletId,
|
||||
networkId = params.network.id,
|
||||
derivationPath = getDerivationPath(),
|
||||
formValues = formValues,
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun observeValidatorUpdates() = modelScope.launch {
|
||||
customCurrencyValidator.consumeUpdates { validatorState ->
|
||||
createdCurrency = null
|
||||
|
||||
when (validatorState) {
|
||||
is CustomCurrencyValidator.Status.NotStarted,
|
||||
is CustomCurrencyValidator.Status.Validating,
|
||||
-> Unit
|
||||
is CustomCurrencyValidator.Status.SearchingToken -> updateStateWithProgress()
|
||||
is CustomCurrencyValidator.Status.UnexpectedException -> showErrorDialog()
|
||||
is CustomCurrencyValidator.Status.FormValidationException -> updateStateWithExceptions(
|
||||
exceptions = validatorState.exceptions,
|
||||
)
|
||||
is CustomCurrencyValidator.Status.TokenNotFound -> updateStateWithNotFoundNotification()
|
||||
is CustomCurrencyValidator.Status.Validated -> {
|
||||
createdCurrency = validatorState.currency
|
||||
|
||||
updateStateWithCurrency(
|
||||
currency = validatorState.currency,
|
||||
fillForm = validatorState.fillForm,
|
||||
isAlreadyAdded = validatorState.isAlreadyAdded,
|
||||
isCustom = validatorState.isCustom,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStateWithCurrency(
|
||||
currency: CryptoCurrency,
|
||||
fillForm: Boolean,
|
||||
isAlreadyAdded: Boolean,
|
||||
isCustom: Boolean,
|
||||
) {
|
||||
state.update { state ->
|
||||
var updatedState = state
|
||||
.updateWithProgress(
|
||||
showProgress = false,
|
||||
canAddToken = !isAlreadyAdded,
|
||||
isWasFilled = fillForm,
|
||||
clearNotifications = true,
|
||||
clearFieldErrors = true,
|
||||
disableSecondaryFields = !isCustom,
|
||||
)
|
||||
|
||||
if (fillForm) {
|
||||
updatedState = updatedState.updateWithCurrency(currency)
|
||||
}
|
||||
|
||||
if (isAlreadyAdded) {
|
||||
updatedState = updatedState.updateWithCurrencyAlreadyAddedNotification()
|
||||
}
|
||||
|
||||
if (isCustom) {
|
||||
updatedState = updatedState.updateWithCurrencyNotFoundNotification()
|
||||
}
|
||||
|
||||
updatedState
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStateWithNotFoundNotification() {
|
||||
state.update { state ->
|
||||
state
|
||||
.updateWithProgress(
|
||||
showProgress = false,
|
||||
canAddToken = false,
|
||||
clearNotifications = true,
|
||||
clearFieldErrors = true,
|
||||
)
|
||||
.updateWithCurrencyNotFoundNotification()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStateWithProgress() {
|
||||
state.update { state ->
|
||||
state.updateWithProgress(
|
||||
showProgress = true,
|
||||
canAddToken = false,
|
||||
clearNotifications = false,
|
||||
clearFieldErrors = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorDialog() {
|
||||
val dialog = ContentMessage { onDismiss ->
|
||||
SimpleOkDialog(
|
||||
message = stringResource(R.string.common_unknown_error),
|
||||
onDismissDialog = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
messageSender.send(dialog)
|
||||
}
|
||||
|
||||
private fun updateStateWithExceptions(exceptions: List<CustomTokenFormValidationException>) {
|
||||
state.update { state ->
|
||||
val validatedState = state
|
||||
.updateWithProgress(
|
||||
showProgress = false,
|
||||
canAddToken = false,
|
||||
clearNotifications = true,
|
||||
clearFieldErrors = true,
|
||||
)
|
||||
|
||||
exceptions.fold(validatedState) { stateAcc, exception ->
|
||||
when (exception) {
|
||||
is CustomTokenFormValidationException.ContractAddress -> {
|
||||
stateAcc.updateWithContractAddressException(exception)
|
||||
}
|
||||
is CustomTokenFormValidationException.Decimals -> {
|
||||
stateAcc.updateWithDecimalsException(exception)
|
||||
}
|
||||
is CustomTokenFormValidationException.EmptyName -> {
|
||||
stateAcc // No need to display error
|
||||
}
|
||||
is CustomTokenFormValidationException.EmptySymbol -> {
|
||||
stateAcc // No need to display error
|
||||
}
|
||||
is CustomTokenFormValidationException.DataError -> {
|
||||
Timber.e(exception.cause, "Unable to validate custom currency")
|
||||
stateAcc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(CONTRACT_ADDRESS_PLACEHOLDER),
|
||||
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(DECIMALS_PLACEHOLDER),
|
||||
onValueChange = ::updateDecimals,
|
||||
),
|
||||
)
|
||||
|
||||
return formValues.fillValues(form)
|
||||
}
|
||||
|
||||
private fun getDerivationPath(): Network.DerivationPath {
|
||||
return params.derivationPath?.value ?: params.network.derivationPath
|
||||
}
|
||||
|
||||
private fun updateContractAddress(value: String) {
|
||||
state.update { state ->
|
||||
state.updateTokenForm {
|
||||
copy(
|
||||
contractAddress = contractAddress.updateValue(value),
|
||||
wasFilled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTokenName(value: String) {
|
||||
state.update { state ->
|
||||
state.updateTokenForm {
|
||||
copy(
|
||||
name = name.updateValue(value),
|
||||
wasFilled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTokenSymbol(value: String) {
|
||||
state.update { state ->
|
||||
state.updateTokenForm {
|
||||
copy(
|
||||
symbol = symbol.updateValue(value),
|
||||
wasFilled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDecimals(value: String) {
|
||||
state.update { state ->
|
||||
state.updateTokenForm {
|
||||
copy(
|
||||
decimals = decimals.updateValue(value),
|
||||
wasFilled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addCurrency() = resource(
|
||||
acquire = {
|
||||
state.update { state ->
|
||||
state.updateWithProgress(showProgress = true)
|
||||
}
|
||||
},
|
||||
release = {
|
||||
state.update { state ->
|
||||
state.updateWithProgress(showProgress = false)
|
||||
}
|
||||
},
|
||||
) {
|
||||
val currency = createdCurrency
|
||||
if (currency == null) {
|
||||
Timber.e("Trying to add currency without validation")
|
||||
showErrorDialog()
|
||||
return@resource
|
||||
}
|
||||
|
||||
derivePublicKeysUseCase(params.userWalletId, listOf(currency)).getOrElse {
|
||||
Timber.e(it, "Failed to derive public keys")
|
||||
showErrorDialog()
|
||||
return@resource
|
||||
}
|
||||
|
||||
addCryptoCurrenciesUseCase(params.userWalletId, currency).getOrElse {
|
||||
Timber.e(it, "Failed to add currency")
|
||||
showErrorDialog()
|
||||
return@resource
|
||||
}
|
||||
|
||||
params.onCurrencyAdded()
|
||||
}
|
||||
|
||||
private fun selectNetwork() {
|
||||
params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm))
|
||||
}
|
||||
|
||||
private fun selectDerivationPath() {
|
||||
params.onSelectDerivationPathClick(CustomTokenFormValues(state.value.tokenForm))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CONTRACT_ADDRESS_PLACEHOLDER = "0x000000000000000000000000000..."
|
||||
const val DECIMALS_PLACEHOLDER = "0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
package com.tangem.features.managetokens.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
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.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.GetSupportedNetworksUseCase
|
||||
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.DerivationPathSelector
|
||||
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.NetworkSelector
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorDialogConfig
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM.HeaderUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
|
||||
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
|
||||
import com.tangem.features.managetokens.entity.item.DerivationPathUM
|
||||
import com.tangem.features.managetokens.entity.item.SelectableItemUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.utils.mapper.toCurrencyNetworkModel
|
||||
import com.tangem.features.managetokens.utils.mapper.toDerivationPathModel
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class CustomTokenSelectorModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params: CustomTokenSelectorComponent.Params = paramsContainer.require()
|
||||
|
||||
val dialogNavigation: SlotNavigation<CustomTokenSelectorDialogConfig> = SlotNavigation()
|
||||
|
||||
val state: MutableStateFlow<CustomTokenSelectorUM> = MutableStateFlow(
|
||||
value = getInitialState(),
|
||||
)
|
||||
|
||||
init {
|
||||
loadItems()
|
||||
}
|
||||
|
||||
private fun getInitialState(): CustomTokenSelectorUM = when (params) {
|
||||
is NetworkSelector -> CustomTokenSelectorUM(
|
||||
header = if (params.selectedNetwork == null) {
|
||||
HeaderUM.Description
|
||||
} else {
|
||||
HeaderUM.None
|
||||
},
|
||||
items = persistentListOf(),
|
||||
)
|
||||
is DerivationPathSelector -> CustomTokenSelectorUM(
|
||||
header = HeaderUM.CustomDerivationButton(
|
||||
value = (params.selectedDerivationPath?.value as? Network.DerivationPath.Custom)?.value,
|
||||
onClick = ::showCustomDerivationInput,
|
||||
),
|
||||
items = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadItems() = modelScope.launch {
|
||||
val items = when (params) {
|
||||
is NetworkSelector -> loadNetworks(params).toImmutableList()
|
||||
is DerivationPathSelector -> loadDerivationPaths(params).toImmutableList()
|
||||
}
|
||||
|
||||
state.update { state ->
|
||||
state.copy(items = items)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadNetworks(selector: NetworkSelector): List<SelectableItemUM> {
|
||||
return getSupportedNetworks(selector.userWalletId).map { network ->
|
||||
network.toCurrencyNetworkModel(
|
||||
isSelected = network.id == selector.selectedNetwork?.id,
|
||||
onSelectedStateChange = {
|
||||
val model = SelectedNetwork(
|
||||
id = network.id,
|
||||
name = stringReference(network.name),
|
||||
derivationPath = network.derivationPath,
|
||||
canHandleTokens = network.canHandleTokens,
|
||||
)
|
||||
|
||||
selector.onNetworkSelected(model)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadDerivationPaths(selector: DerivationPathSelector): List<SelectableItemUM> {
|
||||
val derivationPaths = mutableListOf<DerivationPathUM>()
|
||||
val defaultPath = selector.selectedNetwork.let { network ->
|
||||
network.toDerivationPathModel(
|
||||
isSelected = network.id == selector.selectedDerivationPath?.id,
|
||||
onSelectedStateChange = {
|
||||
val model = SelectedDerivationPath(
|
||||
id = network.id,
|
||||
networkName = resourceReference(R.string.custom_token_derivation_path_default),
|
||||
value = network.derivationPath,
|
||||
)
|
||||
|
||||
selector.onDerivationPathSelected(model)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (defaultPath != null) {
|
||||
derivationPaths.add(defaultPath)
|
||||
}
|
||||
|
||||
getSupportedNetworks(selector.userWalletId)
|
||||
.mapNotNullTo(derivationPaths) { network ->
|
||||
if (network.id == selector.selectedNetwork.id) {
|
||||
return@mapNotNullTo null // Skip default path
|
||||
}
|
||||
|
||||
network.toDerivationPathModel(
|
||||
isSelected = network.id == selector.selectedDerivationPath?.id,
|
||||
onSelectedStateChange = {
|
||||
val model = SelectedDerivationPath(
|
||||
id = network.id,
|
||||
networkName = stringReference(network.name),
|
||||
value = network.derivationPath,
|
||||
)
|
||||
|
||||
selector.onDerivationPathSelected(model)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return derivationPaths
|
||||
}
|
||||
|
||||
private suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> {
|
||||
return getSupportedNetworksUseCase(userWalletId).getOrElse { e ->
|
||||
val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error))
|
||||
messageSender.send(message)
|
||||
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCustomDerivationInput() {
|
||||
val config = when (params) {
|
||||
is NetworkSelector -> return
|
||||
is DerivationPathSelector -> CustomTokenSelectorDialogConfig.CustomDerivationInput(params.userWalletId)
|
||||
}
|
||||
|
||||
dialogNavigation.activate(config)
|
||||
}
|
||||
|
||||
fun selectCustomDerivationPath(value: SelectedDerivationPath) {
|
||||
when (params) {
|
||||
is NetworkSelector -> return
|
||||
is DerivationPathSelector -> params.onDerivationPathSelected(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +1,97 @@
|
|||
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.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
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.managetokens.SaveManagedTokensUseCase
|
||||
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.ManageTokensBottomSheetConfig
|
||||
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.BatchFetchResult
|
||||
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.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
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,
|
||||
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
|
||||
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<ManageTokensBottomSheetConfig> = 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)
|
||||
|
||||
observeSearchQueryChanges()
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.launchPagination(params.userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadList() {
|
||||
modelScope.launch {
|
||||
manageTokensListManager.reload(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 = getLoadingItems(),
|
||||
topBar = ManageTokensTopBarUM.ReadContent(
|
||||
title = resourceReference(R.string.common_search_tokens),
|
||||
onBackButtonClick = router::pop,
|
||||
|
|
@ -59,20 +103,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 = getLoadingItems(),
|
||||
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,161 +128,179 @@ internal class ManageTokensModel @Inject constructor(
|
|||
isActive = false,
|
||||
onActiveChange = ::toggleSearchBar,
|
||||
),
|
||||
onSaveClick = ::onSaveClick,
|
||||
hasChanges = false,
|
||||
saveChanges = ::saveChanges,
|
||||
loadMore = ::loadMoreItems,
|
||||
isSavingInProgress = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onAddCustomToken() {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun observeSearchQueryChanges() {
|
||||
state
|
||||
.distinctUntilChanged { old, new ->
|
||||
// It's also used to skip search activation to avoid searching an empty query
|
||||
old.search.query == new.search.query &&
|
||||
(old.search.isActive == new.search.isActive || new.search.isActive)
|
||||
}
|
||||
.transform { state ->
|
||||
val query = state.search.query
|
||||
|
||||
if (state.search.isActive) {
|
||||
emit(query)
|
||||
}
|
||||
}
|
||||
.sample(periodMillis = 1_000)
|
||||
.onEach { query ->
|
||||
manageTokensListManager.search(
|
||||
userWalletId = params.userWalletId,
|
||||
query = query,
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun 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.copySealed(
|
||||
items = getLoadingItems(),
|
||||
)
|
||||
} else {
|
||||
state.copySealed(
|
||||
items = getLoadingItems(),
|
||||
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 -> {
|
||||
(status.lastResult as? BatchFetchResult.Error)?.let { fetchError ->
|
||||
Timber.e(fetchError.throwable)
|
||||
}
|
||||
|
||||
state.copySealed(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
scrollToTop = if (state.isInitialBatchLoading && state.items.isNotEmpty()) {
|
||||
triggeredEvent(
|
||||
data = Unit,
|
||||
onConsume = ::consumeScrollToTopEvent,
|
||||
)
|
||||
} else {
|
||||
state.scrollToTop
|
||||
},
|
||||
)
|
||||
}
|
||||
is PaginationStatus.EndOfPagination -> state.copySealed(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLoadingItems(): ImmutableList<CurrencyItemUM> {
|
||||
return List(size = 10) { index ->
|
||||
CurrencyItemUM.Loading(index)
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
private fun consumeScrollToTopEvent() {
|
||||
this.state.update { state ->
|
||||
state.copySealed(
|
||||
scrollToTop = consumedEvent(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 navigateToAddCustomToken() {
|
||||
params.userWalletId?.let {
|
||||
bottomSheetNavigation.activate(ManageTokensBottomSheetConfig.AddCustomToken(it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveChanges() {
|
||||
modelScope.launch {
|
||||
state.update { state -> state.copySealed(isSavingInProgress = true) }
|
||||
saveManagedTokensUseCase.invoke(
|
||||
userWalletId = requireNotNull(params.userWalletId),
|
||||
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
|
||||
currenciesToRemove = manageTokensListManager.currenciesToRemove.value,
|
||||
).fold(
|
||||
ifLeft = { Timber.e(it, "Failed to save changes") },
|
||||
ifRight = { router.pop() },
|
||||
)
|
||||
state.update { state -> state.copySealed(isSavingInProgress = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchCurrencies(query: String) {
|
||||
state.update { state ->
|
||||
state.copySealed(
|
||||
search = state.search.copy(
|
||||
query = query,
|
||||
isActive = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSearchBar(isActive: Boolean) {
|
||||
state.update { state ->
|
||||
state.copySealed(
|
||||
search = state.search.copy(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
|
||||
}
|
||||
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(),
|
||||
search = state.search.copy(
|
||||
isActive = isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,40 @@ 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 = Network.DerivationPath.None,
|
||||
canHandleTokens = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
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 = Network.DerivationPath.None,
|
||||
canHandleTokens = false,
|
||||
),
|
||||
addTokenButton = AddCustomTokenButtonUM.Visible(
|
||||
isEnabled = false,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
PreviewAddCustomTokenComponent(
|
||||
initialState = AddCustomTokenConfig(
|
||||
userWalletId = UserWalletId(stringValue = "321"),
|
||||
step = AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR,
|
||||
popBack = {},
|
||||
selectedDerivationPath = SelectedDerivationPath(
|
||||
id = Network.ID(value = "0"),
|
||||
value = Network.DerivationPath.None,
|
||||
networkName = stringReference("Ethereum"),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,97 +2,153 @@ 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.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
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,
|
||||
showProgress = model.isValidating,
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -104,14 +160,25 @@ private fun TextField(
|
|||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
var isFocused by remember { mutableStateOf(value = false) }
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
val color by animateColorAsState(
|
||||
targetValue = if (model.error != null) {
|
||||
TangemTheme.colors.text.warning
|
||||
} else {
|
||||
TangemTheme.colors.text.tertiary
|
||||
targetValue = when {
|
||||
!model.isEnabled -> {
|
||||
TangemTheme.colors.text.disabled
|
||||
}
|
||||
model.error != null -> {
|
||||
TangemTheme.colors.text.warning
|
||||
}
|
||||
model.value.isNotBlank() || isFocused -> {
|
||||
TangemTheme.colors.text.tertiary
|
||||
}
|
||||
else -> {
|
||||
TangemTheme.colors.text.disabled
|
||||
}
|
||||
},
|
||||
label = "Field label color",
|
||||
)
|
||||
|
|
@ -123,12 +190,27 @@ private fun TextField(
|
|||
)
|
||||
},
|
||||
content = {
|
||||
val color by animateColorAsState(
|
||||
targetValue = if (model.isEnabled) {
|
||||
TangemTheme.colors.text.primary1
|
||||
} else {
|
||||
TangemTheme.colors.text.disabled
|
||||
},
|
||||
label = "Field value color",
|
||||
)
|
||||
|
||||
SimpleTextField(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing12)
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged {
|
||||
isFocused = it.isFocused
|
||||
},
|
||||
value = model.value,
|
||||
color = color,
|
||||
onValueChange = model.onValueChange,
|
||||
readOnly = false,
|
||||
placeholder = model.placeholder,
|
||||
readOnly = !model.isEnabled && !isFocused,
|
||||
singleLine = true,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
|
|
@ -170,9 +252,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,16 +261,30 @@ private class PreviewCustomTokenFormComponentProvider :
|
|||
|
||||
override val values: Sequence<PreviewCustomTokenFormComponent>
|
||||
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",
|
||||
placeholder = stringReference("0x1234567890"),
|
||||
onValueChange = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
PreviewCustomTokenFormComponent(
|
||||
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,288 @@
|
|||
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.block.information.InformationBlock
|
||||
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.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.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.background(
|
||||
color = TangemTheme.colors.background.secondary,
|
||||
),
|
||||
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),
|
||||
enteredDerivationPath = header.value,
|
||||
isSelected = header.value != null,
|
||||
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 = false,
|
||||
showCustomBadge = false,
|
||||
),
|
||||
showCustom = false,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
SelectedIcon(isVisible = model.isSelected)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomDerivationButton(
|
||||
enteredDerivationPath: String?,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
InformationBlock(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable(onClick = onClick),
|
||||
title = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.custom_token_custom_derivation),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
text = enteredDerivationPath ?: stringResource(id = R.string.custom_token_custom_derivation_title),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
},
|
||||
action = {
|
||||
SelectedIcon(isVisible = isSelected)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DerivationPathItem(model: DerivationPathUM, modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
shape = RectangleShape,
|
||||
title = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
Text(
|
||||
text = model.networkName.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
text = model.value,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
},
|
||||
action = {
|
||||
SelectedIcon(isVisible = model.isSelected)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectedIcon(isVisible: Boolean, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(
|
||||
modifier = modifier.size(TangemTheme.dimens.size24),
|
||||
visible = isVisible,
|
||||
) {
|
||||
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: CustomTokenSelectorComponent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
component.Content(modifier = Modifier)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomTokenNetworkSelectorComponentPreviewProvider :
|
||||
PreviewParameterProvider<CustomTokenSelectorComponent> {
|
||||
override val values: Sequence<CustomTokenSelectorComponent>
|
||||
get() = sequenceOf(
|
||||
PreviewCustomTokenSelectorComponent(
|
||||
params = CustomTokenSelectorComponent.Params.DerivationPathSelector(
|
||||
userWalletId = UserWalletId(stringValue = "321"),
|
||||
selectedNetwork = SelectedNetwork(
|
||||
id = Network.ID(value = "0"),
|
||||
name = stringReference("Ethereum"),
|
||||
derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"),
|
||||
canHandleTokens = true,
|
||||
),
|
||||
selectedDerivationPath = SelectedDerivationPath(
|
||||
id = Network.ID(value = "0"),
|
||||
value = Network.DerivationPath.Card("m/44'/0'/0'/0/0"),
|
||||
networkName = stringReference(""),
|
||||
),
|
||||
onDerivationPathSelected = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -1,15 +1,18 @@
|
|||
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.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
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,49 +23,80 @@ 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
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.TangemSwitch
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
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.ChainRowContainer
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.components.rows.model.ChainRowUM
|
||||
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
import com.tangem.core.ui.res.LocalSnackbarHostState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
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 +104,13 @@ 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,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
TangemSnackbarHost(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
hostState = LocalSnackbarHostState.current,
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
|
|
@ -81,10 +118,12 @@ 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,
|
||||
showProgress = state.isSavingInProgress,
|
||||
onClick = state.saveChanges,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -92,20 +131,35 @@ 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
|
||||
private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
private fun SaveChangesButton(
|
||||
isVisible: Boolean,
|
||||
showProgress: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
modifier = modifier,
|
||||
visible = isVisible,
|
||||
|
|
@ -116,86 +170,64 @@ private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier:
|
|||
PrimaryButtonIconEnd(
|
||||
text = stringResource(id = R.string.common_save),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
showProgress = showProgress,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingContent() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = TangemTheme.colors.background.primary),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(color = TangemTheme.colors.icon.accent)
|
||||
}
|
||||
}
|
||||
private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
search: SearchBarUM,
|
||||
items: ImmutableList<CurrencyItemUM>,
|
||||
isLoading: Boolean,
|
||||
hasChanges: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
Currencies(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
items = items,
|
||||
search = search,
|
||||
listState = listState,
|
||||
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()
|
||||
}
|
||||
EventEffect(event = state.scrollToTop) {
|
||||
listState.animateScrollToItem(index = 0)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun Currencies(items: ImmutableList<CurrencyItemUM>, search: SearchBarUM, modifier: Modifier = Modifier) {
|
||||
private fun Currencies(
|
||||
listState: LazyListState,
|
||||
items: ImmutableList<CurrencyItemUM>,
|
||||
showLoadingItem: Boolean,
|
||||
isEditable: Boolean,
|
||||
onLoadMore: () -> Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val bottomBarHeight = with(LocalDensity.current) {
|
||||
WindowInsets.systemBars.getBottom(density = this).toDp()
|
||||
}
|
||||
|
||||
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 -> {
|
||||
|
|
@ -204,16 +236,78 @@ private fun Currencies(items: ImmutableList<CurrencyItemUM>, search: SearchBarUM
|
|||
item = item,
|
||||
)
|
||||
}
|
||||
is CurrencyItemUM.Loading -> {
|
||||
LoadingItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingItem(modifier: Modifier = Modifier) {
|
||||
ChainRowContainer(
|
||||
modifier = modifier,
|
||||
icon = {
|
||||
CurrencyIcon(CurrencyIconState.Loading)
|
||||
},
|
||||
text = {
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(70.dp),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(
|
||||
width = 24.dp,
|
||||
height = 16.dp,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
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 +320,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 +361,22 @@ 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Modifier = Modifier) {
|
||||
private fun NetworksList(
|
||||
networks: NetworksUM,
|
||||
currencyId: String,
|
||||
isEditable: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticManager = LocalHapticManager.current
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = modifier,
|
||||
visible = networks is NetworksUM.Expanded,
|
||||
|
|
@ -286,8 +396,17 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod
|
|||
isLastItem = index == currentItems.lastIndex,
|
||||
content = {
|
||||
BlockchainRow(
|
||||
modifier = Modifier
|
||||
.padding(end = TangemTheme.dimens.spacing8)
|
||||
.combinedClickable(
|
||||
onLongClick = network.onLongClick,
|
||||
onClick = {},
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
),
|
||||
model = with(network) {
|
||||
BlockchainRowUM(
|
||||
id = id,
|
||||
name = name,
|
||||
type = type,
|
||||
iconResId = iconResId,
|
||||
|
|
@ -296,10 +415,19 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod
|
|||
)
|
||||
},
|
||||
action = {
|
||||
TangemSwitch(
|
||||
checked = network.isSelected,
|
||||
onCheckedChange = network.onSelectedStateChange,
|
||||
)
|
||||
if (isEditable) {
|
||||
TangemSwitch(
|
||||
checked = network.isSelected,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
hapticManager.perform(TangemHapticEffect.View.ToggleOn)
|
||||
} else {
|
||||
hapticManager.perform(TangemHapticEffect.View.ToggleOff)
|
||||
}
|
||||
network.onSelectedStateChange(checked)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
|
|
@ -313,9 +441,19 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod
|
|||
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_ManageTokens() {
|
||||
private fun Preview_ManageTokens(
|
||||
@PreviewParameter(PreviewManageTokensComponentProvider::class) component: ManageTokensComponent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
PreviewManageTokensComponent().Content(Modifier.fillMaxWidth())
|
||||
component.Content(Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
|
||||
private class PreviewManageTokensComponentProvider : PreviewParameterProvider<ManageTokensComponent> {
|
||||
override val values: Sequence<ManageTokensComponent>
|
||||
get() = sequenceOf(
|
||||
PreviewManageTokensComponent(),
|
||||
PreviewManageTokensComponent(isLoading = true),
|
||||
)
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -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,22 @@
|
|||
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.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun CurrencyUnsupportedDialog(title: TextReference, message: TextReference, onDismiss: () -> Unit) {
|
||||
BasicDialog(
|
||||
title = title.resolveReference(),
|
||||
message = message.resolveReference(),
|
||||
confirmButton = DialogButtonUM(
|
||||
title = stringResource(R.string.common_ok),
|
||||
onClick = onDismiss,
|
||||
),
|
||||
onDismissDialog = onDismiss,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.features.managetokens.ui.dialog
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
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.AdditionalTextInputDialogUM
|
||||
import com.tangem.core.ui.components.DialogButtonUM
|
||||
import com.tangem.core.ui.components.TextInputDialog
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent
|
||||
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenDerivationInputComponent
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomDerivationInputUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun CustomDerivationInputDialog(model: CustomDerivationInputUM, onDismiss: () -> Unit) {
|
||||
val value by rememberUpdatedState(newValue = model.value)
|
||||
|
||||
TextInputDialog(
|
||||
title = stringResource(id = R.string.custom_token_custom_derivation_title),
|
||||
fieldValue = value,
|
||||
confirmButton = DialogButtonUM(
|
||||
title = stringResource(id = R.string.common_ok),
|
||||
enabled = model.isConfirmEnabled,
|
||||
onClick = model.onConfirm,
|
||||
),
|
||||
dismissButton = DialogButtonUM(
|
||||
title = stringResource(id = R.string.common_cancel),
|
||||
onClick = onDismiss,
|
||||
),
|
||||
onDismissDialog = onDismiss,
|
||||
onValueChange = model.updateValue,
|
||||
textFieldParams = AdditionalTextInputDialogUM(
|
||||
label = model.error?.resolveReference() ?: stringResource(id = R.string.custom_token_derivation_path),
|
||||
isError = model.error != null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_CustomDerivationInputDialog(
|
||||
@PreviewParameter(ComponentPreviewProvider::class) component: CustomTokenDerivationInputComponent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
component.Dialog()
|
||||
}
|
||||
}
|
||||
|
||||
private class ComponentPreviewProvider : PreviewParameterProvider<CustomTokenDerivationInputComponent> {
|
||||
override val values: Sequence<CustomTokenDerivationInputComponent>
|
||||
get() = sequenceOf(
|
||||
PreviewCustomTokenDerivationInputComponent(),
|
||||
PreviewCustomTokenDerivationInputComponent(
|
||||
value = "m/44'/60'/0'/0/0",
|
||||
),
|
||||
PreviewCustomTokenDerivationInputComponent(
|
||||
value = "m/44'/60'/0'/0/0",
|
||||
error = "Invalid derivation path",
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -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,255 @@
|
|||
package com.tangem.features.managetokens.utils
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase
|
||||
import com.tangem.domain.managetokens.CreateCurrencyUseCase
|
||||
import com.tangem.domain.managetokens.FindTokenUseCase
|
||||
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
|
||||
import com.tangem.domain.managetokens.model.AddCustomTokenForm
|
||||
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
|
||||
import com.tangem.domain.managetokens.model.exceptoin.FindTokenException
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveInAndJoin
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class CustomCurrencyValidator @Inject constructor(
|
||||
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
|
||||
private val createCustomCurrencyUseCase: CreateCurrencyUseCase,
|
||||
private val findTokenUseCase: FindTokenUseCase,
|
||||
private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase,
|
||||
) {
|
||||
|
||||
private val validateFormJobHolder = JobHolder()
|
||||
private val state: MutableStateFlow<State> = MutableStateFlow(
|
||||
value = State(
|
||||
prevValidatedForm = null,
|
||||
prevFoundOrCreatedCurrency = null,
|
||||
status = Status.NotStarted,
|
||||
),
|
||||
)
|
||||
|
||||
suspend fun consumeUpdates(block: suspend (Status) -> Unit) {
|
||||
state
|
||||
.map { it.status }
|
||||
.distinctUntilChanged()
|
||||
.collectLatest { block(it) }
|
||||
}
|
||||
|
||||
suspend fun validateForm(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
formValues: AddCustomTokenForm.Raw,
|
||||
) = coroutineScope {
|
||||
updateStatus(Status.Validating)
|
||||
|
||||
val result = validateTokenFormUseCase(
|
||||
networkId = networkId,
|
||||
formValues = formValues,
|
||||
)
|
||||
|
||||
val validatedForm = result.getOrElse { e ->
|
||||
updateStatus(Status.FormValidationException(e))
|
||||
return@coroutineScope
|
||||
}
|
||||
|
||||
if (state.value.prevValidatedForm == validatedForm) {
|
||||
return@coroutineScope
|
||||
} else {
|
||||
state.update { state ->
|
||||
state.copy(prevValidatedForm = validatedForm)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
when (validatedForm) {
|
||||
is AddCustomTokenForm.Validated.All -> {
|
||||
findOrCreateCurrency(userWalletId, networkId, derivationPath, validatedForm)
|
||||
}
|
||||
is AddCustomTokenForm.Validated.ContractAddress -> {
|
||||
findToken(userWalletId, networkId, derivationPath, validatedForm)
|
||||
}
|
||||
}
|
||||
}.saveInAndJoin(validateFormJobHolder)
|
||||
}
|
||||
|
||||
suspend fun createCoin(userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath) {
|
||||
createCurrency(userWalletId, networkId, derivationPath, validatedForm = null)
|
||||
}
|
||||
|
||||
private suspend fun findOrCreateCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
validatedForm: AddCustomTokenForm.Validated.All,
|
||||
) {
|
||||
val currentState = state.value
|
||||
if (currentState.prevFoundOrCreatedCurrency is CryptoCurrency.Token &&
|
||||
currentState.prevFoundOrCreatedCurrency.contractAddress == validatedForm.contractAddress
|
||||
) {
|
||||
// No need to search for token again if contract address is not changed
|
||||
createCurrency(userWalletId, networkId, derivationPath, validatedForm)
|
||||
return
|
||||
}
|
||||
|
||||
updateStatus(Status.SearchingToken)
|
||||
|
||||
val foundToken = findTokenUseCase(
|
||||
userWalletId = userWalletId,
|
||||
contractAddress = validatedForm.contractAddress,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
).getOrElse { e ->
|
||||
when (e) {
|
||||
is FindTokenException.DataError -> {
|
||||
Timber.e(e.cause, "Unable to find custom currency")
|
||||
updateStatus(Status.UnexpectedException(e.cause))
|
||||
return
|
||||
}
|
||||
is FindTokenException.NotFound -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundToken != null) {
|
||||
updateStateToValidated(userWalletId, foundToken, fillForm = true, isCustom = false)
|
||||
} else {
|
||||
createCurrency(userWalletId, networkId, derivationPath, validatedForm)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun findToken(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
validatedForm: AddCustomTokenForm.Validated.ContractAddress,
|
||||
) {
|
||||
updateStatus(Status.SearchingToken)
|
||||
|
||||
val token = findTokenUseCase(
|
||||
userWalletId = userWalletId,
|
||||
contractAddress = validatedForm.contractAddress,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
).getOrElse { e ->
|
||||
val newStatus = when (e) {
|
||||
is FindTokenException.DataError -> {
|
||||
Timber.e(e.cause, "Unable to find custom currency")
|
||||
Status.UnexpectedException(e.cause)
|
||||
}
|
||||
is FindTokenException.NotFound -> {
|
||||
Status.TokenNotFound
|
||||
}
|
||||
}
|
||||
updateStatus(newStatus)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
updateStateToValidated(userWalletId, token, fillForm = true, isCustom = false)
|
||||
}
|
||||
|
||||
private suspend fun createCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
validatedForm: AddCustomTokenForm.Validated.All?,
|
||||
) {
|
||||
val currency = createCustomCurrencyUseCase(
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
formValues = validatedForm,
|
||||
).getOrElse { e ->
|
||||
Timber.e(e, "Unable to create custom currency")
|
||||
updateStatus(Status.UnexpectedException(e))
|
||||
return
|
||||
}
|
||||
|
||||
updateStateToValidated(userWalletId, currency, fillForm = false, isCustom = validatedForm != null)
|
||||
}
|
||||
|
||||
private suspend fun updateStateToValidated(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
fillForm: Boolean,
|
||||
isCustom: Boolean,
|
||||
) {
|
||||
val currentStatus = state.value.status
|
||||
if (currentStatus is Status.Validated && currentStatus.currency == currency) return
|
||||
|
||||
val isNotAdded = checkIsCurrencyNotAddedUseCase(
|
||||
userWalletId = userWalletId,
|
||||
networkId = currency.network.id,
|
||||
derivationPath = currency.network.derivationPath,
|
||||
contractAddress = when (currency) {
|
||||
is CryptoCurrency.Coin -> null
|
||||
is CryptoCurrency.Token -> currency.contractAddress
|
||||
},
|
||||
).getOrElse { e ->
|
||||
Timber.e(e, "Unable to check if currency is already added")
|
||||
updateStatus(Status.UnexpectedException(e))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
status = Status.Validated(
|
||||
currency = currency,
|
||||
fillForm = fillForm,
|
||||
isAlreadyAdded = !isNotAdded,
|
||||
isCustom = isCustom,
|
||||
),
|
||||
prevFoundOrCreatedCurrency = currency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStatus(status: Status) {
|
||||
state.update { state ->
|
||||
state.copy(status = status)
|
||||
}
|
||||
}
|
||||
|
||||
data class State(
|
||||
val prevValidatedForm: AddCustomTokenForm.Validated?,
|
||||
val prevFoundOrCreatedCurrency: CryptoCurrency?,
|
||||
val status: Status,
|
||||
)
|
||||
|
||||
sealed class Status {
|
||||
|
||||
data object NotStarted : Status()
|
||||
|
||||
data object SearchingToken : Status()
|
||||
|
||||
data object Validating : Status()
|
||||
|
||||
data class Validated(
|
||||
val currency: CryptoCurrency,
|
||||
val fillForm: Boolean,
|
||||
val isAlreadyAdded: Boolean,
|
||||
val isCustom: Boolean,
|
||||
) : Status()
|
||||
|
||||
data class FormValidationException(
|
||||
val exceptions: List<CustomTokenFormValidationException>,
|
||||
) : Status()
|
||||
|
||||
data object TokenNotFound : Status()
|
||||
|
||||
data class UnexpectedException(
|
||||
val cause: Throwable,
|
||||
) : Status()
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Token, Set<Network>>
|
||||
|
||||
internal class ChangedCurrenciesManager {
|
||||
|
||||
val currenciesToAdd: MutableStateFlow<ChangedCurrencies> = MutableStateFlow(emptyMap())
|
||||
val currenciesToRemove: MutableStateFlow<ChangedCurrencies> = MutableStateFlow(emptyMap())
|
||||
|
||||
fun addCurrency(currency: ManagedCryptoCurrency.Token, network: Network) {
|
||||
updateChangedItems(currency, network, currenciesToRemove, currenciesToAdd)
|
||||
}
|
||||
|
||||
fun removeCurrency(currency: ManagedCryptoCurrency.Token, network: Network) {
|
||||
updateChangedItems(currency, network, currenciesToAdd, currenciesToRemove)
|
||||
}
|
||||
|
||||
fun containsCurrency(currency: ManagedCryptoCurrency.Token, network: Network): Boolean {
|
||||
return network in currenciesToAdd.value[currency].orEmpty() ||
|
||||
network in currenciesToRemove.value[currency].orEmpty()
|
||||
}
|
||||
|
||||
private fun updateChangedItems(
|
||||
currency: ManagedCryptoCurrency.Token,
|
||||
network: Network,
|
||||
removeFromIfPresent: MutableStateFlow<ChangedCurrencies>,
|
||||
addToIfNotPresent: MutableStateFlow<ChangedCurrencies>,
|
||||
) {
|
||||
val present = removeFromIfPresent.value[currency].orEmpty()
|
||||
|
||||
if (network in present) {
|
||||
removeFromIfPresent.update { items ->
|
||||
items.toMutableMap().apply {
|
||||
val ids = present - network
|
||||
|
||||
if (ids.isEmpty()) {
|
||||
remove(currency)
|
||||
} else {
|
||||
set(currency, ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addToIfNotPresent.update { items ->
|
||||
val alreadyAdded = items[currency] ?: emptySet()
|
||||
if (network in alreadyAdded) {
|
||||
return@update items
|
||||
}
|
||||
|
||||
items + (currency to alreadyAdded + network)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
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.clipboard.ClipboardManager
|
||||
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.CheckCurrencyUnsupportedUseCase
|
||||
import com.tangem.domain.managetokens.GetManagedTokensUseCase
|
||||
import com.tangem.domain.managetokens.RemoveCustomManagedCryptoCurrencyUseCase
|
||||
import com.tangem.domain.managetokens.CheckHasLinkedTokensUseCase
|
||||
import com.tangem.domain.managetokens.model.*
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.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 kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ComponentScoped
|
||||
internal class ManageTokensListManager @Inject constructor(
|
||||
private val getManagedTokensUseCase: GetManagedTokensUseCase,
|
||||
private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase,
|
||||
private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase,
|
||||
private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
clipboardManager: ClipboardManager,
|
||||
) : 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 },
|
||||
clipboardManager = clipboardManager,
|
||||
)
|
||||
|
||||
val currenciesToAdd: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToAdd.asStateFlow()
|
||||
val currenciesToRemove: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToRemove.asStateFlow()
|
||||
|
||||
@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?) {
|
||||
state.value = ManageTokensListState()
|
||||
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, currency: ManagedCryptoCurrency.Token, network: Network) {
|
||||
changedCurrenciesManager.addCurrency(currency, network)
|
||||
|
||||
sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = true)
|
||||
}
|
||||
|
||||
override fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) {
|
||||
changedCurrenciesManager.removeCurrency(currency, network)
|
||||
|
||||
sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false)
|
||||
}
|
||||
|
||||
override fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) {
|
||||
scope.launch {
|
||||
removeCustomCurrencyUseCase.invoke(userWalletId, currency)
|
||||
.onRight { reload(userWalletId) }
|
||||
.onLeft { Timber.e(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkNeedToShowRemoveNetworkWarning(
|
||||
currency: ManagedCryptoCurrency.Token,
|
||||
network: Network,
|
||||
): Boolean = !changedCurrenciesManager.containsCurrency(currency, network)
|
||||
|
||||
private fun sendSelectCurrencyAction(
|
||||
batchKey: Int,
|
||||
currencyId: ManagedCryptoCurrency.ID,
|
||||
network: Network,
|
||||
isSelected: Boolean,
|
||||
) {
|
||||
val request = ManageTokensUpdateAction.AddCurrency(
|
||||
currencyId = currencyId,
|
||||
network = network,
|
||||
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 = userWalletId,
|
||||
network = network,
|
||||
tempAddedTokens = changedCurrenciesManager.currenciesToAdd.value,
|
||||
tempRemovedTokens = changedCurrenciesManager.currenciesToRemove.value,
|
||||
).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
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun checkCurrencyUnsupportedState(
|
||||
userWalletId: UserWalletId,
|
||||
sourceNetwork: ManagedCryptoCurrency.SourceNetwork,
|
||||
): CurrencyUnsupportedState? {
|
||||
return checkCurrencyUnsupportedUseCase(
|
||||
userWalletId = userWalletId,
|
||||
sourceNetwork = sourceNetwork,
|
||||
).getOrElse {
|
||||
Timber.e(
|
||||
it,
|
||||
"""
|
||||
Failed to check currency unsupported state
|
||||
|- User wallet ID: $userWalletId
|
||||
|- Source Network: $sourceNetwork
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val message = SnackbarMessage(
|
||||
message = it.localizedMessage
|
||||
?.let(::stringReference)
|
||||
?: resourceReference(R.string.common_error),
|
||||
)
|
||||
messageSender.send(message)
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,24 @@
|
|||
package com.tangem.features.managetokens.utils.list
|
||||
|
||||
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
|
||||
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, currency: ManagedCryptoCurrency.Token, network: Network)
|
||||
|
||||
fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network)
|
||||
|
||||
fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom)
|
||||
|
||||
fun checkNeedToShowRemoveNetworkWarning(currency: ManagedCryptoCurrency.Token, network: Network): Boolean
|
||||
|
||||
suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean
|
||||
|
||||
suspend fun checkCurrencyUnsupportedState(
|
||||
userWalletId: UserWalletId,
|
||||
sourceNetwork: ManagedCryptoCurrency.SourceNetwork,
|
||||
): CurrencyUnsupportedState?
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
package com.tangem.features.managetokens.utils.list
|
||||
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.ContentMessage
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
|
||||
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.impl.R
|
||||
import com.tangem.features.managetokens.ui.dialog.CurrencyUnsupportedDialog
|
||||
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 com.tangem.utils.extensions.addOrReplace
|
||||
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 clipboardManager: ClipboardManager,
|
||||
) {
|
||||
|
||||
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 }
|
||||
val currencyBatch = state.value.currencyBatches.getOrNull(indexToUpdate)
|
||||
|
||||
if (indexToUpdate == -1 || currencyBatch == null) {
|
||||
val newBatch = Batch(
|
||||
key = key,
|
||||
data = data.map { item ->
|
||||
item.toUiModel(
|
||||
isEditable = canEditItems,
|
||||
onRemoveCustomCurrencyClick = ::removeCustomCurrency,
|
||||
onExpandNetworksClick = ::toggleCurrencyNetworksVisibility,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
batches.addOrReplace(newBatch) { it.key == key }
|
||||
} else {
|
||||
val uiBatchToUpdate = currentUiBatches[indexToUpdate]
|
||||
|
||||
if (uiBatchToUpdate.data == data) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
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 = {
|
||||
val userWalletId = requireNotNull(state.value.userWalletId) { "UserWalletId is null. Can not remove" }
|
||||
actions.removeCustomCurrency(userWalletId = userWalletId, currency = currency)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
onLongTap = ::copyContractAddress,
|
||||
)
|
||||
|
||||
batches.updateUiBatchesItem(
|
||||
indexToBatch = batchIndex to uiBatch,
|
||||
indexToItem = currencyIndex to updatedUiItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyContractAddress(source: ManagedCryptoCurrency.SourceNetwork) {
|
||||
if (source is ManagedCryptoCurrency.SourceNetwork.Default) {
|
||||
clipboardManager.setText(text = source.contractAddress)
|
||||
showSnackbarMessage(resourceReference(R.string.contract_address_copied_message))
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSnackbarMessage(messageText: TextReference) {
|
||||
val message = SnackbarMessage(message = messageText)
|
||||
messageSender.send(message)
|
||||
}
|
||||
|
||||
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) {
|
||||
val userWalletId = state.value.userWalletId
|
||||
val unsupportedState = userWalletId?.let { actions.checkCurrencyUnsupportedState(it, source) }
|
||||
if (unsupportedState != null) {
|
||||
showUnsupportedWarning(unsupportedState)
|
||||
} else {
|
||||
actions.addCurrency(batchKey, currency, source.network)
|
||||
}
|
||||
} else {
|
||||
if (actions.checkNeedToShowRemoveNetworkWarning(currency, source.network)) {
|
||||
showRemoveNetworkWarning(
|
||||
currency = currency,
|
||||
network = source.network,
|
||||
isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main,
|
||||
onConfirm = {
|
||||
actions.removeCurrency(batchKey, currency, source.network)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
actions.removeCurrency(batchKey, currency, source.network)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) {
|
||||
val message = ContentMessage { onDismiss ->
|
||||
CurrencyUnsupportedDialog(
|
||||
title = resourceReference(R.string.common_warning),
|
||||
message = when (unsupportedState) {
|
||||
is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_curve_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_curve_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
messageSender.send(message)
|
||||
}
|
||||
|
||||
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,67 @@
|
|||
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.domain.tokens.model.Network
|
||||
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,
|
||||
onLongTap: (SourceNetwork) -> Unit,
|
||||
): NetworksUM {
|
||||
return if (isExpanded) {
|
||||
NetworksUM.Expanded(
|
||||
networks = availableNetworks.map {
|
||||
it.toCurrencyNetworkModel(
|
||||
isSelected = it.network in addedIn,
|
||||
isEditable = isItemsEditable,
|
||||
onSelectedStateChange = onSelectedStateChange,
|
||||
onLongTap = onLongTap,
|
||||
)
|
||||
}.toImmutableList(),
|
||||
)
|
||||
} else {
|
||||
NetworksUM.Collapsed
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Network.toCurrencyNetworkModel(
|
||||
isSelected: Boolean,
|
||||
onSelectedStateChange: (Boolean) -> Unit,
|
||||
): CurrencyNetworkUM {
|
||||
return CurrencyNetworkUM(
|
||||
network = this,
|
||||
name = name,
|
||||
iconResId = id.getIconRes(isColored = true),
|
||||
isSelected = isSelected,
|
||||
type = standardType.name,
|
||||
onLongClick = {},
|
||||
isMainNetwork = false,
|
||||
onSelectedStateChange = onSelectedStateChange,
|
||||
)
|
||||
}
|
||||
|
||||
private fun SourceNetwork.toCurrencyNetworkModel(
|
||||
isSelected: Boolean,
|
||||
isEditable: Boolean,
|
||||
onSelectedStateChange: (SourceNetwork, Boolean) -> Unit,
|
||||
onLongTap: (SourceNetwork) -> Unit,
|
||||
): CurrencyNetworkUM {
|
||||
return CurrencyNetworkUM(
|
||||
network = network,
|
||||
name = network.name.uppercase(),
|
||||
iconResId = id.getIconRes(isColored = isSelected || !isEditable),
|
||||
isSelected = isSelected || !isEditable,
|
||||
type = typeName,
|
||||
onLongClick = { onLongTap(this) },
|
||||
isMainNetwork = this is SourceNetwork.Main,
|
||||
onSelectedStateChange = { selected ->
|
||||
onSelectedStateChange(this, selected)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.managetokens.utils.mapper
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
|
||||
import com.tangem.features.managetokens.entity.item.DerivationPathUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
internal fun Network.toDerivationPathModel(
|
||||
isSelected: Boolean,
|
||||
onSelectedStateChange: (Boolean) -> Unit,
|
||||
): DerivationPathUM? {
|
||||
return DerivationPathUM(
|
||||
id = id.value,
|
||||
value = derivationPath.value ?: return null,
|
||||
networkName = stringReference(name),
|
||||
isSelected = isSelected,
|
||||
onSelectedStateChange = onSelectedStateChange,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun SelectedNetwork.toDerivationPathModel(
|
||||
isSelected: Boolean,
|
||||
onSelectedStateChange: (Boolean) -> Unit,
|
||||
): DerivationPathUM? {
|
||||
return DerivationPathUM(
|
||||
id = id.value,
|
||||
value = derivationPath.value ?: return null,
|
||||
networkName = resourceReference(R.string.custom_token_derivation_path_default),
|
||||
isSelected = isSelected,
|
||||
onSelectedStateChange = onSelectedStateChange,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.managetokens.utils.mapper
|
||||
|
||||
import com.tangem.domain.managetokens.model.AddCustomTokenForm
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
|
||||
|
||||
internal fun CustomTokenFormUM.TokenFormUM.mapToDomainModel(): AddCustomTokenForm.Raw {
|
||||
return AddCustomTokenForm.Raw(
|
||||
contractAddress = contractAddress.value,
|
||||
symbol = symbol.value,
|
||||
name = name.value,
|
||||
decimals = decimals.value,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
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,
|
||||
onLongTap: (SourceNetwork) -> Unit,
|
||||
): CurrencyItemUM {
|
||||
if (currency !is ManagedCryptoCurrency.Token) return this
|
||||
|
||||
return when (this) {
|
||||
is CurrencyItemUM.Custom,
|
||||
is CurrencyItemUM.Loading,
|
||||
-> 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,
|
||||
onLongTap = onLongTap,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun CurrencyItemUM.update(currency: ManagedCryptoCurrency): CurrencyItemUM {
|
||||
return when (this) {
|
||||
is CurrencyItemUM.Custom,
|
||||
is CurrencyItemUM.Loading,
|
||||
-> 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.network in currency.addedIn
|
||||
|
||||
network.copy(
|
||||
iconResId = network.network.id.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 = network.id.getIconRes(isSelected),
|
||||
isSelected = isSelected,
|
||||
)
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
internal fun Network.ID.getIconRes(isColored: Boolean): Int = if (isColored) {
|
||||
getActiveIconRes(value)
|
||||
} else {
|
||||
getGreyedOutIconRes(value)
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
package com.tangem.features.managetokens.utils.ui
|
||||
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
|
||||
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal fun CustomTokenFormUM.updateTokenForm(
|
||||
block: CustomTokenFormUM.TokenFormUM.() -> CustomTokenFormUM.TokenFormUM,
|
||||
): CustomTokenFormUM {
|
||||
val form = tokenForm ?: return this
|
||||
|
||||
val updatedForm = form.block()
|
||||
|
||||
return copy(tokenForm = updatedForm)
|
||||
}
|
||||
|
||||
internal fun TextInputFieldUM.updateValue(
|
||||
value: String = this.value,
|
||||
error: TextReference? = this.error,
|
||||
isEnabled: Boolean = this.isEnabled,
|
||||
clearError: Boolean = false,
|
||||
): TextInputFieldUM {
|
||||
return copy(
|
||||
value = value,
|
||||
error = if (clearError) null else error,
|
||||
isEnabled = isEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithProgress(
|
||||
showProgress: Boolean,
|
||||
isWasFilled: Boolean = this.tokenForm?.wasFilled ?: false,
|
||||
canAddToken: Boolean = this.canAddToken,
|
||||
clearNotifications: Boolean = false,
|
||||
clearFieldErrors: Boolean = false,
|
||||
disableSecondaryFields: Boolean = false,
|
||||
): CustomTokenFormUM {
|
||||
return copy(
|
||||
isValidating = showProgress,
|
||||
canAddToken = canAddToken,
|
||||
notifications = if (clearNotifications) persistentListOf() else notifications,
|
||||
).updateTokenForm {
|
||||
copy(
|
||||
contractAddress = contractAddress.updateValue(
|
||||
clearError = clearFieldErrors,
|
||||
),
|
||||
name = name.updateValue(
|
||||
isEnabled = !showProgress && !disableSecondaryFields,
|
||||
clearError = clearFieldErrors,
|
||||
),
|
||||
symbol = symbol.updateValue(
|
||||
isEnabled = !showProgress && !disableSecondaryFields,
|
||||
clearError = clearFieldErrors,
|
||||
),
|
||||
decimals = decimals.updateValue(
|
||||
isEnabled = !showProgress && !disableSecondaryFields,
|
||||
clearError = clearFieldErrors,
|
||||
),
|
||||
wasFilled = isWasFilled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithCurrency(currency: CryptoCurrency): CustomTokenFormUM {
|
||||
return updateTokenForm {
|
||||
copy(
|
||||
contractAddress = contractAddress.updateValue(error = null),
|
||||
name = name.updateValue(currency.name),
|
||||
symbol = symbol.updateValue(currency.symbol),
|
||||
decimals = decimals.updateValue(currency.decimals.toString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithContractAddressException(
|
||||
exception: CustomTokenFormValidationException.ContractAddress,
|
||||
): CustomTokenFormUM {
|
||||
return updateTokenForm {
|
||||
copy(
|
||||
contractAddress = contractAddress.updateValue(
|
||||
error = when (exception) {
|
||||
CustomTokenFormValidationException.ContractAddress.Empty -> {
|
||||
null
|
||||
}
|
||||
CustomTokenFormValidationException.ContractAddress.Invalid -> {
|
||||
resourceReference(R.string.custom_token_creation_error_invalid_contract_address)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithDecimalsException(
|
||||
exception: CustomTokenFormValidationException.Decimals,
|
||||
): CustomTokenFormUM {
|
||||
return updateTokenForm {
|
||||
copy(
|
||||
decimals = decimals.updateValue(
|
||||
error = when (exception) {
|
||||
is CustomTokenFormValidationException.Decimals.Empty -> {
|
||||
null
|
||||
}
|
||||
is CustomTokenFormValidationException.Decimals.Invalid -> {
|
||||
resourceReference(
|
||||
R.string.custom_token_creation_error_wrong_decimals,
|
||||
wrappedList(ValidateTokenFormUseCase.MAX_DECIMALS),
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithCurrencyNotFoundNotification(): CustomTokenFormUM {
|
||||
val notification = CustomTokenFormUM.NotificationUM(
|
||||
id = "currency_not_found",
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(R.string.custom_token_validation_error_not_found_title),
|
||||
subtitle = resourceReference(R.string.custom_token_validation_error_not_found_description),
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
),
|
||||
)
|
||||
|
||||
return copy(
|
||||
notifications = notifications.mutate {
|
||||
it.add(notification)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithCurrencyAlreadyAddedNotification(): CustomTokenFormUM {
|
||||
val notification = CustomTokenFormUM.NotificationUM(
|
||||
id = "currency_already_added",
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(R.string.custom_token_creation_error_token_already_exist_title),
|
||||
subtitle = resourceReference(R.string.custom_token_creation_error_token_already_exist_message),
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
),
|
||||
)
|
||||
|
||||
return copy(
|
||||
notifications = notifications.mutate {
|
||||
it.add(notification)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
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.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
interface MarketsTokenDetailsComponent : ComposableContentComponent {
|
||||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val token: TokenMarketParams,
|
||||
val appCurrency: AppCurrency,
|
||||
val showPortfolio: Boolean,
|
||||
val analyticsParams: AnalyticsParams?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnalyticsParams(
|
||||
val blockchain: String?,
|
||||
val source: String,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
)
|
||||
|
||||
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,20 @@
|
|||
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 com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
interface TokenMarketBlockComponent : ComposableContentComponent {
|
||||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(appComponentContext: AppComponentContext, params: Params): TokenMarketBlockComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -17,9 +17,26 @@ dependencies {
|
|||
implementation(projects.core.navigation)
|
||||
|
||||
/* Domain */
|
||||
implementation(projects.domain.markets)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.markets)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
// Instead, create some kind of interface for such cases.
|
||||
/* Redux -_- */
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(deps.reKotlin)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.coil)
|
||||
|
|
@ -31,6 +48,7 @@ dependencies {
|
|||
implementation(deps.compose.ui.utils)
|
||||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.androidx.activity.compose)
|
||||
implementation(deps.markdown.composeview)
|
||||
|
||||
/* DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
@ -45,7 +63,15 @@ dependencies {
|
|||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
|
||||
/* Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.uiCharts)
|
||||
implementation(projects.common.routing)
|
||||
|
||||
/* Libs */
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
}
|
||||
|
|
@ -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,32 +0,0 @@
|
|||
package com.tangem.features.markets.details.api
|
||||
|
||||
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.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
interface MarketsTokenDetailsComponent {
|
||||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val token: TokenMarketSerializable,
|
||||
val appCurrency: AppCurrency,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext, params: Params, onBack: () -> Unit): MarketsTokenDetailsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,28 +1,85 @@
|
|||
package com.tangem.features.markets.details.impl
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.analytics.MarketDetailsAnalyticsEvent
|
||||
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,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent {
|
||||
|
||||
private val model: MarketsTokenDetailsModel = getOrCreateModel(params)
|
||||
// applying l2 compatibility
|
||||
private val updatedParams = params.copy(
|
||||
token = params.token.copy(
|
||||
id = getTokenIdIfL2Network(params.token.id),
|
||||
),
|
||||
)
|
||||
private val analyticsParams = params.analyticsParams
|
||||
|
||||
private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams)
|
||||
|
||||
private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.showPortfolio) {
|
||||
portfolioComponentFactory.create(
|
||||
context = child("my_portfolio"),
|
||||
params = MarketsPortfolioComponent.Params(
|
||||
updatedParams.token,
|
||||
analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) },
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
init {
|
||||
componentScope.launch {
|
||||
model.networksState.collectLatest {
|
||||
when (it) {
|
||||
is TokenNetworksState.NetworksAvailable -> portfolioComponent?.setTokenNetworks(it.networks)
|
||||
TokenNetworksState.NoNetworksAvailable -> portfolioComponent?.setNoNetworksAvailable()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Analytics ===
|
||||
if (analyticsParams != null) {
|
||||
analyticsEventHandler.send(
|
||||
MarketDetailsAnalyticsEvent.EventBuilder(
|
||||
token = params.token,
|
||||
).screenOpened(
|
||||
blockchain = analyticsParams.blockchain,
|
||||
source = analyticsParams.source,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheetContent(
|
||||
|
|
@ -41,23 +98,66 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
|
|||
val bsState by bottomSheetState
|
||||
|
||||
LaunchedEffect(bsState) {
|
||||
model.containerBottomSheetState.value = bsState
|
||||
model.isVisibleOnScreen.value = bsState == BottomSheetState.EXPANDED
|
||||
}
|
||||
|
||||
BackHandler(enabled = bsState == BottomSheetState.EXPANDED) {
|
||||
navigateBack()
|
||||
}
|
||||
|
||||
MarketsTokenDetailsContent(
|
||||
state = state,
|
||||
onBackClick = onBack,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
modifier = modifier,
|
||||
backgroundColor = LocalMainBottomSheetColor.current.value,
|
||||
addTopBarStatusBarPadding = false,
|
||||
state = state,
|
||||
onBackClick = {
|
||||
if (bsState == BottomSheetState.EXPANDED) {
|
||||
navigateBack()
|
||||
}
|
||||
},
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
portfolioBlock = portfolioComponent?.let { component ->
|
||||
{ blockModifier ->
|
||||
component.Content(blockModifier)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
BackHandler {
|
||||
navigateBack()
|
||||
}
|
||||
|
||||
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 = portfolioComponent?.let { component ->
|
||||
{ blockModifier ->
|
||||
component.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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.features.markets.details.impl.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
|
||||
internal class MarketDetailsAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) {
|
||||
|
||||
data class EventBuilder(
|
||||
val token: TokenMarketParams,
|
||||
) {
|
||||
fun screenOpened(blockchain: String?, source: String) = MarketDetailsAnalyticsEvent(
|
||||
event = "Token Chart Screen Opened",
|
||||
params = buildMap {
|
||||
put("Token", token.symbol)
|
||||
blockchain?.let { put("blockchain", it) }
|
||||
put("Source", source)
|
||||
},
|
||||
)
|
||||
|
||||
fun intervalChanged(intervalType: IntervalType, interval: PriceChangeInterval) = MarketDetailsAnalyticsEvent(
|
||||
event = "Button - Period",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
"Period" to interval.toAnalyticsString(),
|
||||
"Source" to intervalType.source,
|
||||
),
|
||||
)
|
||||
|
||||
fun readMoreClicked() = MarketDetailsAnalyticsEvent(
|
||||
event = "Button - Read More",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
),
|
||||
)
|
||||
|
||||
fun linkClicked(linkTitle: String) = MarketDetailsAnalyticsEvent(
|
||||
event = "Button - Links",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
"Link" to linkTitle,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
enum class IntervalType(val source: String) {
|
||||
Chart("Chart"),
|
||||
PricePerformance("Price"),
|
||||
Insights("Insights"),
|
||||
}
|
||||
}
|
||||
|
||||
private fun PriceChangeInterval.toAnalyticsString() = when (this) {
|
||||
PriceChangeInterval.H24 -> "24h"
|
||||
PriceChangeInterval.WEEK -> "7d"
|
||||
PriceChangeInterval.MONTH -> "1m"
|
||||
PriceChangeInterval.MONTH3 -> "3m"
|
||||
PriceChangeInterval.MONTH6 -> "6m"
|
||||
PriceChangeInterval.YEAR -> "1y"
|
||||
PriceChangeInterval.ALL_TIME -> "All"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ 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.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
|
|
@ -10,24 +14,26 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
|||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.analytics.MarketDetailsAnalyticsEvent
|
||||
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.QuotesStateUpdater
|
||||
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
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
|
|
@ -43,18 +49,21 @@ 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,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private var quotesJob = JobHolder()
|
||||
private val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>()
|
||||
private val analyticsEventBuilder = MarketDetailsAnalyticsEvent.EventBuilder(token = params.token)
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
|
|
@ -70,13 +79,36 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
onInfoClick = {
|
||||
showInfoBottomSheet(it)
|
||||
},
|
||||
onLinkClick = {
|
||||
urlOpener.openUrl(it.url)
|
||||
onLinkClick = { link ->
|
||||
urlOpener.openUrl(link.url)
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(analyticsEventBuilder.linkClicked(linkTitle = link.title))
|
||||
},
|
||||
// === Analytics ===
|
||||
onPricePerformanceIntervalChanged = {
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.intervalChanged(
|
||||
intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance,
|
||||
interval = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onInsightsIntervalChanged = {
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.intervalChanged(
|
||||
intervalType = MarketDetailsAnalyticsEvent.IntervalType.Insights,
|
||||
interval = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
// ==================
|
||||
)
|
||||
|
||||
private val descriptionConverter = DescriptionConverter(
|
||||
onReadModeClicked = {
|
||||
showInfoBottomSheet(it)
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(analyticsEventBuilder.readMoreClicked())
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -93,7 +125,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
BigDecimalFormatter.formatFiatPriceUncapped(
|
||||
fiatAmount = value,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = "",
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -113,10 +145,11 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
private var lastUpdatedTimestamp: Long = DateTime.now().millis
|
||||
private val currentTokenInfo = MutableStateFlow<TokenMarketInfo?>(null)
|
||||
private val lastUpdatedTimestamp = MutableStateFlow(DateTime.now().millis)
|
||||
|
||||
val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED)
|
||||
val isVisibleOnScreen = MutableStateFlow(false)
|
||||
val networksState = MutableStateFlow<TokenNetworksState>(TokenNetworksState.Loading)
|
||||
|
||||
val state = MutableStateFlow(
|
||||
MarketsTokenDetailsUM(
|
||||
|
|
@ -127,19 +160,16 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
),
|
||||
dateTimeText = resourceReference(R.string.common_today),
|
||||
priceChangePercentText = BigDecimalFormatter.formatPercent(
|
||||
percent = params.token.tokenQuotes.h24Percent,
|
||||
useAbsoluteValue = true,
|
||||
),
|
||||
priceChangeType = if (params.token.tokenQuotes.h24Percent < BigDecimal.ZERO) {
|
||||
PriceChangeType.DOWN
|
||||
} else {
|
||||
PriceChangeType.UP
|
||||
priceChangePercentText = params.token.tokenQuotes.h24Percent?.let {
|
||||
BigDecimalFormatter.formatPercent(
|
||||
percent = it,
|
||||
useAbsoluteValue = true,
|
||||
)
|
||||
},
|
||||
priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(),
|
||||
iconUrl = params.token.imageUrl,
|
||||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
dataProducer = chartDataProducer,
|
||||
chartLook = MarketChartLook(),
|
||||
onLoadRetryClick = ::onLoadRetryClicked,
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
onMarkerPointSelected = ::onMarkerPointSelected,
|
||||
|
|
@ -157,6 +187,22 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
private val quotesStateUpdater = QuotesStateUpdater(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
state = state,
|
||||
currentQuotes = currentQuotes,
|
||||
lastUpdatedTimestamp = lastUpdatedTimestamp,
|
||||
currentTokenInfo = currentTokenInfo,
|
||||
onPricePerformanceIntervalChanged = {
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.intervalChanged(
|
||||
intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance,
|
||||
interval = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
private val loadChartJobHolder = JobHolder()
|
||||
|
||||
init {
|
||||
|
|
@ -180,7 +226,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 +255,8 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
appCurrency = currentAppCurrency.value,
|
||||
interval = interval,
|
||||
tokenId = params.token.id,
|
||||
tokenSymbol = params.token.symbol,
|
||||
preview = false,
|
||||
)
|
||||
|
||||
state.update {
|
||||
|
|
@ -225,9 +273,9 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
chart.onRight {
|
||||
chartDataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.Data(
|
||||
x = it.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
y = it.priceY.toImmutableList(),
|
||||
)
|
||||
x = it.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
).sorted()
|
||||
|
||||
updateLook {
|
||||
it.copy(
|
||||
|
|
@ -242,6 +290,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 {
|
||||
|
|
@ -272,34 +325,11 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
val tokenMarketInfo = getTokenMarketInfoUseCase(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
tokenId = params.token.id,
|
||||
tokenSymbol = params.token.symbol,
|
||||
)
|
||||
|
||||
tokenMarketInfo.fold(
|
||||
ifRight = { result ->
|
||||
currentQuotes.value = result.quotes
|
||||
val percent = result.quotes.getPercentByInterval(interval = state.value.selectedInterval)
|
||||
state.update {
|
||||
it.copy(
|
||||
priceText = result.quotes.currentPrice.formatAsPrice(currentAppCurrency.value),
|
||||
priceChangePercentText = result.quotes.getFormattedPercentByInterval(
|
||||
interval = it.selectedInterval,
|
||||
),
|
||||
priceChangeType = percent.percentChangeType(),
|
||||
body = MarketsTokenDetailsUM.Body.Content(
|
||||
description = descriptionConverter.convert(result),
|
||||
infoBlocks = infoConverter.convert(result),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
chartDataProducer.runTransaction {
|
||||
updateLook {
|
||||
it.copy(
|
||||
type = getChartTypeByPercent(percent),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
ifRight = { result -> updateInfo(result) },
|
||||
ifLeft = {
|
||||
state.update {
|
||||
if (it.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) {
|
||||
|
|
@ -319,48 +349,54 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun updateQuotes(newQuotes: TokenQuotes) {
|
||||
val triggerPriceChangeType = getFormattedPriceChange(
|
||||
currentPrice = currentQuotes.value.currentPrice,
|
||||
updatedPrice = newQuotes.currentPrice,
|
||||
)
|
||||
val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) {
|
||||
triggeredEvent(
|
||||
data = triggerPriceChangeType,
|
||||
onConsume = {
|
||||
state.update { it.copy(triggerPriceChange = consumedEvent()) }
|
||||
},
|
||||
private fun updateInfo(newInfo: TokenMarketInfo) {
|
||||
lastUpdatedTimestamp.value = DateTime.now().millis
|
||||
|
||||
currentTokenInfo.value = newInfo
|
||||
currentQuotes.value = newInfo.quotes
|
||||
|
||||
val percent = newInfo.quotes.getPercentByInterval(interval = state.value.selectedInterval)
|
||||
|
||||
state.update {
|
||||
it.copy(
|
||||
priceText = newInfo.quotes.currentPrice.formatAsPrice(currentAppCurrency.value),
|
||||
priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval(
|
||||
interval = it.selectedInterval,
|
||||
),
|
||||
priceChangeType = percent.percentChangeType(),
|
||||
body = MarketsTokenDetailsUM.Body.Content(
|
||||
description = descriptionConverter.convert(newInfo),
|
||||
infoBlocks = infoConverter.convert(newInfo),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
consumedEvent()
|
||||
}
|
||||
|
||||
val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval)
|
||||
val priceChangeType = percent.percentChangeType()
|
||||
val networks = newInfo.networks?.filter {
|
||||
BlockchainUtils.isSupportedNetworkId(it.networkId)
|
||||
}
|
||||
|
||||
// wait until marker is removed
|
||||
state.first { it.markerSet.not() }
|
||||
|
||||
currentQuotes.value = newQuotes
|
||||
lastUpdatedTimestamp = DateTime.now().millis
|
||||
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency.value),
|
||||
priceChangePercentText = newQuotes.getFormattedPercentByInterval(
|
||||
interval = stateToUpdate.selectedInterval,
|
||||
),
|
||||
priceChangeType = priceChangeType,
|
||||
triggerPriceChange = trigger,
|
||||
dateTimeText = getDefaultDateTimeString(stateToUpdate.selectedInterval),
|
||||
)
|
||||
networksState.value = if (networks.isNullOrEmpty()) {
|
||||
TokenNetworksState.NoNetworksAvailable
|
||||
} else {
|
||||
TokenNetworksState.NetworksAvailable(networks)
|
||||
}
|
||||
|
||||
chartDataProducer.runTransaction {
|
||||
updateLook {
|
||||
it.copy(
|
||||
type = getChartTypeByPercent(percent),
|
||||
)
|
||||
it.copy(type = percent.percentChangeType().toChartType())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateQuotes(newQuotes: TokenQuotes) {
|
||||
quotesStateUpdater.updateQuotes(newQuotes)
|
||||
|
||||
val percent = newQuotes
|
||||
.getPercentByInterval(interval = state.value.selectedInterval)
|
||||
|
||||
chartDataProducer.runTransaction {
|
||||
updateLook {
|
||||
it.copy(type = percent.percentChangeType().toChartType())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -368,6 +404,15 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
|
||||
if (state.value.selectedInterval == interval) return
|
||||
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.intervalChanged(
|
||||
intervalType = MarketDetailsAnalyticsEvent.IntervalType.Chart,
|
||||
interval = interval,
|
||||
),
|
||||
)
|
||||
// ==================
|
||||
|
||||
val quotes = currentQuotes.value
|
||||
val priceChangePercent = quotes.getFormattedPercentByInterval(interval)
|
||||
|
||||
|
|
@ -428,7 +473,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
chartDataProducer.runTransaction {
|
||||
updateLook {
|
||||
it.copy(
|
||||
type = getChartTypeByPercent(percent),
|
||||
type = percent.percentChangeType().toChartType(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -475,9 +520,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
launch {
|
||||
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
|
||||
// Update quotes only when content is visible on the screen
|
||||
isVisibleOnScreen.first { it }
|
||||
|
||||
loadQuotes()
|
||||
|
|
@ -490,7 +533,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
interval = interval,
|
||||
startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval(
|
||||
interval = interval,
|
||||
currentTimestamp = lastUpdatedTimestamp,
|
||||
currentTimestamp = lastUpdatedTimestamp.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ internal class DescriptionConverter(
|
|||
),
|
||||
),
|
||||
body = stringReference(value.fullDescription ?: ""),
|
||||
showGeneratedAINotification = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ package com.tangem.features.markets.details.impl.model.converters
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
|
|
@ -14,13 +15,14 @@ import com.tangem.utils.Provider
|
|||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Stable
|
||||
internal class InsightsConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
private val onIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Insights, InsightsUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.Insights): InsightsUM {
|
||||
|
|
@ -48,10 +50,14 @@ internal class InsightsConverter(
|
|||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_insights),
|
||||
body = stringReference("//TODO"),
|
||||
body = resourceReference(
|
||||
R.string.markets_insights_info_description_message,
|
||||
wrappedList(value.sourceNetworks.joinToString { it.name }),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
onIntervalChanged = onIntervalChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -62,74 +68,79 @@ internal class InsightsConverter(
|
|||
liquidityChange: BigDecimal?,
|
||||
buyPressureChange: BigDecimal?,
|
||||
): ImmutableList<InfoPointUM> {
|
||||
return persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = experiencedBuyerChange.convertChange(),
|
||||
change = experiencedBuyerChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
body = resourceReference(R.string.markets_token_details_experienced_buyers_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = buyPressureChange.convertChange(isFiatValue = true),
|
||||
change = buyPressureChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
body = resourceReference(R.string.markets_token_details_buy_pressure_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = holdersChange.convertChange(),
|
||||
change = holdersChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
body = resourceReference(R.string.markets_token_details_holders_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = liquidityChange.convertChange(),
|
||||
change = liquidityChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
body = resourceReference(R.string.markets_token_details_liquidity_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
return listOfNotNull(
|
||||
experiencedBuyerChange?.let {
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = experiencedBuyerChange.convertChange(),
|
||||
change = experiencedBuyerChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
body = resourceReference(R.string.markets_token_details_experienced_buyers_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
buyPressureChange?.let {
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = buyPressureChange.convertChange(isFiatValue = true),
|
||||
change = buyPressureChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
body = resourceReference(R.string.markets_token_details_buy_pressure_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
holdersChange?.let {
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = holdersChange.convertChange(),
|
||||
change = holdersChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
body = resourceReference(R.string.markets_token_details_holders_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
liquidityChange?.let {
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = liquidityChange.convertChange(),
|
||||
change = liquidityChange.changeType(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
body = resourceReference(R.string.markets_token_details_liquidity_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
private fun BigDecimal?.changeType(): InfoPointUM.ChangeType? {
|
||||
private fun BigDecimal.changeType(): InfoPointUM.ChangeType? {
|
||||
return when {
|
||||
this == null -> null
|
||||
this > BigDecimal.ZERO -> InfoPointUM.ChangeType.UP
|
||||
this < BigDecimal.ZERO -> InfoPointUM.ChangeType.DOWN
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal?.convertChange(isFiatValue: Boolean = false): String {
|
||||
if (this == null) return StringsSigns.DASH_SIGN
|
||||
|
||||
private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String {
|
||||
val value = if (isFiatValue) {
|
||||
val currency = appCurrency()
|
||||
BigDecimalFormatter.formatCompactFiatAmount(
|
||||
|
|
@ -141,11 +152,9 @@ internal class InsightsConverter(
|
|||
BigDecimalFormatter.formatCompactAmount(amount = this.abs())
|
||||
}
|
||||
|
||||
val spacing = if (isFiatValue) " " else ""
|
||||
|
||||
return when {
|
||||
this > BigDecimal.ZERO -> StringsSigns.PLUS + spacing + value
|
||||
this < BigDecimal.ZERO -> StringsSigns.MINUS + spacing + value
|
||||
this > BigDecimal.ZERO -> StringsSigns.PLUS + value
|
||||
this < BigDecimal.ZERO -> StringsSigns.MINUS + value
|
||||
this == BigDecimal.ZERO -> value
|
||||
else -> StringsSigns.DASH_SIGN
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM.Link
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -25,7 +25,7 @@ internal class LinksConverter(
|
|||
|
||||
private fun TokenMarketInfo.Link.convert(): LinksUM.Link {
|
||||
return LinksUM.Link(
|
||||
title = stringReference(title),
|
||||
title = title,
|
||||
iconRes = getIconById(id),
|
||||
url = link,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ import com.tangem.utils.StringsSigns
|
|||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
@Stable
|
||||
internal class MetricsConverter(
|
||||
|
|
@ -115,20 +112,16 @@ internal class MetricsConverter(
|
|||
}
|
||||
|
||||
private fun BigDecimal?.formatAmount(crypto: Boolean = false): String {
|
||||
if (this == null) return StringsSigns.DASH_SIGN
|
||||
|
||||
return if (crypto) {
|
||||
val formatter = NumberFormat.getNumberInstance(Locale.getDefault()).apply {
|
||||
maximumFractionDigits = 0
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
formatter.format(this)
|
||||
BigDecimalFormatter.formatCompactAmount(amount = this)
|
||||
} else {
|
||||
val currency = appCurrency()
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = this,
|
||||
BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = this,
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
decimals = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,29 +3,31 @@ package com.tangem.features.markets.details.impl.model.converters
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
@Stable
|
||||
internal class PricePerformanceConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
) : Converter<TokenMarketInfo.PricePerformance, PricePerformanceUM> {
|
||||
private val onIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.PricePerformance): PricePerformanceUM {
|
||||
fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM {
|
||||
return PricePerformanceUM(
|
||||
h24 = value.day.convert(),
|
||||
month = value.month.convert(),
|
||||
all = value.allTime.convert(),
|
||||
h24 = value.day.convert(currentPrice),
|
||||
month = value.month.convert(currentPrice),
|
||||
all = value.allTime.convert(currentPrice),
|
||||
onIntervalChanged = onIntervalChanged,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Range?.convert(): PricePerformanceUM.Value {
|
||||
if (this == null) {
|
||||
private fun TokenMarketInfo.Range?.convert(currentPrice: BigDecimal): PricePerformanceUM.Value {
|
||||
if (this == null || this.low == null || this.high == null) {
|
||||
return PricePerformanceUM.Value(
|
||||
low = StringsSigns.DASH_SIGN,
|
||||
high = StringsSigns.DASH_SIGN,
|
||||
|
|
@ -36,7 +38,7 @@ internal class PricePerformanceConverter(
|
|||
return PricePerformanceUM.Value(
|
||||
low = low.convert(),
|
||||
high = high.convert(),
|
||||
indicatorFraction = calculateFraction(),
|
||||
indicatorFraction = calculateFraction(currentPrice),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -50,10 +52,15 @@ internal class PricePerformanceConverter(
|
|||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Range.calculateFraction(): Float {
|
||||
if (low == null || high == null || low == BigDecimal.ZERO) return 0f
|
||||
return (high!! - low!!).divide(low!!, RoundingMode.HALF_UP)
|
||||
.setScale(2, RoundingMode.HALF_UP)
|
||||
.toFloat().coerceAtMost(1f)
|
||||
private fun TokenMarketInfo.Range.calculateFraction(currentPrice: BigDecimal): Float {
|
||||
return when {
|
||||
low == null || high == null || high == BigDecimal.ZERO || currentPrice < low -> 0f
|
||||
currentPrice > high || low == high -> 1f
|
||||
else -> {
|
||||
(currentPrice - low!!).divide(high!! - low!!, RoundingMode.HALF_UP)
|
||||
.setScale(2, RoundingMode.HALF_UP)
|
||||
.toFloat().coerceAtMost(1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.markets.details.impl.model.converters
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM
|
||||
|
|
@ -14,20 +15,37 @@ internal class TokenMarketInfoConverter(
|
|||
appCurrency: Provider<AppCurrency>,
|
||||
onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
onLinkClick: (LinksUM.Link) -> Unit,
|
||||
onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
onInsightsIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.InformationBlocks> {
|
||||
|
||||
private val insightsConverter = InsightsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
|
||||
private val insightsConverter = InsightsConverter(
|
||||
appCurrency = appCurrency,
|
||||
onInfoClick = onInfoClick,
|
||||
onIntervalChanged = onInsightsIntervalChanged,
|
||||
)
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
// TODO second markets iteration
|
||||
private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick)
|
||||
private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
|
||||
private val pricePerformanceConverter = PricePerformanceConverter(appCurrency = appCurrency)
|
||||
private val pricePerformanceConverter = PricePerformanceConverter(
|
||||
appCurrency = appCurrency,
|
||||
onIntervalChanged = onPricePerformanceIntervalChanged,
|
||||
)
|
||||
private val linksConverter = LinksConverter(onLinkClick = onLinkClick)
|
||||
|
||||
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks {
|
||||
return MarketsTokenDetailsUM.InformationBlocks(
|
||||
insights = value.insights?.let { insightsConverter.convert(it) },
|
||||
securityScore = securityScoreConverter.convert(Unit),
|
||||
securityScore = null,
|
||||
metrics = value.metrics?.let { metricsConverter.convert(it) },
|
||||
pricePerformance = value.pricePerformance?.let { pricePerformanceConverter.convert(it) },
|
||||
pricePerformance = value.pricePerformance?.let {
|
||||
pricePerformanceConverter.convert(
|
||||
value = it,
|
||||
currentPrice = value.quotes.currentPrice,
|
||||
)
|
||||
},
|
||||
links = value.links?.let { linksConverter.convert(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
|
||||
}
|
||||
}
|
||||
|
|
@ -67,10 +69,8 @@ internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: Bi
|
|||
}
|
||||
|
||||
internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType {
|
||||
val updatedPriceDecimals = BigDecimalFormatter.getProperFiatPriceDecimals(updatedPrice)
|
||||
|
||||
val current = currentPrice.setScale(updatedPriceDecimals, RoundingMode.HALF_UP)
|
||||
val updated = updatedPrice.setScale(updatedPriceDecimals, RoundingMode.HALF_UP)
|
||||
val current = BigDecimalFormatter.getFiatPriceUncappedWithScale(value = currentPrice).first
|
||||
val updated = BigDecimalFormatter.getFiatPriceUncappedWithScale(value = updatedPrice).first
|
||||
|
||||
return when {
|
||||
updated > current -> PriceChangeType.UP
|
||||
|
|
@ -85,15 +85,4 @@ internal fun PriceChangeType.toChartType(): MarketChartLook.Type {
|
|||
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
|
||||
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal fun getChartTypeByPercent(percent: BigDecimal?): MarketChartLook.Type {
|
||||
val scaled = percent?.setScale(4, RoundingMode.HALF_UP)
|
||||
return when {
|
||||
scaled == null -> return MarketChartLook.Type.Neutral
|
||||
scaled > BigDecimal.ZERO -> MarketChartLook.Type.Growing
|
||||
scaled < BigDecimal.ZERO -> MarketChartLook.Type.Falling
|
||||
else -> MarketChartLook.Type.Neutral
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ internal object MarketsDateTimeFormatters {
|
|||
|
||||
private val dateFormatter = DateTimeFormatters.dateDDMMYYYY
|
||||
|
||||
internal fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
|
||||
fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> { value: BigDecimal ->
|
||||
value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter)
|
||||
|
|
@ -44,7 +44,7 @@ internal object MarketsDateTimeFormatters {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference {
|
||||
fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> resourceReference(R.string.common_today)
|
||||
PriceChangeInterval.WEEK,
|
||||
|
|
@ -78,10 +78,7 @@ internal object MarketsDateTimeFormatters {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun formatDateByIntervalWithMarker(
|
||||
interval: PriceChangeInterval,
|
||||
markerTimestamp: BigDecimal,
|
||||
): TextReference {
|
||||
fun formatDateByIntervalWithMarker(interval: PriceChangeInterval, markerTimestamp: BigDecimal): TextReference {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.WEEK,
|
||||
|
|
@ -127,4 +124,14 @@ internal object MarketsDateTimeFormatters {
|
|||
PriceChangeInterval.ALL_TIME -> 0
|
||||
}
|
||||
}
|
||||
|
||||
fun getDefaultDateTimeString(interval: PriceChangeInterval, currentTimestamp: Long): TextReference {
|
||||
return formatDateByInterval(
|
||||
interval = interval,
|
||||
startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval(
|
||||
interval = interval,
|
||||
currentTimestamp = currentTimestamp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.features.markets.details.impl.model.state
|
||||
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenQuotes
|
||||
import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter
|
||||
import com.tangem.features.markets.details.impl.model.formatter.*
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class QuotesStateUpdater(
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val state: MutableStateFlow<MarketsTokenDetailsUM>,
|
||||
private val currentQuotes: MutableStateFlow<TokenQuotes>,
|
||||
private val lastUpdatedTimestamp: MutableStateFlow<Long>,
|
||||
private val currentTokenInfo: MutableStateFlow<TokenMarketInfo?>,
|
||||
private val onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
) {
|
||||
private val pricePerformanceConverter = PricePerformanceConverter(
|
||||
currentAppCurrency,
|
||||
onIntervalChanged = onPricePerformanceIntervalChanged,
|
||||
)
|
||||
|
||||
suspend fun updateQuotes(newQuotes: TokenQuotes) {
|
||||
val triggerPriceChangeType = getFormattedPriceChange(
|
||||
currentPrice = currentQuotes.value.currentPrice,
|
||||
updatedPrice = newQuotes.currentPrice,
|
||||
)
|
||||
val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) {
|
||||
triggeredEvent(
|
||||
data = triggerPriceChangeType,
|
||||
onConsume = {
|
||||
state.update { it.copy(triggerPriceChange = consumedEvent()) }
|
||||
},
|
||||
)
|
||||
} else {
|
||||
consumedEvent()
|
||||
}
|
||||
|
||||
val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval)
|
||||
val priceChangeType = percent.percentChangeType()
|
||||
|
||||
// wait until marker is removed
|
||||
state.first { it.markerSet.not() }
|
||||
|
||||
currentQuotes.value = newQuotes
|
||||
lastUpdatedTimestamp.value = DateTime.now().millis
|
||||
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency()),
|
||||
priceChangePercentText = newQuotes.getFormattedPercentByInterval(
|
||||
interval = stateToUpdate.selectedInterval,
|
||||
),
|
||||
priceChangeType = priceChangeType,
|
||||
triggerPriceChange = trigger,
|
||||
dateTimeText = MarketsDateTimeFormatters.getDefaultDateTimeString(
|
||||
stateToUpdate.selectedInterval,
|
||||
currentTimestamp = lastUpdatedTimestamp.value,
|
||||
),
|
||||
body = stateToUpdate.body.updatePricePerformance(newQuotes.currentPrice),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MarketsTokenDetailsUM.Body.updatePricePerformance(price: BigDecimal): MarketsTokenDetailsUM.Body {
|
||||
val currentPricePerformance = currentTokenInfo.value?.pricePerformance ?: return this
|
||||
|
||||
return if (this is MarketsTokenDetailsUM.Body.Content) {
|
||||
copy(
|
||||
infoBlocks = infoBlocks.copy(
|
||||
pricePerformance = pricePerformanceConverter.convert(currentPricePerformance, price),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
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