Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-21 17:59:40 +05:00
commit a22e77ebe8
669 changed files with 27695 additions and 7905 deletions

View file

@ -8,4 +8,6 @@ package com.tangem.features.wallet.featuretoggles
interface WalletFeatureToggles {
val isAddAndManageTokensEnabled: Boolean
val isAddFundsStage1Enabled: Boolean
}

View file

@ -105,8 +105,8 @@ dependencies {
implementation(projects.domain.offramp)
implementation(projects.domain.onramp)
implementation(projects.domain.onramp.models)
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
implementation(projects.domain.stories)
implementation(projects.domain.stories.models)
implementation(projects.domain.quotes)
implementation(projects.domain.settings)
implementation(projects.domain.staking)
@ -119,6 +119,7 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.notifications)
implementation(projects.domain.pushNotificationPreferences)
implementation(projects.domain.transaction)
implementation(projects.domain.yieldSupply)
implementation(projects.domain.yieldSupply.models)
@ -136,6 +137,7 @@ dependencies {
implementation(projects.features.onboardingV2.api)
implementation(projects.features.onramp.api)
implementation(projects.features.pushNotifications.api)
implementation(projects.features.pushNotificationSettings.api)
implementation(projects.features.swap.api)
implementation(projects.features.tester.api)
implementation(projects.features.tokendetails.api)

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

@ -1,13 +1,22 @@
package com.tangem.feature.wallet.child.organizetokens.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.HorizontalDivider
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.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.ds.contextmenu.TangemContextMenu
import com.tangem.core.ui.ds.contextmenu.TangemContextMenuCheckboxItem
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensUM
import com.tangem.feature.wallet.impl.R
@ -25,19 +34,104 @@ internal fun OrganizeDropDownMenu(
offset = DpOffset.Zero,
modifier = modifier,
) {
TangemContextMenuCheckboxItem(
title = TextReference.Res(R.string.organize_tokens_sort_by_balance),
isChecked = organizeMenuUM.isSortedByBalance,
onClick = organizeMenuUM.onSortClick,
)
HorizontalDivider(
thickness = 0.5.dp,
color = TangemTheme.colors2.border.neutral.quaternary,
)
TangemContextMenuCheckboxItem(
title = TextReference.Res(R.string.organize_tokens_group),
isChecked = organizeMenuUM.isGrouped,
onClick = organizeMenuUM.onGroupClick,
Menu(
organizeMenuUM = organizeMenuUM,
onDropdownDismiss = onDropdownDismiss,
)
}
}
@Composable
private fun Menu(
onDropdownDismiss: () -> Unit,
organizeMenuUM: OrganizeTokensUM.OrganizeMenuUM,
modifier: Modifier = Modifier,
) {
Column(modifier) {
SortByBalanceMenuSection(
organizeMenuUM = organizeMenuUM,
onDropdownDismiss = onDropdownDismiss,
)
GroupTokensMenuSection(
organizeMenuUM = organizeMenuUM,
onDropdownDismiss = onDropdownDismiss,
)
}
}
@Composable
private fun SortByBalanceMenuSection(organizeMenuUM: OrganizeTokensUM.OrganizeMenuUM, onDropdownDismiss: () -> Unit) {
Text(
text = stringResourceSafe(R.string.organize_tokens_sort_by_balance),
style = TangemTheme.typography2.headingSemibold17,
color = if (organizeMenuUM.isSortedByBalance) {
TangemTheme.colors2.text.status.disabled
} else {
TangemTheme.colors2.text.neutral.primary
},
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.widthIn(238.dp)
.clickableSingle(
onClick = {
organizeMenuUM.onSortClick()
onDropdownDismiss()
},
enabled = !organizeMenuUM.isSortedByBalance,
)
.padding(vertical = TangemTheme.dimens2.x5, horizontal = TangemTheme.dimens2.x4),
)
HorizontalDivider(
thickness = 0.5.dp,
color = TangemTheme.colors2.border.neutral.quaternary,
)
}
@Composable
private fun GroupTokensMenuSection(organizeMenuUM: OrganizeTokensUM.OrganizeMenuUM, onDropdownDismiss: () -> Unit) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.widthIn(238.dp)
.clickableSingle(
onClick = {
organizeMenuUM.onGroupClick()
onDropdownDismiss()
},
)
.padding(vertical = TangemTheme.dimens2.x5, horizontal = TangemTheme.dimens2.x4),
) {
Text(
text = stringResourceSafe(R.string.organize_tokens_group),
style = TangemTheme.typography2.headingSemibold17,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
if (organizeMenuUM.isGrouped) {
Box(
modifier = Modifier
.padding(TangemTheme.dimens2.x0_5)
.size(TangemTheme.dimens2.x5)
.background(
color = TangemTheme.colors2.graphic.neutral.primary,
shape = CircleShape,
),
) {
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_check_default_24),
),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primaryInverted,
modifier = Modifier
.align(Alignment.Center)
.padding(TangemTheme.dimens2.x0_5)
.size(TangemTheme.dimens2.x4),
)
}
}
}
}

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
@ -26,6 +27,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.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
@ -33,8 +35,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 +52,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
@ -93,7 +102,9 @@ internal fun OrganizeTokensContent(
organizeMenuUM = organizeTokensUM.organizeMenuUM,
showDropdownMenu = isShowDropdownMenu,
onDropdownDismiss = { isShowDropdownMenu = false },
modifier = Modifier.hazeEffectTangem(hazeState),
modifier = Modifier.hazeEffectTangem(hazeState) {
blurRadius = 6.dp
},
)
},
)
@ -219,7 +230,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 +302,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

