Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-13 16:42:47 +03:00
commit 43c11d0460
384 changed files with 19228 additions and 3308 deletions

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.child.managetokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
@ -48,9 +49,11 @@ internal class AddAndManageBottomSheetComponent(
@Composable
override fun BottomSheet() {
val portfolioSelectorSlot by portfolioSelectorSlot.subscribeAsState()
val state by model.state.collectAsStateWithLifecycle()
AddAndManageBottomSheetContent(
onAddTokensClick = model::onAddTokensClick,
shouldShowOrganizeButton = state.shouldShowOrganize,
onOrganizeTokensClick = model::onOrganizeTokensClick,
onDismiss = ::dismiss,
)

View file

@ -7,6 +7,8 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.account.models.hasMultiCurrencyAccount
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.models.account.AccountId
import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent
import com.tangem.feature.wallet.child.managetokens.analytics.PortfolioAnalyticsEvent
@ -14,7 +16,10 @@ import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -25,18 +30,20 @@ internal class AddAndManageModel @Inject constructor(
private val portfolioFetcherFactory: PortfolioFetcher.Factory,
private val analyticsEventHandler: AnalyticsEventHandler,
val portfolioSelectorController: PortfolioSelectorController,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
) : Model() {
private val params = paramsContainer.require<AddAndManageBottomSheetComponent.Params>()
val portfolioSelectorNavigation: SlotNavigation<Unit> = SlotNavigation()
val portfolioFetcher: PortfolioFetcher by lazy {
portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.Wallet(params.userWalletId),
scope = modelScope,
)
}
val state: StateFlow<AddAndManageState>
field = MutableStateFlow(AddAndManageState(shouldShowOrganize = true))
val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback {
override val onDismiss: () -> Unit = { portfolioSelectorNavigation.dismiss() }
@ -45,6 +52,7 @@ internal class AddAndManageModel @Inject constructor(
init {
observeAccountSelection()
updateShouldShowOrganizeButtonState()
}
fun onAddTokensClick() {
@ -85,4 +93,12 @@ internal class AddAndManageModel @Inject constructor(
}
}
}
private fun updateShouldShowOrganizeButtonState() {
modelScope.launch {
val accountStatusesList = singleAccountStatusListSupplier.getSyncOrNull(params.userWalletId)
val hasMultiCurrencyAccount = accountStatusesList?.hasMultiCurrencyAccount() == true
state.update { it.copy(shouldShowOrganize = hasMultiCurrencyAccount) }
}
}
}

View file

@ -0,0 +1,5 @@
package com.tangem.feature.wallet.child.managetokens.model
data class AddAndManageState(
val shouldShowOrganize: Boolean,
)

View file

@ -31,6 +31,7 @@ import com.tangem.core.ui.res.TangemThemePreview
@Composable
internal fun AddAndManageBottomSheetContent(
onAddTokensClick: () -> Unit,
shouldShowOrganizeButton: Boolean,
onOrganizeTokensClick: () -> Unit,
onDismiss: () -> Unit,
) {
@ -53,6 +54,7 @@ internal fun AddAndManageBottomSheetContent(
content = {
AddAndManageContent(
onAddTokensClick = onAddTokensClick,
shouldShowOrganizeButton = shouldShowOrganizeButton,
onOrganizeTokensClick = onOrganizeTokensClick,
)
},
@ -60,7 +62,11 @@ internal fun AddAndManageBottomSheetContent(
}
@Composable
private fun AddAndManageContent(onAddTokensClick: () -> Unit, onOrganizeTokensClick: () -> Unit) {
private fun AddAndManageContent(
onAddTokensClick: () -> Unit,
shouldShowOrganizeButton: Boolean,
onOrganizeTokensClick: () -> Unit,
) {
Column(
modifier = Modifier.padding(
start = 16.dp,
@ -75,23 +81,25 @@ private fun AddAndManageContent(onAddTokensClick: () -> Unit, onOrganizeTokensCl
onClick = onAddTokensClick,
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = 0,
lastIndex = 1,
addDefaultPadding = false,
backgroundColor = TangemTheme.colors.background.action,
),
)
AddAndManageRow(
iconRes = R.drawable.ic_filter_default_24,
title = ResR.string.add_and_manage_sheet_organize_title,
subtitle = ResR.string.add_and_manage_sheet_organize_subtitle,
onClick = onOrganizeTokensClick,
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = 1,
lastIndex = 1,
lastIndex = if (shouldShowOrganizeButton) 1 else 0,
addDefaultPadding = false,
backgroundColor = TangemTheme.colors.background.action,
),
)
if (shouldShowOrganizeButton) {
AddAndManageRow(
iconRes = R.drawable.ic_filter_default_24,
title = ResR.string.add_and_manage_sheet_organize_title,
subtitle = ResR.string.add_and_manage_sheet_organize_subtitle,
onClick = onOrganizeTokensClick,
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = 1,
lastIndex = 1,
addDefaultPadding = false,
backgroundColor = TangemTheme.colors.background.action,
),
)
}
}
}
@ -152,6 +160,7 @@ private fun AddAndManageBottomSheetContent_Preview() {
TangemThemePreview {
AddAndManageContent(
onAddTokensClick = {},
shouldShowOrganizeButton = true,
onOrganizeTokensClick = {},
)
}

View file

@ -7,6 +7,8 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.event.consumedEvent
@ -268,11 +270,15 @@ internal class OrganizeTokensModel @Inject constructor(
text = resourceReference(R.string.common_cancel),
onClick = ::onCancelClick,
type = TangemButtonType.Secondary,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X12,
),
applyButton = TangemButtonUM(
text = resourceReference(R.string.common_apply),
onClick = ::onApplyClick,
type = TangemButtonType.Primary,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X12,
),
scrollListToTop = consumedEvent(),
isBalanceHidden = true,

View file

@ -35,6 +35,7 @@ internal class OrganizeTokensListConverter(
return value.accountStatuses
.asSequence()
.filterCryptoPortfolio()
.filter { it.tokenList !is TokenList.Empty }
.flatMap { accountStatus ->
buildList {
addIf(

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
@ -33,8 +34,14 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.ds.button.TangemButton
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.ds.row.header.TangemHeaderRow
import com.tangem.core.ui.ds.row.token.TangemTokenRow
import com.tangem.core.ui.ds.row.internal.TangemRowTail
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.ds.row.token.internal.TokenRowEndContent
import com.tangem.core.ui.ds.row.token.internal.TokenRowTitle
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarActionContent
import com.tangem.core.ui.ds.topbar.TangemTopBarActionUM
@ -44,6 +51,7 @@ import com.tangem.core.ui.reordarable.ReorderableItem
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.utils.lazyListItemPosition
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM
@ -219,7 +227,7 @@ private fun LazyItemScope.DraggableItem(
headerRowUM = item.headerRowUM,
isBalanceHidden = isBalanceHidden,
)
is OrganizeRowItemUM.Token -> TangemTokenRow(
is OrganizeRowItemUM.Token -> OrganizeTokenRow(
modifier = modifierWithBackground,
tokenRowUM = item.tokenRowUM,
reorderableState = reorderableState,
@ -291,6 +299,55 @@ private fun Modifier.applyShapeAndShadow(roundingMode: RoundingModeUM, showShado
}
}
@Composable
private fun OrganizeTokenRow(
tokenRowUM: TangemTokenRowUM,
isBalanceHidden: Boolean,
reorderableState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
) {
TangemRowContainer(
content = {
TangemIcon(
tangemIconUM = tokenRowUM.headIconUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x3)
.size(TangemTheme.dimens2.x10)
.testTag(tag = TokenElementsTestTags.TOKEN_ICON),
)
TokenRowTitle(
titleUM = tokenRowUM.titleUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.START_TOP)
.padding(end = TangemTheme.dimens2.x2)
.testTag(tag = TokenElementsTestTags.TOKEN_TITLE),
)
TokenRowEndContent(
endContentUM = tokenRowUM.topEndContentUM,
isBalanceHidden = isBalanceHidden,
textStyle = TangemTheme.typography2.captionSemibold12,
textColor = TangemTheme.colors2.text.neutral.secondary,
placeholderWidth = TangemTheme.dimens2.x11,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.START_BOTTOM)
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
)
TangemRowTail(
tangemRowTailUM = tokenRowUM.tailUM,
reorderableState = reorderableState,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.TAIL)
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
)
},
modifier = modifier,
)
}
@Composable
@ReadOnlyComposable
private fun getItemGap(roundingMode: RoundingModeUM): PaddingValues {

View file

@ -243,10 +243,6 @@ internal class WalletModel @Inject constructor(
} else {
null
}
val isBackedUp = when (selectedWallet) {
is UserWallet.Cold -> selectedWallet.scanResponse.card.backupStatus?.isActive == true
is UserWallet.Hot -> selectedWallet.backedUp
}
val result = getAppThemeModeUseCase().firstOrNull()
val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM
val appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }.code
@ -254,7 +250,7 @@ internal class WalletModel @Inject constructor(
WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(
hasMobileWallet = hasMobileWallet,
accountsCount = accountsCount,
isBackedUp = isBackedUp,
isBackedUp = selectedWallet.isBackedUpForAnalytics(),
theme = theme.value,
isImported = selectedWallet.isImported(),
referralId = appsFlyerStore.get()?.refcode,

View file

@ -18,9 +18,9 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.TangemPayEntryPoint
@ -40,7 +40,7 @@ internal interface TangemPayIntents {
fun onRefreshPayToken(userWallet: UserWallet)
fun openDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig)
fun openDetails(status: AccountStatus.Payment)
fun onKycProgressClicked(userWalletId: UserWalletId)
@ -110,11 +110,8 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
}
}
override fun openDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) {
router.openTangemPayDetails(
userWalletId = userWalletId,
config = config,
)
override fun openDetails(status: AccountStatus.Payment) {
router.openTangemPayDetails(status = status)
}
override fun onKycProgressClicked(userWalletId: UserWalletId) {

View file

@ -12,13 +12,13 @@ import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.qrscanning.models.QrSendTarget
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.qrscanning.models.QrSendTarget
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent
@ -142,8 +142,8 @@ internal class DefaultWalletRouter @Inject constructor(
router.push(route = AppRoute.TangemPayOnboarding(mode = mode))
}
override fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) {
router.push(AppRoute.TangemPayDetails(userWalletId = userWalletId, config = config))
override fun openTangemPayDetails(status: AccountStatus.Payment) {
router.push(AppRoute.TangemPayDetails(status = status))
}
override fun openYieldSupplyBottomSheet(

View file

@ -7,13 +7,13 @@ import com.tangem.common.routing.AppRoute
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.qrscanning.models.QrSendTarget
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.qrscanning.models.QrSendTarget
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent
@ -82,7 +82,7 @@ internal interface InnerWalletRouter {
fun openTangemPayOnboarding(mode: AppRoute.TangemPayOnboarding.Mode)
fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig)
fun openTangemPayDetails(status: AccountStatus.Payment)
/** Open BS abput yield supply active and all money deposited in AAVE */
fun openYieldSupplyBottomSheet(

View file

@ -1,56 +1,14 @@
package com.tangem.feature.wallet.presentation.wallet.analytics
import com.tangem.core.analytics.models.*
import com.tangem.domain.models.wallet.UserWalletId
sealed class WalletScreenAnalyticsEvent {
sealed class Basic(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Basic", event = event, params = params) {
class WalletToppedUp(userWalletId: UserWalletId, walletType: AnalyticsParam.WalletType) :
Basic(
event = "Topped up",
params = mapOf(AnalyticsParam.CURRENCY to walletType.value),
),
OneTimeAnalyticsEvent, AppsFlyerIncludedEvent {
override val oneTimeEventId: String = id + userWalletId.stringValue
}
class CardWasScanned(source: AnalyticsParam.ScreensSources) : Basic(
event = "Card Was Scanned",
params = mapOf(
AnalyticsParam.SOURCE to source.value,
),
)
class BalanceLoaded(balance: AnalyticsParam.CardBalanceState, tokensCount: Int?) : Basic(
event = "Balance Loaded",
params = buildMap {
put(AnalyticsParam.BALANCE, balance.value)
tokensCount?.let { put(AnalyticsParam.TOKENS_COUNT, it.toString()) }
},
), AppsFlyerIncludedEvent
class TokenBalance(balance: AnalyticsParam.EmptyFull, token: String) : Basic(
event = "Token Balance",
params = mapOf(
AnalyticsParam.STATE to balance.value,
AnalyticsParam.TOKEN_PARAM to token,
),
)
}
sealed class MainScreen(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Main Screen", event = event, params = params) {
class ScreenOpenedLegacy : MainScreen(event = "Screen opened")
data class ScreenOpened(
private val hasMobileWallet: Boolean,
private val accountsCount: Int?,

View file

@ -8,6 +8,7 @@ import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.extensions.isZero
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase
@ -18,7 +19,6 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
@ -206,7 +206,12 @@ internal class TokenListAnalyticsSender @Inject constructor(
AnalyticsParam.WalletType.SingleCurrency(currency.currency.name)
}
analyticsEventHandler.send(Basic.WalletToppedUp(userWallet.walletId, walletType))
analyticsEventHandler.send(
Basic.ToppedUp(
userWalletId = userWallet.walletId.stringValue,
walletType = walletType,
),
)
}
}

View file

@ -12,16 +12,12 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
import java.util.Currency
private const val POLYGON_CHAIN_ID = 137
internal class TangemPayMainBlockConverter(
private val tangemPayClickIntents: TangemPayIntents,
private val isRedesignEnabled: Boolean,
@ -63,24 +59,9 @@ internal class TangemPayMainBlockConverter(
currencyCode = statusValue.fiatBalance.currency,
balance = statusValue.fiatBalance.availableBalance,
),
balanceSubtitle = stringReference("USDC"), // TODO hardcode for now
balanceSubtitle = stringReference(statusValue.cryptoCurrency.symbol),
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,
onClick = {
// Dummy config for deactivated account just to open details screen
tangemPayClickIntents.openDetails(
userWalletId = value.account.userWalletId,
config = TangemPayDetailsConfig(
customerId = "",
cardId = "",
isPinSet = false,
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
cardNumberEnd = "",
chainId = POLYGON_CHAIN_ID,
displayName = null,
isTangemPayDeactivated = true,
),
)
},
onClick = { tangemPayClickIntents.openDetails(value) },
)
is PaymentAccountStatusValue.Loaded -> {
val card = statusValue.cards.firstOrNull() ?: return TangemPayMainUM.TemporaryUnavailable
@ -91,27 +72,9 @@ internal class TangemPayMainBlockConverter(
currencyCode = statusValue.currencyCode,
balance = statusValue.fiatBalance.availableBalance,
),
balanceSubtitle = stringReference("USDC"), // TODO hardcode for now
balanceSubtitle = stringReference(statusValue.cryptoCurrency.symbol),
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,
onClick = {
tangemPayClickIntents.openDetails(
value.account.userWalletId,
TangemPayDetailsConfig(
customerId = statusValue.customerId,
cardId = card.id,
isPinSet = card.hasPinCode,
cardFrozenState = if (card.isFrozen) {
TangemPayCardFrozenState.Frozen
} else {
TangemPayCardFrozenState.Unfrozen
},
cardNumberEnd = card.lastDigits,
chainId = POLYGON_CHAIN_ID,
displayName = card.displayName,
isTangemPayDeactivated = false,
),
)
},
onClick = { tangemPayClickIntents.openDetails(value) },
)
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.models.hasMultiCurrencyAccount
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.TotalFiatBalance
@ -40,7 +41,7 @@ internal class TokenListStateConverter(
private val clickIntents: WalletClickIntents,
private val yieldModuleApyMap: Map<String, BigDecimal>,
private val stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability>,
private val shouldShowMainPromo: Boolean,
shouldShowMainPromo: Boolean,
private val isAddAndManageTokensEnabled: Boolean,
) : Converter<WalletTokensListState, WalletTokensListState> {
@ -169,7 +170,8 @@ internal class TokenListStateConverter(
}
private fun getOrganizeTokensButtonStateV2(accountList: AccountStatusList): WalletOrganizeTokensButtonConfig? {
return if (accountList.flattenCurrencies().size > 1 && !isSingleCurrencyWalletWithToken()) {
val shouldShowOrganizeIfOldButton = accountList.hasMultiCurrencyAccount() || isAddAndManageTokensEnabled
return if (shouldShowOrganizeIfOldButton && !isSingleCurrencyWalletWithToken()) {
WalletOrganizeTokensButtonConfig(
textRes = organizeButtonTextRes(),
iconRes = organizeButtonIconRes(),

View file

@ -84,15 +84,11 @@ internal class WalletTokensListUMConverter(
.asSequence()
.flatMap { accountStatus ->
if (isAccountsModeEnabled) {
val currencies = accountStatus.tokenList.flattenCurrencies()
val isCollapsable = currencies.isNotEmpty()
val isExpanded =
currencies.isEmpty() || expandedAccounts.contains(accountStatus.account.accountId)
sequenceOf(
TokensListItemUM2.Portfolio(
tokenRowUM = accountRowConverter.convert(accountStatus),
isExpanded = isExpanded,
isCollapsable = isCollapsable,
isExpanded = expandedAccounts.contains(accountStatus.account.accountId),
isCollapsable = true,
onEmptyClick = { clickIntents.onManageTokensClick(accountStatus.account.accountId) },
tokenList = getTokenListItems(
accountStatus,

View file

@ -3,12 +3,12 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.common.extensions.isZero
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetPrimaryCurrencyTransformer
import dagger.assisted.Assisted
@ -70,7 +70,7 @@ internal class PrimaryCurrencySubscriber @AssistedInject constructor(
cardBalanceState?.let { balanceState ->
// do not send tokens count for single currency wallet
analyticsEventHandler.send(
event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(
event = Basic.BalanceLoaded(
balance = balanceState,
tokensCount = null,
),

View file

@ -48,7 +48,7 @@ import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheet
import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheet
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.expressTransactionsItems
import com.tangem.common.ui.expressStatus.expressTransactionsItemsLegacy
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggableHeaderLegacy
@ -236,7 +236,7 @@ private fun WalletContent(
marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier)
}
if (walletState is WalletState.SingleCurrency.Content) {
expressTransactionsItems(
expressTransactionsItemsLegacy(
expressTxs = walletState.expressTxsToDisplay,
modifier = itemModifier,
)

View file

@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.ui
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
@ -42,6 +43,7 @@ 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.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.tangem.core.ui.components.BottomFade
@ -57,6 +59,7 @@ import com.tangem.core.ui.components.sheetscaffold.*
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar
import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior
import com.tangem.core.ui.extensions.softLayerShadow
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.*
import com.tangem.core.ui.utils.TangemSharedTransitionLayout
@ -209,16 +212,27 @@ private fun WalletContent2(
.fillMaxSize()
.hazeSourceTangem(zIndex = -2f),
) {
NorthernLightsBackground(
containerColor = if (LocalIsInDarkTheme.current) {
TangemTheme.colors2.surface.level1
} else {
TangemTheme.colors2.surface.level2
},
val backgroundColor = if (LocalIsInDarkTheme.current) {
TangemTheme.colors2.surface.level1
} else {
TangemTheme.colors2.surface.level2
}
Box(
modifier = Modifier
.graphicsLayer { alpha = 1 - behavior.state.collapsedFraction * 2 }
.matchParentSize(),
.matchParentSize()
.background(backgroundColor),
)
val isSheetExpanded by remember {
derivedStateOf { bottomSheetState.targetValue == TangemSheetValue.Expanded }
}
if (!isSheetExpanded) {
NorthernLightsBackground(
containerColor = backgroundColor,
modifier = Modifier
.graphicsLayer { alpha = 1 - behavior.state.collapsedFraction * 2 }
.matchParentSize(),
)
}
WalletPagerIndicator(
pagerState = walletsPagerState,
@ -351,13 +365,23 @@ private inline fun BaseScaffoldWithMarkets(
val peekHeight = bottomSheetHeaderHeightProvider() + TangemTheme.dimens2.x3 + bottomBarHeight
val coroutineScope = rememberCoroutineScope()
val background = TangemTheme.colors2.surface.level2
val bottomSheetState = rememberTangemStandardBottomSheetState()
val scaffoldState = rememberTangemBottomSheetScaffoldState(bottomSheetState = bottomSheetState)
val expandedBackground = TangemTheme.colors2.surface.level2
val collapsedBackground = TangemTheme.colors2.surface.level3
val background by animateColorAsState(
targetValue = if (bottomSheetState.targetValue == TangemSheetValue.Expanded) {
expandedBackground
} else {
collapsedBackground
},
label = "bottomSheetBackground",
)
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember(background) { mutableStateOf(background) },
LocalMainBottomSheetColor provides remember { mutableStateOf(background) }.apply { value = background },
) {
val backgroundColor by LocalMainBottomSheetColor.current
var isSearchFieldFocused by remember { mutableStateOf(false) }
@ -487,6 +511,13 @@ private fun BottomSheet(
Box(
modifier = Modifier
.fillMaxWidth()
.softLayerShadow(
radius = 16.dp,
color = Color.Black.copy(alpha = if (LocalIsInDarkTheme.current) .24f else .12f),
shape = shape,
offset = DpOffset(x = 0.dp, y = (-6).dp),
isAlphaContentClip = true,
)
.clip(shape)
.background(backgroundColor)
.onFocusChanged(onFocusChange),

View file

@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common
import android.content.res.Configuration
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
@ -80,7 +79,6 @@ internal fun WalletTopBar(
},
endContent = {
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x5),
modifier = Modifier
.clip(CircleShape)
.background(

View file

@ -34,12 +34,13 @@ import com.tangem.common.ui.tokens.NonContentItemContent
import com.tangem.common.ui.tokens.SlideInItemVisibility
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.account.toBoxSize
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.tokenlist.NON_CONTENT_TOKENS_LIST_KEY
import com.tangem.core.ui.components.tokenlist.TokenListItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.button.PrimaryInverseTangemButton
import com.tangem.core.ui.ds.button.SecondaryTangemButton
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.image.TangemIcon
@ -205,8 +206,6 @@ private fun LazyListScope.portfolioItem(
if (listItem.tokenList.isEmpty()) {
nonContentAccountItem(
listItem = listItem,
index = index,
lastIndex = lastIndex,
modifier = modifier,
)
} else {
@ -221,7 +220,7 @@ private fun LazyListScope.portfolioItem(
modifier = modifier
.animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null)
.roundedShapeItemDecoration(
radius = 18.dp,
radius = if (listItem.isExpanded) TangemTheme.dimens2.x6 else TangemTheme.dimens2.x5,
currentIndex = tokenIndex + 1,
addDefaultPadding = false,
lastIndex = lastIndex,
@ -293,7 +292,7 @@ private fun LazyListScope.accountItem(
.semantics { lazyListItemPosition = index }
.roundedShapeItemDecoration(
currentIndex = 0,
radius = 18.dp,
radius = if (listItem.isExpanded) TangemTheme.dimens2.x6 else TangemTheme.dimens2.x5,
addDefaultPadding = false,
lastIndex = effectiveLastIndex,
backgroundColor = TangemTheme.colors2.surface.level3,
@ -382,12 +381,14 @@ internal fun PortfolioRowItem(
headIcon
}
val iconBoxSize = when (headIcon) {
is TangemIconUM.Empty -> TangemTheme.dimens2.x9
else -> size.toBoxSize()
}
TangemIcon(
tangemIconUM = sizedHeadIcon,
modifier = modifier
.conditionalCompose(headIcon is TangemIconUM.Empty) {
size(TangemTheme.dimens2.x9)
}
.size(iconBoxSize)
.sharedBounds(
sharedContentState = iconSharedContentState,
animatedVisibilityScope = animatedContentScope,
@ -489,23 +490,18 @@ private fun LazyListScope.nonContentItem2(onEmptyClick: () -> Unit, modifier: Mo
}
}
private fun LazyListScope.nonContentAccountItem(
listItem: TokensListItemUM2.Portfolio,
index: Int,
lastIndex: Int,
modifier: Modifier = Modifier,
) {
private fun LazyListScope.nonContentAccountItem(listItem: TokensListItemUM2.Portfolio, modifier: Modifier = Modifier) {
item(
key = "$NON_CONTENT_TOKENS_LIST_KEY account-${listItem.tokenRowUM.id}",
contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${listItem.tokenRowUM.id}",
) {
SlideInItemVisibility(
currentIndex = index + 1,
lastIndex = lastIndex,
currentIndex = 1,
lastIndex = 1,
modifier = modifier
.animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null)
.roundedShapeItemDecoration(
radius = 18.dp,
radius = if (listItem.isExpanded) TangemTheme.dimens2.x6 else TangemTheme.dimens2.x5,
addDefaultPadding = false,
currentIndex = 1,
lastIndex = 1,
@ -542,8 +538,8 @@ internal fun NonContentItemContentV2(textColor: Color, modifier: Modifier = Modi
textAlign = TextAlign.Center,
style = TangemTheme.typography2.bodyRegular14,
)
SpacerH(TangemTheme.dimens2.x2)
PrimaryInverseTangemButton(
SpacerH(TangemTheme.dimens2.x4)
SecondaryTangemButton(
text = resourceReference(id = R.string.common_add_tokens),
onClick = onClick,
size = TangemButtonSize.X8,

View file

@ -4,12 +4,18 @@ import com.google.common.truth.Truth.assertThat
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.decompose.model.MutableParamsContainer
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
@ -38,6 +44,9 @@ internal class AddAndManageModelTest {
private val portfolioSelectorController: PortfolioSelectorController = mockk(relaxed = true) {
every { selectedAccount } returns flowOf(null)
}
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk(relaxed = true) {
coEvery { getSyncOrNull(any<UserWalletId>()) } returns null
}
private val onDismiss: () -> Unit = mockk(relaxed = true)
private val onOrganizeTokensClick: () -> Unit = mockk(relaxed = true)
@ -58,6 +67,7 @@ internal class AddAndManageModelTest {
portfolioFetcherFactory = portfolioFetcherFactory,
analyticsEventHandler = analyticsEventHandler,
portfolioSelectorController = portfolioSelectorController,
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
)
@Test
@ -98,4 +108,38 @@ internal class AddAndManageModelTest {
verify(exactly = 1) { onDismiss() }
verify(exactly = 1) { onOrganizeTokensClick() }
}
@Test
fun `GIVEN wallet has multi currency account WHEN model is created THEN shouldShowOrganize is true`() = runTest {
coEvery { singleAccountStatusListSupplier.getSyncOrNull(userWalletId) } returns
accountStatusListWithCurrencyCounts(2)
val model = createModel()
assertThat(model.state.value.shouldShowOrganize).isTrue()
}
@Test
fun `GIVEN wallet has no multi currency account WHEN model is created THEN shouldShowOrganize is false`() = runTest {
coEvery { singleAccountStatusListSupplier.getSyncOrNull(userWalletId) } returns
accountStatusListWithCurrencyCounts(1)
val model = createModel()
assertThat(model.state.value.shouldShowOrganize).isFalse()
}
private fun accountStatusListWithCurrencyCounts(vararg currencyCounts: Int): AccountStatusList {
val statuses: List<AccountStatus> = currencyCounts.map { count ->
val tokenList = mockk<TokenList> {
every { flattenCurrencies() } returns List(count) { mockk<CryptoCurrencyStatus>() }
}
mockk<AccountStatus.CryptoPortfolio> {
every { this@mockk.tokenList } returns tokenList
}
}
return mockk {
every { accountStatuses } returns statuses
}
}
}