@ -37,7 +37,6 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejec
import com.tangem.feature.walletsettings.component.RenameWalletComponent
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.feed.entry.components.FeedEntryComponent
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams
@ -63,7 +62,6 @@ internal class WalletComponent @AssistedInject constructor(
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory,
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles,
private val networkSelectionComponentFactory: NetworkSelectionComponent.Factory,
private val tokenActionsComponentFactory: TokenActionsComponent.Factory,
private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
@ -85,8 +83,7 @@ internal class WalletComponent @AssistedInject constructor(
)
}
private val promoBannersBlockComponent: PromoBannersBlockComponent? by lazy {
if (!newPromoBannersFeatureToggles.isNewPromoBannersEnabled) return@lazy null
private val promoBannersBlockComponent: PromoBannersBlockComponent by lazy {
promoBannersBlockComponentFactory.create(
context = child("promoBannersBlockComponent"),
params = PromoBannersBlockComponent.Params(

View file

@ -47,6 +47,7 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWal
import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory
import com.tangem.feature.wallet.presentation.wallet.domain.WalletContentFetcher
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.domain.pushnotificationpreferences.PreloadWalletPushNotificationPreferencesUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
@ -61,8 +62,10 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejec
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.pushnotificationsettings.PushNotificationSettingsFeatureToggles
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import com.tangem.utils.logging.TangemLogger
@ -93,6 +96,7 @@ internal class WalletModel @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender,
private val walletNameMigrationUseCase: WalletNameMigrationUseCase,
private val preloadWalletPushNotificationPreferencesUseCase: PreloadWalletPushNotificationPreferencesUseCase,
private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase,
private val walletImageResolver: WalletImageResolver,
private val onrampStatusFactory: OnrampStatusFactory,
@ -120,6 +124,8 @@ internal class WalletModel @Inject constructor(
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
private val uiMessageSender: UiMessageSender,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val walletFeatureToggles: WalletFeatureToggles,
private val pushNotificationSettingsFeatureToggles: PushNotificationSettingsFeatureToggles,
private val startAssetsDiscoveryUseCase: StartAssetsDiscoveryUseCase,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
@ -144,6 +150,7 @@ internal class WalletModel @Inject constructor(
maybeMigrateNames()
maybeSetWalletFirstTimeUsage()
preloadPushNotificationPreferences()
updateYieldSupplyApy()
subscribeToUserWalletsUpdates()
subscribeOnBalanceHiding()
@ -190,6 +197,19 @@ internal class WalletModel @Inject constructor(
}
}
private fun preloadPushNotificationPreferences() {
if (!pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled) return
getWalletsUseCase()
.map { wallets -> wallets.map(UserWallet::walletId) }
.distinctUntilChanged()
.onEach { walletIds ->
walletIds.forEach { walletId ->
modelScope.launch { preloadWalletPushNotificationPreferencesUseCase(walletId) }
}
}
.launchIn(modelScope)
}
private fun maybeSetWalletFirstTimeUsage() {
modelScope.launch {
setWalletFirstTimeUsageUseCase()
@ -243,10 +263,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 +270,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,
@ -548,6 +564,7 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled,
),
)
@ -594,6 +611,7 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled,
),
)
}
@ -615,6 +633,7 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled,
),
)
}
@ -629,6 +648,7 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled,
),
)
@ -690,6 +710,7 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled,
),
)

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

@ -1,8 +1,11 @@
package com.tangem.feature.wallet.child.wallet.model.intents
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.onramp.FetchHotCryptoUseCase
import com.tangem.domain.settings.NeverToShowWalletsScrollPreview
@ -44,6 +47,7 @@ internal class WalletClickIntents @Inject constructor(
private val tangemPayIntents: TangemPayClickIntentsImplementor,
private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase,
private val designFeatureToggles: DesignFeatureToggles,
private val analyticsEventHandler: AnalyticsEventHandler,
) : BaseWalletClickIntents(),
WalletCardClickIntents by walletCardClickIntentsImplementor,
WalletWarningsClickIntents by warningsClickIntentsImplementer,
@ -114,6 +118,11 @@ internal class WalletClickIntents @Inject constructor(
refreshSingleCurrencyContent(showRefreshState = true)
}
fun onAddFundsClick(userWalletId: UserWalletId) {
analyticsEventHandler.send(MainScreenAnalyticsEvent.ButtonAddFunds())
router.openAddFunds(userWalletId)
}
private fun refreshMultiCurrencyContent(showRefreshState: Boolean) {
val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return

View file

@ -41,8 +41,8 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.offramp.GetOfframpUrlUseCase
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.domain.stories.GetStoryContentUseCase
import com.tangem.domain.stories.models.StoryContentIds
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.tokens.NeedShowYieldSupplyDepositedWarningUseCase
import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase
@ -65,6 +65,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertUM
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -453,16 +455,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
override fun onMultiWalletSwapClick(userWalletId: UserWalletId) {
val selectedWallet = stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content ?: return
when (selectedWallet.tokensListState) {
is WalletTokensListState.ContentState.Content,
is WalletTokensListState.ContentState.PortfolioContent,
-> Unit
WalletTokensListState.ContentState.Loading,
WalletTokensListState.ContentState.Locked,
WalletTokensListState.Empty,
-> return
}
if (!isMultiWalletTokensLoaded()) return
modelScope.launch {
val swapRoute = getSwapRoute(
@ -576,6 +569,24 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
return true
}
private fun isMultiWalletTokensLoaded(): Boolean {
return if (stateHolder.value.isRedesignEnabled) {
val selectedWalletUM = stateHolder.getSelectedWalletUM() as? WalletUM.Content ?: return false
selectedWalletUM.tokensListUM is WalletTokensListUM.Content
} else {
val selectedWallet = stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content ?: return false
when (selectedWallet.tokensListState) {
is WalletTokensListState.ContentState.Content,
is WalletTokensListState.ContentState.PortfolioContent,
-> true
WalletTokensListState.ContentState.Loading,
WalletTokensListState.ContentState.Locked,
WalletTokensListState.Empty,
-> false
}
}
}
private fun onMultiWalletActionClick(
statusFlow: Flow<Lce<Throwable, Any>>,
route: AppRoute,

View file

@ -12,7 +12,6 @@ import com.tangem.core.analytics.models.event.AssetsDiscoveryAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.review.ReviewManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.assetsdiscovery.usecase.AcknowledgeAssetsDiscoveryCompletionUseCase
import com.tangem.domain.card.SetCardWasScannedUseCase
import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -29,17 +28,11 @@ import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.notifications.SetShouldShowNotificationUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.settings.NeverToSuggestRateAppUseCase
import com.tangem.domain.settings.RemindToRateAppLaterUseCase
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
@ -75,10 +68,6 @@ internal interface WalletWarningsClickIntents {
fun onCloseRateAppWarningClick()
fun onClosePromoClick(promoId: PromoId)
fun onPromoClick(promoId: PromoId, cryptoCurrency: CryptoCurrency? = null)
fun onSupportClick()
fun onBackupErrorClick()
@ -91,8 +80,6 @@ internal interface WalletWarningsClickIntents {
fun onFinishWalletActivationClick(isBackupExists: Boolean)
fun onYieldPromoTermsAndConditionsClick()
fun onUpgradeHotWalletClick(userWalletId: UserWalletId)
fun onCloseUpgradeBannerClick(userWalletId: UserWalletId)
@ -115,10 +102,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val dispatchers: CoroutineDispatcherProvider,
private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val urlOpener: UrlOpener,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
@ -243,91 +228,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
}
override fun onClosePromoClick(promoId: PromoId) {
analyticsEventHandler.send(
when (promoId) {
PromoId.Referral -> MainScreen.ReferralPromoButtonDismiss()
PromoId.Sepa -> PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.Sepa,
action = PromotionBannerClicked.BannerAction.Closed(),
)
PromoId.VisaPresale -> PromoAnalyticsEvent.VisaWaitlistPromoDismiss()
PromoId.BlackFriday -> PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.BlackFriday,
action = PromotionBannerClicked.BannerAction.Closed(),
)
PromoId.OnePlusOne -> PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.OnePlusOne,
action = PromotionBannerClicked.BannerAction.Closed(),
)
PromoId.YieldPromo -> PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.YieldPromo,
action = PromotionBannerClicked.BannerAction.Closed(),
)
},
)
modelScope.launch(dispatchers.main) {
shouldShowPromoWalletUseCase.neverToShow(promoId)
}
}
override fun onPromoClick(promoId: PromoId, cryptoCurrency: CryptoCurrency?) {
val userWallet = getSelectedUserWallet() ?: return
when (promoId) {
PromoId.Referral -> {
analyticsEventHandler.send(MainScreen.ReferralPromoButtonParticipate())
appRouter.push(ReferralProgram(userWalletId = userWallet.walletId))
}
PromoId.Sepa -> {
analyticsEventHandler.send(
PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.Sepa,
action = PromotionBannerClicked.BannerAction.Clicked(),
),
)
cryptoCurrency ?: return
appRouter.push(
Onramp(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
source = OnrampSource.SEPA_BANNER,
shouldLaunchSepa = true,
),
)
}
PromoId.VisaPresale -> {
analyticsEventHandler.send(PromoAnalyticsEvent.VisaWaitlistPromoJoin())
urlOpener.openUrl(VISA_PROMO_LINK)
}
PromoId.BlackFriday -> {
analyticsEventHandler.send(
PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.BlackFriday,
action = PromotionBannerClicked.BannerAction.Clicked(),
),
)
urlOpener.openUrl(BLACK_FRIDAY_PROMO_LINK)
}
PromoId.OnePlusOne -> {
analyticsEventHandler.send(
PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.OnePlusOne,
action = PromotionBannerClicked.BannerAction.Clicked(),
),
)
urlOpener.openUrl(ONE_PLUS_ONE_PROMO_LINK)
}
PromoId.YieldPromo -> Unit // banner is not clickable, only terms and conditions button
}
}
override fun onSupportClick() {
val userWallet = getSelectedUserWallet() ?: return
@ -478,17 +378,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
}
override fun onYieldPromoTermsAndConditionsClick() {
analyticsEventHandler.send(
PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.YieldPromo,
action = PromotionBannerClicked.BannerAction.Clicked(),
),
)
urlOpener.openUrl(YIELD_PROMO_TERMS_LINK)
}
override fun onUpgradeHotWalletClick(userWalletId: UserWalletId) {
modelScope.launch(dispatchers.main) {
val userWallet = getUserWalletUseCase(userWalletId).getOrNull()
@ -519,21 +408,4 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
AccountId.forMainCryptoPortfolio(userWalletId),
)
}
private companion object {
const val VISA_PROMO_LINK = "https://tangem.com/en/cardwaitlist/?utm_source=tangem-app-banner" +
"&utm_medium=banner" +
"&utm_campaign=tangempaywaitlist"
const val BLACK_FRIDAY_PROMO_LINK = "https://tangem.com/en/pricing/" +
"?promocode=BF2025" +
"&utm_source=tangem-app-banner" +
"&utm_medium=banner" +
"&utm_campaign=BlackFriday2025"
const val ONE_PLUS_ONE_PROMO_LINK = "https://tangem.com/pricing/" +
"?cat=family" +
"&utm_source=tangem-app-banner" +
"&utm_medium=banner" +
"&utm_campaign=BOGO50"
const val YIELD_PROMO_TERMS_LINK = "https://tangem.com/docs/yield-mode-toc.html"
}
}

View file

@ -11,4 +11,7 @@ internal class DefaultWalletFeatureToggles @Inject constructor(
override val isAddAndManageTokensEnabled: Boolean
get() = featureToggles.isFeatureEnabled(FeatureToggles.ADD_AND_MANAGE_TOKENS_ENABLED)
override val isAddFundsStage1Enabled: Boolean
get() = featureToggles.isFeatureEnabled(FeatureToggles.AND_15310_ADD_FUNDS_STAGE1)
}

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
@ -119,6 +119,10 @@ internal class DefaultWalletRouter @Inject constructor(
router.push(AppRoute.Home())
}
override fun openAddFunds(userWalletId: UserWalletId) {
router.push(AppRoute.AddFunds(userWalletId = userWalletId))
}
override fun isWalletLastScreen(): Boolean {
return router.stack.lastOrNull() is AppRoute.Wallet
}
@ -142,8 +146,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(
@ -117,4 +117,7 @@ internal interface InnerWalletRouter {
/** Open network selection bottom sheet for multiple QR matches */
fun openNetworkSelectionBottomSheet(target: QrSendTarget.Multiple)
/** Open Add Funds screen */
fun openAddFunds(userWalletId: UserWalletId)
}

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?,
@ -167,12 +125,6 @@ sealed class WalletScreenAnalyticsEvent {
if (blockchain != null) put("Blockchain", blockchain)
},
)
// region Referral Promo
class ReferralPromo : MainScreen(event = "Referral Banner")
class ReferralPromoButtonParticipate : MainScreen(event = "Button - Referral Participate")
class ReferralPromoButtonDismiss : MainScreen(event = "Button - Referral Dismiss")
//endregion
}
sealed class PushBannerPromo(

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

@ -4,7 +4,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.*
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen.*
@ -69,28 +68,6 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.RateApp -> HowDoYouLikeTangem()
is WalletNotification.Critical.BackupError -> BackupError()
is WalletNotification.NoteMigration -> NotePromo()
is WalletNotification.SwapPromo -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.Empty, // Use it on new promo action
)
is WalletNotification.Sepa -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.Sepa,
)
is WalletNotification.BlackFridayPromo -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.BlackFriday,
)
is WalletNotification.OnePlusOnePromo -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.OnePlusOne,
)
is WalletNotification.YieldPromo -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.YieldPromo,
)
is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo()
is WalletNotification.VisaPresalePromo -> VisaWaitlistPromo()
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
is WalletNotification.Informational.NoAccount,
is WalletNotification.Warning.LowSignatures,
@ -138,14 +115,6 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotificationUM.RateApp -> HowDoYouLikeTangem()
is WalletNotificationUM.BackupError -> BackupError()
is WalletNotificationUM.NoteMigration -> NotePromo()
is WalletNotificationUM.OnePlusOnePromo -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.OnePlusOne,
)
is WalletNotificationUM.YieldPromo -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.YieldPromo,
)
is WalletNotificationUM.FinishWalletActivation -> {
val activationState = if (notificationUM.isBackupExists) {
NoticeFinishActivation.ActivationState.Unfinished

View file

@ -24,8 +24,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase
@ -54,7 +52,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
private val backupValidator: BackupValidator,
private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase,
private val notificationsRepository: NotificationsRepository,
private val accountDependencies: AccountDependencies,
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
@ -82,13 +79,9 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
accountStatusListFlow,
isReadyToShowRateAppUseCase().distinctUntilChanged(),
isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.OnePlusOne)
.distinctUntilChanged(),
notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key)
.distinctUntilChanged(),
getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo)
.distinctUntilChanged(),
shouldShowUpgradeHotWalletBannerUseCase.invoke(userWallet.walletId)
.distinctUntilChanged(),
getUpgradeBannerClosureTimestampUseCase(userWallet.walletId)
@ -99,13 +92,11 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val accountStatusList = array[0] as AccountStatusList
val isReadyToShowRating = array[1] as Boolean
val isNeedToBackup = array[2] as Boolean
val shouldShowOnePlusOnePromo = array[3] as Boolean
val shouldShowEnablePushesReminderNotification = array[4] as Boolean
val shouldAccessCodeSkipped = array[5] as Boolean
val shouldShowYieldPromo = array[6] as Boolean
val shouldShowUpgradeBanner = array[7] as Boolean
val closureTimestamp = array[8] as? Long
val assetsDiscoveryProgress = array[9] as AssetsDiscoveryProgress
val shouldShowEnablePushesReminderNotification = array[3] as Boolean
val shouldAccessCodeSkipped = array[4] as Boolean
val shouldShowUpgradeBanner = array[5] as Boolean
val closureTimestamp = array[6] as? Long
val assetsDiscoveryProgress = array[7] as AssetsDiscoveryProgress
val flattenCurrencies = accountStatusList.flattenCurrencies()
val paymentAccountStatus = accountStatusList.accountStatuses
@ -132,10 +123,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
shouldAccessCodeSkipped = shouldAccessCodeSkipped,
)
addOnePlusOnePromoNotification(clickIntents, shouldShowOnePlusOnePromo)
addYieldPromoNotification(clickIntents, shouldShowYieldPromo)
addInformationalNotifications(
userWallet = userWallet,
cardTypesResolver = cardTypesResolver,
@ -162,9 +149,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
!notificationsRepository.isUserAllowToSubscribeOnPushNotifications(),
)
// Remove in first iteration of yield supply feature
// addYieldSupplyNotifications(flattenCurrencies)
val hasCriticalOrWarning = any { notification ->
notification is WalletNotification.Critical || notification is WalletNotification.Warning
}
@ -296,41 +280,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
.map(CryptoCurrencyStatus::currency)
}
private fun MutableList<WalletNotification>.addOnePlusOnePromoNotification(
clickIntents: WalletClickIntents,
shouldShowPromo: Boolean,
) {
addIf(
element = WalletNotification.OnePlusOnePromo(
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.OnePlusOne) },
onClick = { clickIntents.onPromoClick(promoId = PromoId.OnePlusOne) },
),
condition = shouldShowPromo,
)
}
private fun MutableList<WalletNotification>.addYieldPromoNotification(
clickIntents: WalletClickIntents,
shouldShowPromo: Boolean,
) {
addIf(
element = WalletNotification.YieldPromo(
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.YieldPromo) },
onTermsAndConditionsClick = { clickIntents.onYieldPromoTermsAndConditionsClick() },
),
condition = shouldShowPromo,
)
}
// private fun MutableList<WalletNotification>.addYieldSupplyNotifications(
// flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
// ) {
// addIf(
// element = WalletNotification.Warning.YeildSupplyApprove,
// condition = flattenCurrencies.hasTokensWithActivatedSupplyWithoutApprove(),
// )
// }
private fun MutableList<WalletNotification>.addWarningNotifications(
cardTypesResolver: CardTypesResolver?,
flattenCurrencies: List<CryptoCurrencyStatus>,
@ -393,15 +342,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
return this.any { it.value is CryptoCurrencyStatus.Unreachable }
}
// Remove in first iteration of yield supply feature
// private fun Lce<TokenListError, List<CryptoCurrencyStatus>>.hasTokensWithActivatedSupplyWithoutApprove(): Boolean {
// val flattenCurrencies = getOrNull(isPartialContentAccepted = false) ?: return false
// val yieldSupplyEnabled = yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled
// return yieldSupplyEnabled && flattenCurrencies.any {
// it.value.yieldSupplyStatus?.isAllowedToSpend == false
// }
// }
private fun MutableList<WalletNotification>.addAssetsDiscoveryCompletedNotification(
userWallet: UserWallet,
assetsDiscoveryProgress: AssetsDiscoveryProgress,

View file

@ -5,10 +5,7 @@ import com.tangem.common.ui.notifications.NotificationId
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
@ -29,32 +26,22 @@ import javax.inject.Inject
@ModelScoped
internal class GetWalletNotificationsCarouselFactory @Inject constructor(
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val notificationsRepository: NotificationsRepository,
) {
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotificationUM>> {
return combine(
flow = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo)
.distinctUntilChanged(),
flow2 = notificationsRepository.getShouldShowNotification(
flow = notificationsRepository.getShouldShowNotification(
NotificationId.EnablePushesReminderNotification.key,
).distinctUntilChanged(),
flow3 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.OnePlusOne)
.distinctUntilChanged(),
flow4 = isReadyToShowRateAppUseCase().distinctUntilChanged(),
flow5 = getWalletsUseCase().conflate(),
) { showYieldPromo, showPushesNotification, showOnePlusOnePromo, showRateAppPromo, wallets ->
flow2 = isReadyToShowRateAppUseCase().distinctUntilChanged(),
flow3 = getWalletsUseCase().conflate(),
) { showPushesNotification, showRateAppPromo, wallets ->
buildList {
addNoteMigrationNotification(userWallet, wallets, clickIntents)
addRateAppNotification(showRateAppPromo, clickIntents)
if (userWallet.isMultiCurrency) {
addOnePlusOnePromoNotification(clickIntents, showOnePlusOnePromo)
addYieldPromoNotification(clickIntents, showYieldPromo)
}
addPushNotification(
shouldShow = showPushesNotification,
isPushesAllowed = notificationsRepository.isUserAllowToSubscribeOnPushNotifications(),
@ -77,30 +64,6 @@ internal class GetWalletNotificationsCarouselFactory @Inject constructor(
}
}
private fun MutableList<WalletNotificationUM>.addYieldPromoNotification(
clickIntents: WalletClickIntents,
shouldShowPromo: Boolean,
) {
addIf(shouldShowPromo) {
WalletNotificationUM.YieldPromo(
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.YieldPromo) },
onTermsAndConditionsClick = { clickIntents.onYieldPromoTermsAndConditionsClick() },
)
}
}
private fun MutableList<WalletNotificationUM>.addOnePlusOnePromoNotification(
clickIntents: WalletClickIntents,
shouldShowPromo: Boolean,
) {
addIf(shouldShowPromo) {
WalletNotificationUM.OnePlusOnePromo(
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.OnePlusOne) },
onClick = { clickIntents.onPromoClick(promoId = PromoId.OnePlusOne) },
)
}
}
private fun MutableList<WalletNotificationUM>.addNoteMigrationNotification(
userWallet: UserWallet,
userWallets: List<UserWallet>,

View file

@ -51,6 +51,20 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
),
)
data class AddFunds(
override val enabled: Boolean,
override val dimContent: Boolean,
override val onClick: () -> Unit,
) : WalletManageButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_add_funds),
iconResId = R.drawable.ic_plus_24,
onClick = onClick,
isEnabled = enabled,
shouldDimContent = dimContent,
),
)
/**
* Send
*

View file

@ -12,7 +12,6 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.feature.wallet.impl.R
import org.joda.time.DateTime
/**
* Wallet notification component state
@ -229,19 +228,6 @@ sealed class WalletNotification(val config: NotificationConfig) {
),
)
data class SwapPromo(
val startDateTime: DateTime,
val endDateTime: DateTime,
val onCloseClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(id = R.string.swap_promo_title),
subtitle = resourceReference(id = R.string.swap_promo_text),
iconResId = R.drawable.img_okx_dex_logo,
onCloseClick = onCloseClick,
),
)
data class NoteMigration(val onClick: () -> Unit) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.wallet_promo_banner_title),
@ -282,108 +268,6 @@ sealed class WalletNotification(val config: NotificationConfig) {
),
)
data class ReferralPromo(
val onCloseClick: () -> Unit,
val onClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.notification_referral_promo_title),
subtitle = resourceReference(R.string.notification_referral_promo_text),
iconResId = R.drawable.img_referral_promo,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.notification_referral_promo_button),
onClick = onClick,
),
iconSize = 54.dp,
),
)
data class VisaPresalePromo(
val onCloseClick: () -> Unit,
val onClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.notification_visa_waitlist_promo_title),
subtitle = resourceReference(R.string.notification_visa_waitlist_promo_text),
iconResId = R.drawable.img_visa_waitlist_promo,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.notification_referral_promo_button),
onClick = onClick,
),
iconSize = 54.dp,
),
)
data class Sepa(
val onCloseClick: () -> Unit,
val onClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.notification_sepa_title),
subtitle = resourceReference(R.string.notification_sepa_text),
iconResId = R.drawable.img_notification_sepa,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.notification_sepa_button),
onClick = onClick,
),
iconSize = 54.dp,
),
)
data class BlackFridayPromo(
val onCloseClick: () -> Unit,
val onClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.notification_black_friday_title),
subtitle = resourceReference(R.string.notification_black_friday_text),
iconResId = R.drawable.img_black_friday_promo,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_claim),
onClick = onClick,
),
iconSize = 54.dp,
),
)
data class OnePlusOnePromo(
val onCloseClick: () -> Unit,
val onClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.notification_one_plus_one_title),
subtitle = resourceReference(R.string.notification_one_plus_one_text),
iconResId = R.drawable.img_one_plus_one_promo,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.notification_one_plus_one_button),
onClick = onClick,
),
iconSize = 54.dp,
),
)
data class YieldPromo(
val onCloseClick: () -> Unit,
val onTermsAndConditionsClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.notification_yield_promo_title),
subtitle = resourceReference(R.string.notification_yield_promo_text),
iconResId = R.drawable.ic_yield_promo_36,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.notification_yield_promo_button),
onClick = onTermsAndConditionsClick,
),
iconSize = 36.dp,
),
)
data class PushNotifications(
val onCloseClick: () -> Unit,
val onEnabledClick: () -> Unit,

View file

@ -132,7 +132,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(id = R.string.common_contact_support),
type = TangemButtonType.PrimaryInverse,
type = TangemButtonType.Secondary,
onClick = onClick,
),
),
@ -153,7 +153,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(id = R.string.button_start_backup_process),
type = TangemButtonType.PrimaryInverse,
type = TangemButtonType.Secondary,
onClick = onClick,
),
),
@ -204,7 +204,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(R.string.hw_activation_need_finish),
type = TangemButtonType.PrimaryInverse,
type = TangemButtonType.Secondary,
onClick = onClick,
),
),
@ -360,53 +360,6 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
type = WalletNotificationType.Promo,
)
data class OnePlusOnePromo(
val onCloseClick: () -> Unit,
val onClick: () -> Unit,
) : WalletNotificationUM(
messageUM = TangemMessageUM(
id = "OnePlusOnePromoNotification",
title = resourceReference(R.string.notification_one_plus_one_title),
subtitle = resourceReference(R.string.notification_one_plus_one_text),
messageEffect = TangemMessageEffect.Magic,
iconUM = TangemIconUM.Image(R.drawable.img_one_plus_one_promo),
iconSize = 54.dp,
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(R.string.common_later),
type = TangemButtonType.PrimaryInverse,
onClick = onCloseClick,
),
TangemMessageButtonUM(
text = resourceReference(R.string.notification_one_plus_one_button),
type = TangemButtonType.Primary,
onClick = onClick,
),
),
),
type = WalletNotificationType.Promo,
)
data class YieldPromo(
val onCloseClick: () -> Unit,
val onTermsAndConditionsClick: () -> Unit,
) : WalletNotificationUM(
messageUM = TangemMessageUM(
id = "YieldPromoNotification",
title = resourceReference(R.string.notification_yield_promo_title),
subtitle = resourceReference(R.string.notification_yield_promo_text),
onCloseClick = onCloseClick,
messageEffect = TangemMessageEffect.Magic,
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(R.string.notification_yield_promo_button),
type = TangemButtonType.Primary,
onClick = onTermsAndConditionsClick,
),
),
),
type = WalletNotificationType.Promo,
)
// endregion
// region Survey
@ -423,7 +376,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(id = R.string.warning_button_could_be_better),
type = TangemButtonType.PrimaryInverse,
type = TangemButtonType.Secondary,
onClick = onDislikeClick,
),
TangemMessageButtonUM(
@ -454,7 +407,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(R.string.common_later),
type = TangemButtonType.PrimaryInverse,
type = TangemButtonType.Secondary,
onClick = onCloseClick,
),
TangemMessageButtonUM(
@ -483,7 +436,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
TangemMessageButtonUM(
text = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_button),
onClick = onStartMigrationClick,
type = TangemButtonType.PrimaryInverse,
type = TangemButtonType.Secondary,
),
),
),

View file

@ -13,6 +13,7 @@ internal class AddWalletTransformer(
private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val isAddFundsStage1Enabled: Boolean,
) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy {
@ -20,6 +21,7 @@ internal class AddWalletTransformer(
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = isAddFundsStage1Enabled,
)
}

View file

@ -27,6 +27,7 @@ internal class InitializeWalletsTransformer(
private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val isAddFundsStage1Enabled: Boolean,
) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy {
@ -34,6 +35,7 @@ internal class InitializeWalletsTransformer(
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = isAddFundsStage1Enabled,
)
}
@ -147,8 +149,18 @@ internal class InitializeWalletsTransformer(
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
if (isSingleWalletWithToken) return persistentListOf()
val firstButton = if (isAddFundsStage1Enabled) {
WalletManageButton.AddFunds(
enabled = false,
dimContent = false,
onClick = {},
)
} else {
WalletManageButton.Buy(enabled = false, dimContent = false, onClick = {})
}
return persistentListOf(
WalletManageButton.Buy(enabled = false, dimContent = false, onClick = {}),
firstButton,
WalletManageButton.Swap(enabled = false, dimContent = false, onClick = {}),
WalletManageButton.Sell(enabled = false, dimContent = false, onClick = {}),
)

View file

@ -24,6 +24,7 @@ internal class ReinitializeNewWalletTransformer(
private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val isAddFundsStage1Enabled: Boolean,
) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy {
@ -31,6 +32,7 @@ internal class ReinitializeNewWalletTransformer(
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = isAddFundsStage1Enabled,
)
}

View file

@ -20,6 +20,7 @@ internal class ReinitializeWalletTransformer(
private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val isAddFundsStage1Enabled: Boolean,
) : WalletStateTransformer(userWalletId = userWallet.walletId) {
private val walletLoadingStateFactory by lazy {
@ -27,6 +28,7 @@ internal class ReinitializeWalletTransformer(
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = isAddFundsStage1Enabled,
)
}

View file

@ -72,10 +72,11 @@ internal class SetRefreshStateTransformer(
private fun PersistentList<WalletManageButton>.toUpdatedState(): PersistentList<WalletManageButton> {
val isButtonsEnabled = !isRefreshing
return mutate {
it.mapNotNull { button ->
return mutate { items ->
items.mapNotNull { button ->
when (button) {
is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.AddFunds -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.Receive -> button

View file

@ -18,6 +18,7 @@ internal class UnlockWalletTransformer(
private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val isAddFundsStage1Enabled: Boolean,
) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy {
@ -25,6 +26,7 @@ internal class UnlockWalletTransformer(
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = isAddFundsStage1Enabled,
)
}

View file

@ -13,16 +13,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,
@ -64,25 +60,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,
isReissuing = false,
),
)
},
onClick = { tangemPayClickIntents.openDetails(value) },
)
is PaymentAccountStatusValue.Loaded -> {
val card = statusValue.cards.firstOrNull() ?: return TangemPayMainUM.TemporaryUnavailable
@ -97,28 +77,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,
isReissuing = card.isReissuing,
),
)
},
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

@ -1,6 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.state.utils
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
@ -16,11 +18,24 @@ internal fun WalletState.MultiCurrency.Content.disableButtons(): PersistentList<
}
internal fun WalletUM.Content.enableButtons(): PersistentList<TangemButtonUM> {
return buttons.map { it.copy(isEnabled = true) }.toPersistentList()
return buttons.map { it.withEnabled(isEnabled = true) }.toPersistentList()
}
internal fun WalletUM.Content.disableButtons(): PersistentList<TangemButtonUM> {
return buttons.map { it.copy(isEnabled = false) }.toPersistentList()
return buttons.map { it.withEnabled(isEnabled = false) }.toPersistentList()
}
private fun TangemButtonUM.withEnabled(isEnabled: Boolean): TangemButtonUM {
val refreshedIcon = (tangemIconUM as? TangemIconUM.Icon)?.copy(
tint = {
if (isEnabled) {
TangemTheme.colors2.graphic.neutral.primary
} else {
TangemTheme.colors2.graphic.neutral.quaternary
}
},
) ?: tangemIconUM
return copy(isEnabled = isEnabled, tangemIconUM = refreshedIcon)
}
private fun WalletState.MultiCurrency.Content.changeAvailability(enabled: Boolean): PersistentList<WalletManageButton> {
@ -28,6 +43,7 @@ private fun WalletState.MultiCurrency.Content.changeAvailability(enabled: Boolea
.map { action ->
when (action) {
is WalletManageButton.Buy -> action.copy(enabled = enabled)
is WalletManageButton.AddFunds -> action.copy(enabled = enabled)
is WalletManageButton.Sell -> action.copy(enabled = enabled)
is WalletManageButton.Swap -> action.copy(enabled = enabled)
else -> action

View file

@ -34,6 +34,7 @@ internal class WalletLoadingStateFactory(
private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val isAddFundsStage1Enabled: Boolean,
) {
fun create(userWallet: UserWallet): WalletState {
@ -149,7 +150,13 @@ internal class WalletLoadingStateFactory(
userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
if (isSingleWalletWithToken) return persistentListOf()
return persistentListOf(
val firstButton = if (isAddFundsStage1Enabled) {
WalletManageButton.AddFunds(
enabled = true,
dimContent = false,
onClick = { clickIntents.onAddFundsClick(userWallet.walletId) },
)
} else {
WalletManageButton.Buy(
enabled = true,
dimContent = false,
@ -159,7 +166,11 @@ internal class WalletLoadingStateFactory(
WALLET_TYPE,
)
},
),
)
}
return persistentListOf(
firstButton,
WalletManageButton.Swap(
enabled = true,
dimContent = false,

View file

@ -1,8 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.domain.stories.GetStoryContentUseCase
import com.tangem.domain.stories.models.StoryContentIds
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.UpdateMultiWalletActionButtonBadgeTransformer
import dagger.assisted.Assisted

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
@ -184,6 +187,7 @@ private fun WalletContent2(
WalletTopBar(
topBarConfig = state.topBarConfig,
walletBalance = walletBalance,
isBalanceHidden = state.isHidingMode,
behavior = behavior,
)
},
@ -209,16 +213,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 +366,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 +512,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

@ -27,11 +27,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
key = { it::class.java },
contentType = { it::class.java },
itemContent = { item ->
// TODO develop promo banner general component
when (item) {
is WalletNotification.SwapPromo -> {
// Use it on new promo action
}
is WalletNotification.NoteMigration -> {
NoteMigrationNotification(
config = item.config,

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
@ -58,13 +59,13 @@ internal fun WalletPagerIndicator(
.fillMaxWidth()
.height(height)
.alpha(alpha),
contentAlignment = Alignment.TopCenter,
) {
TangemPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.padding(top = padding)
.scale(scaleY = 1f, scaleX = scale)
.fillMaxWidth(),
.scale(scaleY = 1f, scaleX = scale),
)
}
}

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
@ -27,6 +26,7 @@ import com.tangem.core.ui.ds.topbar.TangemTopBarActionUM
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior
import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.LocalRootBackgroundColor
import com.tangem.core.ui.res.TangemTheme
@ -47,12 +47,14 @@ private const val VISIBILITY_THRESHOLD = 0.5f
*
* @param topBarConfig top bar config
* @param walletBalance wallet balance text reference
* @param isBalanceHidden whether the balance must be masked with stars
* @param behavior collapsing behavior
*/
@Composable
internal fun WalletTopBar(
topBarConfig: WalletTopBarConfig,
walletBalance: TextReference?,
isBalanceHidden: Boolean,
behavior: TangemCollapsingAppBarBehavior,
) {
Surface(
@ -64,8 +66,8 @@ internal fun WalletTopBar(
derivedStateOf { behavior.state.collapsedFraction > VISIBILITY_THRESHOLD }
}
val wrappedBalance = remember(walletBalance, isWrappedBalanceShown) {
walletBalance.takeIf { isWrappedBalanceShown }
val wrappedBalance = remember(walletBalance, isWrappedBalanceShown, isBalanceHidden) {
walletBalance?.orMaskWithStars(isBalanceHidden).takeIf { isWrappedBalanceShown }
}
TangemTopBar(
@ -80,7 +82,6 @@ internal fun WalletTopBar(
},
endContent = {
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x5),
modifier = Modifier
.clip(CircleShape)
.background(
@ -175,6 +176,7 @@ private fun WalletTopBar_Preview() {
WalletTopBar(
topBarConfig = WalletTopBarConfig(),
walletBalance = stringReference("$ 8923,05"),
isBalanceHidden = false,
behavior = rememberTangemExitUntilCollapsedScrollBehavior(),
)
}
@ -199,6 +201,7 @@ private fun WalletTopBar_WithQrButton_Preview() {
),
),
walletBalance = stringReference("$ 8923,05"),
isBalanceHidden = false,
behavior = rememberTangemExitUntilCollapsedScrollBehavior(),
)
}

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
@ -55,6 +56,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.core.ui.utils.ProvideSharedTransitionScope
import com.tangem.core.ui.utils.lazyListItemPosition
import com.tangem.core.ui.utils.sharedBoundsSafely
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.TokensListItemUM2
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
@ -205,8 +207,6 @@ private fun LazyListScope.portfolioItem(
if (listItem.tokenList.isEmpty()) {
nonContentAccountItem(
listItem = listItem,
index = index,
lastIndex = lastIndex,
modifier = modifier,
)
} else {
@ -221,7 +221,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 +293,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,13 +382,15 @@ 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)
}
.sharedBounds(
.size(iconBoxSize)
.sharedBoundsSafely(
sharedContentState = iconSharedContentState,
animatedVisibilityScope = animatedContentScope,
boundsTransform = boundsTransform,
@ -422,7 +424,7 @@ internal fun PortfolioRowItem(
TokenRowTitle(
titleUM = resizedTitle,
modifier = modifier.sharedBounds(
modifier = modifier.sharedBoundsSafely(
sharedContentState = titleSharedContentState,
animatedVisibilityScope = animatedContentScope,
boundsTransform = boundsTransform,
@ -489,23 +491,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 +539,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

@ -1,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
android:strokeAlpha="0.1"
android:fillColor="#0099FF"
android:fillAlpha="0.1"/>
<path
android:pathData="M18.057,7.745C20.247,7.745 21.968,7.745 23.312,7.926C24.688,8.111 25.781,8.498 26.639,9.356C27.497,10.214 27.885,11.307 28.07,12.683C28.25,14.027 28.25,15.748 28.25,17.938V18.052C28.25,20.243 28.25,21.964 28.07,23.307C27.885,24.684 27.497,25.776 26.639,26.635C25.781,27.493 24.688,27.879 23.312,28.064C21.968,28.245 20.247,28.245 18.057,28.245H17.943C15.752,28.245 14.031,28.245 12.688,28.064C11.311,27.879 10.219,27.493 9.361,26.635C8.502,25.776 8.116,24.684 7.931,23.307C7.75,21.964 7.75,20.243 7.75,18.052V17.938C7.75,15.748 7.75,14.027 7.931,12.683C8.116,11.307 8.503,10.214 9.361,9.356C10.219,8.498 11.311,8.111 12.688,7.926C14.031,7.745 15.752,7.745 17.943,7.745H18.057ZM13,21.248C12.586,21.248 12.25,21.584 12.25,21.998V23.998C12.25,24.412 12.586,24.748 13,24.748C13.414,24.748 13.75,24.412 13.75,23.998V21.998C13.75,21.584 13.414,21.248 13,21.248ZM18,20.247C17.586,20.247 17.25,20.583 17.25,20.997V23.997C17.25,24.411 17.586,24.747 18,24.747C18.414,24.747 18.75,24.411 18.75,23.997V20.997C18.75,20.583 18.414,20.247 18,20.247ZM23,18.245C22.586,18.245 22.25,18.581 22.25,18.995V23.995C22.25,24.409 22.586,24.745 23,24.745C23.414,24.745 23.75,24.409 23.75,23.995V18.995C23.75,18.581 23.414,18.245 23,18.245ZM21.859,11.247C21.654,11.256 21.416,11.295 21.179,11.335C21.168,11.337 21.157,11.339 21.146,11.341L19.878,11.55C19.47,11.617 19.193,12.002 19.26,12.411C19.327,12.82 19.714,13.097 20.122,13.029L20.846,12.91C18.963,15.552 15.953,16.745 12,16.745C11.586,16.745 11.25,17.081 11.25,17.495C11.25,17.909 11.586,18.245 12,18.245C16.322,18.245 19.898,16.9 22.132,13.689L22.279,14.201C22.393,14.599 22.808,14.829 23.206,14.716C23.604,14.601 23.835,14.186 23.721,13.788L23.403,12.677C23.399,12.666 23.396,12.655 23.393,12.643C23.335,12.44 23.271,12.218 23.189,12.036C23.088,11.814 22.93,11.576 22.645,11.414C22.369,11.257 22.09,11.236 21.859,11.247Z"
android:fillColor="#0099FF"
android:fillType="evenOdd"/>
</vector>

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
}
}
}