Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-15 22:43:52 +03:00
commit d6f9f59866
1729 changed files with 67614 additions and 9361 deletions

View file

@ -145,7 +145,7 @@ dependencies {
implementation(projects.features.walletSettings.api)
implementation(projects.features.biometry.api)
implementation(projects.features.nft.api)
implementation(projects.features.sendV2.api)
implementation(projects.features.send.api)
implementation(projects.features.kyc.api)
implementation(projects.features.tokenRecieve.api)
implementation(projects.features.yieldSupply.api)
@ -153,6 +153,7 @@ dependencies {
implementation(projects.features.promoBanners.api)
implementation(projects.features.tangempay.main.api)
implementation(projects.features.tangempay.details.api)
implementation(projects.features.virtualAccounts.main.api)
/** Common modules */
implementation(projects.common)
@ -160,7 +161,7 @@ dependencies {
implementation(projects.common.ui)
/** Test libraries */
testImplementation(deps.test.junit)
testImplementation(deps.test.junit5)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
testImplementation(deps.test.mockk)

View file

@ -4,11 +4,8 @@
<CurrentIssues>
<ID>BooleanPropertyNaming:DefaultUserWalletsFetcher.kt$DefaultUserWalletsFetcher$@Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean</ID>
<ID>BooleanPropertyNaming:DraggableItem.kt$DraggableItem$abstract val showShadow: Boolean</ID>
<ID>BooleanPropertyNaming:DraggableItem.kt$DraggableItem.RoundingMode$abstract val showGap: Boolean</ID>
<ID>BooleanPropertyNaming:OrganizeTokensState.kt$OrganizeTokensState.ActionsConfig$val showApplyProgress: Boolean = false</ID>
<ID>BooleanPropertyNaming:ScrollToWalletTransformer.kt$ScrollToWalletTransformer$private val withScrollAnimation: Boolean = true</ID>
<ID>BooleanPropertyNaming:TangemPayState.kt$TangemPayState.Progress$val showProgress: Boolean = false</ID>
<ID>BooleanPropertyNaming:TokenActionButtonConfig.kt$TokenActionButtonConfig$val enabled: Boolean = true</ID>
<ID>BooleanPropertyNaming:UpdateMultiWalletActionButtonBadgeTransformer.kt$UpdateMultiWalletActionButtonBadgeTransformer$private val showSwapBadge: Boolean</ID>
<ID>BooleanPropertyNaming:WalletAdditionalInfo.kt$WalletAdditionalInfo$val hideable: Boolean</ID>
<ID>BooleanPropertyNaming:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor$val showSwapStories = maybeSwapStories.getOrNull() != null</ID>
@ -20,24 +17,15 @@
<ID>BooleanPropertyNaming:WalletScreenState.kt$WalletScreenState$val showMarketsOnboarding: Boolean</ID>
<ID>BooleanPropertyNaming:WalletWithFundsChecker.kt$WalletWithFundsChecker$val prevStatus = statusByWalletId.get(userWalletId)</ID>
<ID>MaxChainedCallsOnSameLine:HasSingleWalletSignedHashesUseCase.kt$HasSingleWalletSignedHashesUseCase$userWallet.scanResponse.card.wallets.firstOrNull()?.totalSignedHashes</ID>
<ID>MultilineLambdaItParameter:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler${ Timber.tag(LOG_TAG).e("Error on getting user wallet: $it") showAlert(Failed) }</ID>
<ID>MultilineLambdaItParameter:DefaultUserWalletImageFetcher.kt$DefaultUserWalletImageFetcher${ it.fold( ifLeft = { emit(UserWalletItemUM.ImageState.Loading) }, ifRight = { wallet -&gt; emitAll(walletImage(wallet, size)) }, ) }</ID>
<ID>MultilineLambdaItParameter:OrganizeTokensModel.kt$OrganizeTokensModel${ isBalanceHidden = it.isBalanceHidden stateHolder.updateHiddenState(isBalanceHidden) }</ID>
<ID>MultilineLambdaItParameter:SetRefreshStateTransformer.kt$SetRefreshStateTransformer${ it.mapNotNull { button -&gt; when (button) { is WalletManageButton.Buy -&gt; button.copy(enabled = isButtonsEnabled) is WalletManageButton.Send -&gt; button.copy(enabled = isButtonsEnabled) is WalletManageButton.Sell -&gt; button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -&gt; button is WalletManageButton.Stake -&gt; null is WalletManageButton.Swap -&gt; null } } }</ID>
<ID>MultilineLambdaItParameter:TokenListAnalyticsSender.kt$TokenListAnalyticsSender${ val status = it.value if (status is CryptoCurrencyStatus.Loaded) { sendTokenBalancesForSpecificBlockchains(it, status) } }</ID>
<ID>MultilineLambdaItParameter:TokenListStateConverter.kt$TokenListStateConverter${ if (isExtend) { clickIntents.onAccountCollapseClick(it) } else { clickIntents.onAccountExpandClick(it) } }</ID>
<ID>MultilineLambdaItParameter:UseCaseExt.kt${ Timber.e("Impossible to get primary currency status $it") null }</ID>
<ID>MultilineLambdaItParameter:UseCaseExt.kt${ Timber.e("Impossible to get selected wallet $it") null }</ID>
<ID>MultilineLambdaItParameter:WalletCard.kt${ haptic.performHapticFeedback(HapticFeedbackType.LongPress) isMenuVisible = true pressOffset = DpOffset(x = it.x.toDp(), y = it.y.toDp()) }</ID>
<ID>MultilineLambdaItParameter:WalletCard.kt${ val press = PressInteraction.Press(it) interactionSource.emit(press) tryAwaitRelease() interactionSource.emit(PressInteraction.Release(press)) }</ID>
<ID>MultilineLambdaItParameter:WalletContentClickIntents.kt$WalletContentClickIntentsImplementor${ Timber.e( """ Unable to get user wallet |- ID: $userWalletId |- Exception: $it """.trimIndent(), ) return@launch }</ID>
<ID>MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ onAddressTypeSelected( userWalletId = userWalletId, currency = currency, addressModel = it, ) }</ID>
<ID>MultilineLambdaItParameter:WalletNameMigrationUseCase.kt$WalletNameMigrationUseCase${ val defaultName = it.name val suggestedWalletName = suggestedWalletName(defaultName, existingNames) if (defaultName != suggestedWalletName) { userWalletsListRepository.saveWithoutLock(it.copy(name = suggestedWalletName), canOverride = true) } Timber.tag("Migrated names").e(it.walletId.toString() + " " + suggestedWalletName) }</ID>
<ID>MultilineLambdaItParameter:WalletScreen.kt${ PaddingValues( bottom = it.calculateBottomPadding() + marketHintAproxHeight + 52.dp, ) }</ID>
<ID>MultilineLambdaItParameter:WalletScreen.kt${ WalletSnackbarHost( snackbarHostState = it, event = state.event, modifier = Modifier .padding(bottom = TangemTheme.dimens.spacing4) .navigationBarsPadding(), ) }</ID>
<ID>MultilineLambdaItParameter:WalletScreen.kt${ it.organizeTokensButtonConfig?.let { config -&gt; organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } }</ID>
<ID>MultilineLambdaItParameter:WalletScreen.kt${ nftCollections( modifier = itemModifier, state = it.nftState, ) }</ID>
<ID>MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ Timber.e( """ Unable to get user wallet |- ID: $userWalletId |- Exception: $it """.trimIndent(), ) null }</ID>
<ID>MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ router.openOnboardingScreen( scanResponse = it.scanResponse, continueBackup = true, ) }</ID>
<ID>MultilineLambdaItParameter:WalletWithFundsChecker.kt$WalletWithFundsChecker${ val amount = it.value.amount ?: return@any false !amount.isZero() }</ID>
<ID>NamedArguments:TangemSnapFlingBehavior.kt$HighVelocityApproachAnimation$animateDecay(offset, animationState, decayAnimationSpec, onAnimationStep)</ID>
@ -46,19 +34,12 @@
<ID>NamedArguments:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$approach( initialTargetOffset, initialVelocity, animation, snapLayoutInfoProvider, density, onAnimationStep, )</ID>
<ID>NamedArguments:TangemSnapFlingBehavior.kt$approachAnimation( this, initialTargetOffset, initialVelocity, onAnimationStep, )</ID>
<ID>NamedArguments:WalletContent.kt$txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier)</ID>
<ID>NestedScopeFunctions:WalletScreen.kt$let { config -&gt; organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) }</ID>
<ID>NestedScopeFunctions:WalletScreen.kt$let { it.organizeTokensButtonConfig?.let { config -&gt; organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } }</ID>
<ID>NestedScopeFunctions:WalletScreen.kt$let { marketPriceBlockState -&gt; marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier) }</ID>
<ID>NoNameShadowing:DefaultUserWalletsFetcher.kt$DefaultUserWalletsFetcher${ it.isMultiCurrency }</ID>
<ID>NoNameShadowing:MultiCurrencyAccountContent.kt$modifier</ID>
<ID>NoNameShadowing:WalletComponent.kt$WalletComponent$dialog</ID>
<ID>NoNameShadowing:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ it is TokensListItemUM.Token }</ID>
<ID>NoNameShadowing:WalletNFTItem.kt$modifier</ID>
<ID>NoNameShadowing:WalletScreen.kt${ it.organizeTokensButtonConfig?.let { config -&gt; organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } }</ID>
<ID>PropertyUsedBeforeDeclaration:BaseWalletClickIntents.kt$BaseWalletClickIntents$_modelScope</ID>
<ID>PropertyUsedBeforeDeclaration:BaseWalletClickIntents.kt$BaseWalletClickIntents$_router</ID>
<ID>PropertyUsedBeforeDeclaration:OrganizeTokensModel.kt$OrganizeTokensModel$uiState</ID>
<ID>PropertyUsedBeforeDeclaration:WalletScreenPreviewData.kt$WalletScreenPreviewData$buyButton</ID>
<ID>PropertyUsedBeforeDeclaration:WalletStateController.kt$WalletStateController$mutableUiState</ID>
<ID>ReusedModifierInstance:DefaultWalletEntryComponent.kt$DefaultWalletEntryComponent$Content(modifier)</ID>
<ID>ReusedModifierInstance:WalletNFTItem.kt$Image( modifier = modifier .background(TangemTheme.colors.stroke.primary), painter = painterResource(R.drawable.ic_nft_preview_more_16), contentDescription = null, )</ID>
@ -67,14 +48,10 @@
<ID>SuspendFunSwallowedCancellation:WalletModel.kt$WalletModel$runCatching</ID>
<ID>UnnecessaryLet:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$let { abs(it) * sign(initialVelocity) // ensure offset sign is correct }</ID>
<ID>UnnecessaryLet:WalletClickIntents.kt$WalletClickIntents$let(::add)</ID>
<ID>UnnecessaryLet:WalletScreen.kt$let { (state.tokensListState as? WalletTokensListState.ContentState)?.let { it.organizeTokensButtonConfig?.let { config -&gt; organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } }</ID>
<ID>UnnecessaryLet:WalletScreen.kt$let { it.organizeTokensButtonConfig?.let { config -&gt; organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } }</ID>
<ID>UseEmptyCounterpart:DefaultUserWalletImageFetcher.kt$DefaultUserWalletImageFetcher$mapOf&lt;String, ArtworkUM&gt;()</ID>
<ID>UseEmptyCounterpart:ExpandedAccountsHolder.kt$ExpandedAccountsHolder$setOf()</ID>
<ID>UseEmptyCounterpart:PortfolioOrganizeTokensAnalyticsEvent.kt$PortfolioOrganizeTokensAnalyticsEvent$mapOf()</ID>
<ID>UseEmptyCounterpart:PromoActivationAnalytics.kt$PromoActivationAnalytics$mapOf()</ID>
<ID>UseEmptyCounterpart:TokenListStateConverter.kt$TokenListStateConverter$listOf()</ID>
<ID>UseEmptyCounterpart:WalletScreenAnalyticsEvent.kt$WalletScreenAnalyticsEvent.Basic$mapOf()</ID>
<ID>UseEmptyCounterpart:WalletScreenAnalyticsEvent.kt$WalletScreenAnalyticsEvent.MainScreen$mapOf()</ID>
<ID>UseEmptyCounterpart:WalletScreenAnalyticsEvent.kt$WalletScreenAnalyticsEvent.PushBannerPromo$mapOf()</ID>
<ID>UseSumOfInsteadOfFlatMapSize:TokenListStateConverter.kt$TokenListStateConverter$flatMap(NetworkGroup::currencies)</ID>

View file

@ -96,6 +96,7 @@ internal fun OrganizeTokensContent(
onClick = { isShowDropdownMenu = true },
ghostModeProgress = 0f,
),
modifier = Modifier.testTag(OrganizeTokensScreenTestTags.MENU_BUTTON),
type = TangemTopBarType.BottomSheet,
)
OrganizeDropDownMenu(
@ -161,8 +162,7 @@ private fun TokenList(
modifier = Modifier
.align(Alignment.TopCenter)
.reorderable(reorderableListState)
.testTag(OrganizeTokensScreenTestTags.TOKENS_LAZY_LIST)
.hazeSourceTangem(zIndex = 1f),
.testTag(OrganizeTokensScreenTestTags.TOKENS_LAZY_LIST),
state = reorderableListState.listState,
contentPadding = listContentPadding,
) {
@ -221,7 +221,7 @@ private fun LazyItemScope.DraggableItem(
when (item) {
is OrganizeRowItemUM.Network -> TangemHeaderRow(
modifier = modifierWithBackground,
modifier = modifierWithBackground.testTag(OrganizeTokensScreenTestTags.GROUP_TITLE_ITEM),
reorderableState = reorderableState,
headerRowUM = item.headerRowUM,
)
@ -231,7 +231,7 @@ private fun LazyItemScope.DraggableItem(
isBalanceHidden = isBalanceHidden,
)
is OrganizeRowItemUM.Token -> OrganizeTokenRow(
modifier = modifierWithBackground,
modifier = modifierWithBackground.testTag(OrganizeTokensScreenTestTags.TOKEN_LIST_ITEM),
tokenRowUM = item.tokenRowUM,
reorderableState = reorderableState,
isBalanceHidden = isBalanceHidden,
@ -262,11 +262,15 @@ private fun BoxScope.BottomButtons(organizeTokensUM: OrganizeTokensUM) {
) {
TangemButton(
buttonUM = organizeTokensUM.cancelButton,
modifier = Modifier.weight(1f),
modifier = Modifier
.weight(1f)
.testTag(OrganizeTokensScreenTestTags.CANCEL_BUTTON),
)
TangemButton(
buttonUM = organizeTokensUM.applyButton,
modifier = Modifier.weight(1f),
modifier = Modifier
.weight(1f)
.testTag(OrganizeTokensScreenTestTags.APPLY_BUTTON),
)
}
}

View file

@ -14,6 +14,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import com.tangem.core.ui.test.BaseBottomSheetTestTags
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
@ -101,6 +103,7 @@ private fun TokenActionContextMenuContent(actions: ImmutableList<TokenActionButt
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
modifier = Modifier
.testTag(BaseBottomSheetTestTags.ACTION_BUTTON)
.clickable(
enabled = item.isEnabled,
onClick = {
@ -123,7 +126,9 @@ private fun TokenActionContextMenuContent(actions: ImmutableList<TokenActionButt
} else {
TangemTheme.colors2.graphic.neutral.primary
},
modifier = Modifier.size(TangemTheme.dimens2.x5),
modifier = Modifier
.size(TangemTheme.dimens2.x5)
.testTag(BaseBottomSheetTestTags.ACTION_ICON),
)
Text(
text = item.text.resolveReference(),

View file

@ -25,7 +25,7 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent
import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.feature.wallet.child.tokenActions.DefaultTokenActionsComponent
import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent
import com.tangem.feature.wallet.child.wallet.model.WalletModel
@ -36,14 +36,16 @@ import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen2
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedComponent
import com.tangem.feature.walletsettings.component.RenameWalletComponent
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
import com.tangem.features.feed.entry.components.FeedEntryComponent
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.send.v2.api.NetworkSelectionComponent
import com.tangem.features.send.api.NetworkSelectionComponent
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -57,6 +59,7 @@ internal class WalletComponent @AssistedInject constructor(
@Assisted navigate: (WalletRoute) -> Unit,
feedEntryComponentFactory: FeedEntryComponent.Factory,
tangemPayMainBlockComponentFactory: TangemPayMainBlockComponent.Factory,
virtualAccountMainBlockComponentFactory: VirtualAccountMainBlockComponent.Factory,
private val tangemPayTransactionBottomSheetComponentFactory: TangemPayTransactionBottomSheetComponent.Factory,
private val renameWalletComponentFactory: RenameWalletComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
@ -67,6 +70,7 @@ internal class WalletComponent @AssistedInject constructor(
private val networkSelectionComponentFactory: NetworkSelectionComponent.Factory,
private val tokenActionsComponentFactory: TokenActionsComponent.Factory,
private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
private val addFundsComponentFactory: AddFundsComponent.Factory,
private val designFeatureToggles: DesignFeatureToggles,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
@ -84,6 +88,12 @@ internal class WalletComponent @AssistedInject constructor(
params = Unit,
)
}
private val virtualAccountMainBlockComponent by lazy {
virtualAccountMainBlockComponentFactory.create(
context = child("virtualAccountMainBlockComponent"),
params = Unit,
)
}
private val promoBannersBlockComponent: PromoBannersBlockComponent by lazy {
promoBannersBlockComponentFactory.create(
@ -180,6 +190,15 @@ internal class WalletComponent @AssistedInject constructor(
),
)
}
is WalletDialogConfig.AddFunds -> {
addFundsComponentFactory.create(
context = childByContext(componentContext),
params = AddFundsComponent.Params(
launchMode = AddFundsComponent.LaunchMode.ChooseToken(dialogConfig.userWalletId),
onDismiss = model.innerWalletRouter.dialogNavigation::dismiss,
),
)
}
is WalletDialogConfig.AddAndManage -> {
AddAndManageBottomSheetComponent(
appComponentContext = childByContext(componentContext),
@ -270,7 +289,9 @@ internal class WalletComponent @AssistedInject constructor(
if (designFeatureToggles.isRedesignEnabled) {
WalletScreen2(
state = uiState,
promoBannersBlockComponent = promoBannersBlockComponent,
tangemPayComponent = tangemPayMainBlockComponent,
virtualAccountComponent = virtualAccountMainBlockComponent,
bottomSheetContent = { onExpandSheet ->
BottomSheetContent(
bottomSheetState = bottomSheetState,
@ -287,6 +308,7 @@ internal class WalletComponent @AssistedInject constructor(
state = uiState,
promoBannersBlockComponent = promoBannersBlockComponent,
tangemPayComponent = tangemPayMainBlockComponent,
virtualAccountComponent = virtualAccountMainBlockComponent,
bottomSheetContent = { onExpandSheet ->
BottomSheetContent(
bottomSheetState = bottomSheetState,

View file

@ -41,9 +41,9 @@ 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.staking.model.StakingOption
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
import com.tangem.domain.tokens.SaveViewedYieldSupplyWarningUseCase
@ -61,12 +61,7 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
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.model.*
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
@ -316,9 +311,10 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
if (handleUnavailabilityReason(unavailabilityReason)) return
showErrorIfDemoModeOrElse {
showErrorIfDemoModeOrElse { userWallet ->
modelScope.launch(dispatchers.main) {
getOfframpUrlUseCase(
userWalletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code,
).onRight { url ->
@ -480,8 +476,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
)
}
private fun openExplorer() {
val userWalletId = stateHolder.getSelectedWalletId()
private fun openExplorer(userWallet: UserWallet) {
val userWalletId = userWallet.walletId
modelScope.launch(dispatchers.main) {
val currencyStatus = singleAccountStatusListSupplier.unwrap(userWalletId) ?: return@launch
@ -545,7 +541,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
}
private fun showErrorIfDemoModeOrElse(action: () -> Unit) {
private fun showErrorIfDemoModeOrElse(action: (UserWallet) -> Unit) {
val selectedWallet = getSelectedWalletSyncUseCase.unwrap() ?: return
if (selectedWallet is UserWallet.Cold && isDemoCardUseCase(cardId = selectedWallet.cardId)) {
@ -557,7 +553,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
),
)
} else {
action()
action(selectedWallet)
}
}

View file

@ -19,6 +19,7 @@ import com.tangem.feature.wallet.presentation.preview.WalletBalancePreview
import com.tangem.feature.wallet.presentation.preview.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import kotlinx.collections.immutable.persistentListOf
internal object WalletScreenPreviewData {
@ -179,6 +180,7 @@ internal object WalletScreenPreviewData {
onItemClick = {},
),
tangemPayMainUM = TangemPayMainUM.Loading,
virtualAccountMainUM = VirtualAccountMainUM.Loading,
)
private val walletEmpty = WalletUM.Content(
@ -194,6 +196,7 @@ internal object WalletScreenPreviewData {
tokensListUM = WalletTokensListUM.Empty(onEmptyClick = {}),
nftState = WalletNFTItemUM.Hidden,
tangemPayMainUM = TangemPayMainUM.Empty,
virtualAccountMainUM = VirtualAccountMainUM.Empty,
)
private val walletAccountDefault = walletDefault.copy(

View file

@ -120,7 +120,9 @@ internal class DefaultWalletRouter @Inject constructor(
}
override fun openAddFunds(userWalletId: UserWalletId) {
router.push(AppRoute.AddFunds(userWalletId = userWalletId))
dialogNavigation.activate(
configuration = WalletDialogConfig.AddFunds(userWalletId = userWalletId),
)
}
override fun isWalletLastScreen(): Boolean {

View file

@ -6,7 +6,6 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
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.*
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner
import com.tangem.feature.wallet.presentation.wallet.state.model.*
@ -81,9 +80,9 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
-> null
is WalletNotification.FinishWalletActivation -> {
val activationState = if (warning.isBackupExists) {
MainScreen.NoticeFinishActivation.ActivationState.Unfinished
NoticeFinishActivation.ActivationState.Unfinished
} else {
MainScreen.NoticeFinishActivation.ActivationState.NotStarted
NoticeFinishActivation.ActivationState.NotStarted
}
val balanceState = when (warning.type) {
WalletActivationBannerType.Attention -> AnalyticsParam.EmptyFull.Empty
@ -135,6 +134,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
}
is WalletNotificationUM.PushNotifications -> PushBanner()
is WalletNotificationUM.AddFunds -> NoticeAddFunds()
is WalletNotificationUM.TangemPayPromo -> TangemPayAnalyticsEvents.PermanentBannerShowed()
is WalletNotificationUM.UnlockWallets,
is WalletNotificationUM.NoAccount,
is WalletNotificationUM.LowSignatures,
@ -143,6 +143,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotificationUM.CloreMigration,
is WalletNotificationUM.TangemPayRefreshNeeded,
WalletNotificationUM.TangemPayUnreachable,
is WalletNotificationUM.YieldBoostPromo,
-> null
}
}

View file

@ -8,8 +8,11 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.yield.supply.promo.usecase.ShouldShowYieldBoostMainBannerUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.utils.extensions.addIf
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -28,6 +31,9 @@ internal class GetWalletNotificationsCarouselFactory @Inject constructor(
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val notificationsRepository: NotificationsRepository,
private val shouldShowYieldBoostMainBannerUseCase: ShouldShowYieldBoostMainBannerUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
) {
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotificationUM>> {
return combine(
@ -36,7 +42,8 @@ internal class GetWalletNotificationsCarouselFactory @Inject constructor(
).distinctUntilChanged(),
flow2 = isReadyToShowRateAppUseCase().distinctUntilChanged(),
flow3 = getWalletsUseCase().conflate(),
) { showPushesNotification, showRateAppPromo, wallets ->
flow4 = yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged(),
) { showPushesNotification, showRateAppPromo, wallets, shouldShowYieldPromoLocal ->
buildList {
addNoteMigrationNotification(userWallet, wallets, clickIntents)
@ -47,10 +54,33 @@ internal class GetWalletNotificationsCarouselFactory @Inject constructor(
isPushesAllowed = notificationsRepository.isUserAllowToSubscribeOnPushNotifications(),
clickIntents = clickIntents,
)
addYieldBoostBannerNotification(
userWallet = userWallet,
shouldShowLocal = shouldShowYieldPromoLocal,
clickIntents = clickIntents,
)
}.sortedBy { it.type.ordinal }.toImmutableList()
}
}
private suspend fun MutableList<WalletNotificationUM>.addYieldBoostBannerNotification(
userWallet: UserWallet,
shouldShowLocal: Boolean,
clickIntents: WalletClickIntents,
) {
if (!shouldShowLocal) return
if (!yieldSupplyFeatureToggles.isYieldPromoEnabled) return
val shouldShow = shouldShowYieldBoostMainBannerUseCase(userWallet.walletId).getOrNull() == true
if (!shouldShow) return
add(
WalletNotificationUM.YieldBoostPromo(
onExploreClick = { clickIntents.onYieldBoostBannerClick(userWallet.walletId) },
onLaterClick = { clickIntents.onDismissYieldBoostBanner(userWallet.walletId) },
),
)
}
private fun MutableList<WalletNotificationUM>.addRateAppNotification(
isReadyToShowRating: Boolean,
clickIntents: WalletClickIntents,

View file

@ -255,7 +255,10 @@ internal class GetWalletNotificationsFactory @Inject constructor(
onRefreshClick = { walletClickIntents.onRefreshPayToken(userWallet) },
shouldShowProgress = false,
)
is PaymentAccountStatusValue.NotCreated -> null // TODO(Main redesign) and analytics PermanentBannerShowed
is PaymentAccountStatusValue.NotCreated -> WalletNotificationUM.TangemPayPromo(
onLaterClick = { walletClickIntents.onOnboardingBannerCloseClick(userWallet.walletId) },
onLearnMoreClick = { walletClickIntents.onOnboardingBannerClick(userWallet.walletId) },
)
is PaymentAccountStatusValue.Error.Unavailable -> WalletNotificationUM.TangemPayUnreachable
is PaymentAccountStatusValue.Error.CardIssueFailed,
is PaymentAccountStatusValue.Error.ExposedDevice,

View file

@ -62,6 +62,9 @@ internal sealed interface WalletDialogConfig {
val customerId: String,
) : WalletDialogConfig
@Serializable
data class AddFunds(val userWalletId: UserWalletId) : WalletDialogConfig
@Serializable
data class OrganizeTokens(val userWalletId: UserWalletId) : WalletDialogConfig

View file

@ -7,7 +7,12 @@ import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.message.TangemMessageButtonUM
import com.tangem.core.ui.ds.message.TangemMessageEffect
import com.tangem.core.ui.ds.message.TangemMessageUM
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import kotlinx.collections.immutable.persistentListOf
@ -52,8 +57,8 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
data object UsedOutdatedData : WalletNotificationUM(
messageUM = TangemMessageUM(
id = "UsedOutdatedDataNotification",
title = resourceReference(com.tangem.core.res.R.string.warning_outdated_data_title),
subtitle = resourceReference(com.tangem.core.res.R.string.warning_outdated_data_message),
title = resourceReference(R.string.warning_outdated_data_title),
subtitle = resourceReference(R.string.warning_outdated_data_message),
iconUM = TangemIconUM.Icon(
iconRes = R.drawable.ic_error_sync_default_24,
tintReference = { TangemTheme.colors2.graphic.status.attention },
@ -383,6 +388,62 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
type = WalletNotificationType.Promo,
)
data class TangemPayPromo(
private val onLaterClick: () -> Unit,
private val onLearnMoreClick: () -> Unit,
) : WalletNotificationUM(
messageUM = TangemMessageUM(
id = "TangemPayPromo",
iconUM = TangemIconUM.Image(imageRes = R.drawable.ic_visa_in_banner),
title = resourceReference(id = R.string.tangempay_onboarding_banner_title),
subtitle = resourceReference(id = R.string.tangempay_get_banner_description),
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(id = R.string.common_later),
onClick = onLaterClick,
type = TangemButtonType.Secondary,
),
TangemMessageButtonUM(
text = resourceReference(id = R.string.common_learn_more),
onClick = onLearnMoreClick,
type = TangemButtonType.Primary,
),
),
messageEffect = TangemMessageEffect.None,
),
type = WalletNotificationType.Promo,
)
data class YieldBoostPromo(
val onExploreClick: () -> Unit,
val onLaterClick: () -> Unit,
) : WalletNotificationUM(
messageUM = TangemMessageUM(
id = "YieldBoostPromoNotification",
title = combinedReference(
resourceReference(CoreResR.string.yield_apy_boost_banner_title),
stringReference(" · "),
resourceReference(CoreResR.string.yield_apy_boost_banner_title_apy_multiplied),
),
subtitle = resourceReference(CoreResR.string.yield_apy_boost_banner_subtitle),
iconUM = TangemIconUM.Image(imageRes = CoreUiR.drawable.ic_yield_32),
messageEffect = TangemMessageEffect.Magic,
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(CoreResR.string.common_later),
type = TangemButtonType.Secondary,
onClick = onLaterClick,
),
TangemMessageButtonUM(
text = resourceReference(CoreResR.string.yield_apy_boost_banner_button_title),
type = TangemButtonType.Primary,
onClick = onExploreClick,
),
),
),
type = WalletNotificationType.Promo,
)
// endregion
// region Survey
@ -448,8 +509,8 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
) : WalletNotificationUM(
messageUM = TangemMessageUM(
id = "CloreMigrationNotification",
title = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_title),
subtitle = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_description),
title = resourceReference(R.string.warning_clore_migration_title),
subtitle = resourceReference(R.string.warning_clore_migration_description),
iconUM = TangemIconUM.Icon(
iconRes = R.drawable.ic_attention_default_24,
tintReference = { TangemTheme.colors2.graphic.status.attention },
@ -457,7 +518,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
messageEffect = TangemMessageEffect.None,
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_button),
text = resourceReference(R.string.warning_clore_migration_button),
onClick = onStartMigrationClick,
type = TangemButtonType.Secondary,
),

View file

@ -11,6 +11,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedWa
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.WalletStateHolder
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
@ -25,6 +26,7 @@ internal sealed interface WalletState : WalletStateHolder {
abstract val nftState: WalletNFTItemUM
abstract val type: WalletType
abstract val tangemPayMainUM: TangemPayMainUM
abstract val virtualAccountMainUM: VirtualAccountMainUM
abstract val assetsDiscoveryProgressUM: AssetsDiscoveryProgressUM
data class Content(
@ -37,6 +39,7 @@ internal sealed interface WalletState : WalletStateHolder {
override val nftState: WalletNFTItemUM,
override val type: WalletType,
override val tangemPayMainUM: TangemPayMainUM,
override val virtualAccountMainUM: VirtualAccountMainUM = VirtualAccountMainUM.Empty,
override val assetsDiscoveryProgressUM: AssetsDiscoveryProgressUM = AssetsDiscoveryProgressUM.Idle,
) : MultiCurrency()
@ -57,6 +60,7 @@ internal sealed interface WalletState : WalletStateHolder {
override val tokensListState = WalletTokensListState.ContentState.Locked
override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden
override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty
override val virtualAccountMainUM: VirtualAccountMainUM = VirtualAccountMainUM.Empty
override val assetsDiscoveryProgressUM: AssetsDiscoveryProgressUM = AssetsDiscoveryProgressUM.Idle
}
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
@ -26,6 +27,8 @@ internal sealed interface WalletUM {
val tangemPayMainUM: TangemPayMainUM
val virtualAccountMainUM: VirtualAccountMainUM
data class Content(
override val pullToRefreshConfig: PullToRefreshConfig,
override val walletsBalanceUM: WalletBalanceUM,
@ -36,6 +39,7 @@ internal sealed interface WalletUM {
override val nftState: WalletNFTItemUM,
override val type: WalletType,
override val tangemPayMainUM: TangemPayMainUM,
override val virtualAccountMainUM: VirtualAccountMainUM,
) : WalletUM
data class Locked(
@ -49,5 +53,6 @@ internal sealed interface WalletUM {
override val tokensListUM: WalletTokensListUM = WalletTokensListUM.Locked
override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden
override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty
override val virtualAccountMainUM: VirtualAccountMainUM = VirtualAccountMainUM.Empty
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons
import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate
@ -34,11 +35,18 @@ internal class SetRefreshStateTransformer(
override fun transform(walletUM: WalletUM): WalletUM {
return when (walletUM) {
is WalletUM.Content -> walletUM.copy(
pullToRefreshConfig = walletUM.pullToRefreshConfig.toUpdatedState(isRefreshing),
tokensListUM = walletUM.tokensListUM.toUpdatedState(),
buttons = walletUM.enableButtons(),
)
is WalletUM.Content -> {
val tokensListUM = walletUM.tokensListUM.toUpdatedState()
walletUM.copy(
pullToRefreshConfig = walletUM.pullToRefreshConfig.toUpdatedState(isRefreshing),
tokensListUM = tokensListUM,
buttons = if (tokensListUM is WalletTokensListUM.Empty) {
walletUM.disableButtons()
} else {
walletUM.enableButtons()
},
)
}
is WalletUM.Locked -> walletUM
}
}

View file

@ -9,8 +9,10 @@ import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons
import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import com.tangem.utils.logging.TangemLogger
import java.math.BigDecimal
@ -35,6 +37,12 @@ internal class SetTokenListTransformer(
)
}
private val virtualAccountConverter by lazy {
VirtualAccountMainBlockConverter(
isRedesignEnabled = isRedesignEnabled,
)
}
override fun transform(prevState: WalletState): WalletState {
return when (prevState) {
is WalletState.MultiCurrency.Content -> {
@ -42,6 +50,7 @@ internal class SetTokenListTransformer(
walletCardState = prevState.walletCardState.toLoadedState(),
tokensListState = prevState.tokensListState.toLoadedState(),
tangemPayMainUM = prevState.tangemPayMainUM.toLoadedState(),
virtualAccountMainUM = prevState.virtualAccountMainUM.toLoadedVirtualState(),
buttons = prevState.enableButtons(),
)
}
@ -60,11 +69,17 @@ internal class SetTokenListTransformer(
override fun transform(walletUM: WalletUM): WalletUM {
return when (walletUM) {
is WalletUM.Content -> {
val tokensListUM = toLoadedState()
walletUM.copy(
walletsBalanceUM = walletUM.walletsBalanceUM.toLoadedState2(),
tangemPayMainUM = walletUM.tangemPayMainUM.toLoadedState(),
tokensListUM = toLoadedState(),
buttons = walletUM.enableButtons(),
virtualAccountMainUM = walletUM.virtualAccountMainUM.toLoadedVirtualState(),
tokensListUM = tokensListUM,
buttons = if (tokensListUM is WalletTokensListUM.Empty) {
walletUM.disableButtons()
} else {
walletUM.enableButtons()
},
)
}
is WalletUM.Locked -> {
@ -121,6 +136,17 @@ internal class SetTokenListTransformer(
return tangemPayConverter.convert(paymentAccountStatus)
}
private fun VirtualAccountMainUM.toLoadedVirtualState(): VirtualAccountMainUM {
val virtualAccountStatus = when (params) {
is TokenConverterParams.Account -> params.accountList.accountStatuses
.filterIsInstance<AccountStatus.Virtual>()
.firstOrNull()
is TokenConverterParams.Wallet -> return VirtualAccountMainUM.Empty
} ?: return this
return virtualAccountConverter.convert(virtualAccountStatus)
}
private fun toLoadedState(): WalletTokensListUM {
if (params !is TokenConverterParams.Account) {
return WalletTokensListUM.Empty(

View file

@ -63,6 +63,7 @@ internal class SingleWalletOnrampTransactionConverter(
timestampAgoFormatted = mapFormattedDate(value.timestamp),
activeStatus = value.status.toActiveStatusText(currency.name),
toAmount = stringReference(value.toAmount.format { crypto(currency) }),
toAmountValue = value.toAmount,
toFiatAmount = stringReference(
status.fiatRate?.multiply(value.toAmount).format {
fiat(
@ -73,6 +74,7 @@ internal class SingleWalletOnrampTransactionConverter(
),
toAmountSymbol = currency.symbol,
toCurrencyIcon = iconStateConverter.convert(currency),
toAddress = status.networkAddress?.defaultAddress?.value.orEmpty(),
fromAmount = stringReference(
value.fromAmount.format {
fiat(
@ -81,12 +83,14 @@ internal class SingleWalletOnrampTransactionConverter(
)
},
),
fromAmountValue = value.fromAmount,
fromFiatAmount = null,
fromAmountSymbol = value.fromCurrency.code,
fromCurrencyIcon = CurrencyIconState.FiatIcon(
url = value.fromCurrency.image,
fallbackResId = R.drawable.ic_currency_24,
),
fromAddress = null,
iconState = value.status.toIconState(),
onGoToProviderClick = { url ->
analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider())

View file

@ -13,6 +13,7 @@ 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.models.pay.TangemPayCardState
import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.utils.converter.Converter
@ -57,8 +58,8 @@ internal class TangemPayMainBlockConverter(
subtitle = TextReference.Res(R.string.tangempay_status_deactivated),
isBalanceFlickering = statusValue.source == StatusSource.CACHE,
balance = getBalanceText(
currencyCode = statusValue.fiatBalance.currency,
balance = statusValue.fiatBalance.availableBalance,
currencyCode = statusValue.balance.fiatBalance.currency,
balance = statusValue.balance.fiatBalance.availableBalance,
),
balanceSubtitle = stringReference(statusValue.cryptoCurrency.symbol),
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,
@ -67,15 +68,15 @@ internal class TangemPayMainBlockConverter(
is PaymentAccountStatusValue.Loaded -> {
val card = statusValue.cards.firstOrNull() ?: return TangemPayMainUM.TemporaryUnavailable
TangemPayMainUM.Content(
subtitle = if (card.isReissuing) {
subtitle = if (card.state == TangemPayCardState.Reissuing) {
resourceReference(R.string.tangempay_status_replacing)
} else {
stringReference("*${card.lastDigits}")
},
isBalanceFlickering = statusValue.source == StatusSource.CACHE,
balance = getBalanceText(
currencyCode = statusValue.currencyCode,
balance = statusValue.fiatBalance.availableBalance,
currencyCode = statusValue.balance.fiatBalance.currency,
balance = statusValue.balance.fiatBalance.availableBalance,
),
balanceSubtitle = stringReference(statusValue.cryptoCurrency.symbol),
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,

View file

@ -0,0 +1,86 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import androidx.compose.ui.text.SpanStyle
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.formatStyled
import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.VirtualAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
internal class VirtualAccountMainBlockConverter(
private val isRedesignEnabled: Boolean,
) : Converter<AccountStatus.Virtual, VirtualAccountMainUM> {
override fun convert(value: AccountStatus.Virtual): VirtualAccountMainUM {
return when (val statusValue = value.value) {
is VirtualAccountStatusValue.Empty -> VirtualAccountMainUM.Empty
is VirtualAccountStatusValue.NotCreated -> VirtualAccountMainUM.Empty
is VirtualAccountStatusValue.Loading -> VirtualAccountMainUM.Loading
is VirtualAccountStatusValue.UnderReview -> VirtualAccountMainUM.UnderReview(
subtitle = when (statusValue.kycStatus) {
KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed)
else -> TextReference.Res(R.string.tangempay_kyc_in_progress)
},
onClick = {
// TODO([REDACTED_TASK_KEY]): navigate to VA screen
},
)
is VirtualAccountStatusValue.Provisioning -> VirtualAccountMainUM.Provisioning(
onClick = {
// TODO([REDACTED_TASK_KEY]): navigate to VA screen
},
)
is VirtualAccountStatusValue.CountryNotSupported -> VirtualAccountMainUM.CountryNotSupported(
onClick = {
// TODO([REDACTED_TASK_KEY]): navigate to VA screen
},
)
is VirtualAccountStatusValue.Active -> VirtualAccountMainUM.Content(
subtitle = stringReference(statusValue.cryptoCurrency.symbol),
isBalanceFlickering = statusValue.source == StatusSource.CACHE,
balance = getBalanceText(
currencyCode = statusValue.fiatBalance.currency,
balance = statusValue.fiatBalance.availableBalance,
),
balanceSubtitle = stringReference(statusValue.cryptoCurrency.symbol),
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,
onClick = {
// TODO([REDACTED_TASK_KEY]): navigate to VA screen
},
)
is VirtualAccountStatusValue.Error.Unavailable -> VirtualAccountMainUM.TemporaryUnavailable
is VirtualAccountStatusValue.Error.NotSynced -> VirtualAccountMainUM.SyncNeeded
is VirtualAccountStatusValue.Error.ExposedDevice -> VirtualAccountMainUM.ExposedDevice
}
}
private fun getBalanceText(currencyCode: String, balance: BigDecimal): TextReference {
val currency = getJavaCurrencyByCode(currencyCode)
val formattedBalance = if (isRedesignEnabled) {
balance.formatStyled {
fiat(
fiatCurrencyCode = currency.currencyCode,
fiatCurrencySymbol = currency.symbol,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
)
}
} else {
stringReference(
balance.format {
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
},
)
}
return formattedBalance
}
}

View file

@ -193,20 +193,22 @@ internal class WalletTokenCurrencyItemConverter(
is CryptoCurrencyStatus.MissedDerivation -> TangemTokenRowUM.EndContentUM.Content(
text = stringReference(StringsSigns.DASH_SIGN),
)
is CryptoCurrencyStatus.Unreachable -> TangemTokenRowUM.EndContentUM.Content(
text = styledResourceReference(
id = R.string.common_unreachable,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) },
),
endIcons = persistentListOf(
TangemIconUM.Icon(
iconRes = R.drawable.ic_attention_default_24,
tintReference = { TangemTheme.colors2.graphic.status.attention },
),
),
)
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> TangemTokenRowUM.EndContentUM.Empty
-> {
TangemTokenRowUM.EndContentUM.Content(
text = styledResourceReference(
id = R.string.common_unreachable,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) },
),
endIcons = persistentListOf(
TangemIconUM.Icon(
iconRes = R.drawable.ic_attention_default_24,
tintReference = { TangemTheme.colors2.graphic.status.attention },
),
),
)
}
}
}

View file

@ -171,7 +171,7 @@ internal class WalletTokensListUMConverter(
} else {
R.drawable.ic_filter_24
}
return if (accountList.flattenCurrencies().size > 1 && !selectedWallet.isSingleWalletWithToken()) {
return if (accountList.flattenCurrencies().isNotEmpty() && !selectedWallet.isSingleWalletWithToken()) {
TangemButtonUM(
text = resourceReference(textRes),
isEnabled = accountList.totalFiatBalance !is TotalFiatBalance.Loading,

View file

@ -18,6 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfo
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import com.tangem.utils.extensions.addIf
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
@ -70,6 +71,7 @@ internal class WalletLoadingStateFactory(
is UserWallet.Hot -> WalletType.Hot
},
tangemPayMainUM = TangemPayMainUM.Empty,
virtualAccountMainUM = VirtualAccountMainUM.Empty,
)
}
@ -185,6 +187,8 @@ internal class WalletLoadingStateFactory(
}
private fun createWalletActions(userWallet: UserWallet): PersistentList<TangemButtonUM> {
val isSingleWalletWithToken =
userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
return buildList {
add(
WalletActionButtons.AddFunds(
@ -193,7 +197,7 @@ internal class WalletLoadingStateFactory(
).buttonUM,
)
addIf(
condition = !userWallet.isSingleWallet(),
condition = !userWallet.isSingleWallet() && !isSingleWalletWithToken,
element = WalletActionButtons.Swap(
isEnabled = false,
onClick = {

View file

@ -85,15 +85,19 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrenc
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Suppress("LongParameterList")
@Composable
internal fun WalletScreen(
state: WalletScreenState,
tangemPayComponent: TangemPayMainBlockComponent,
virtualAccountComponent: VirtualAccountMainBlockComponent,
promoBannersBlockComponent: ComposableContentComponent? = null,
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
bottomSheetHeaderHeightProvider: () -> Dp,
@ -109,6 +113,7 @@ internal fun WalletScreen(
WalletContent(
state = state,
tangemPayComponent = tangemPayComponent,
virtualAccountComponent = virtualAccountComponent,
walletsListState = walletsListState,
snackbarHostState = snackbarHostState,
isAutoScroll = isAutoScroll,
@ -132,6 +137,7 @@ internal fun WalletScreen(
private fun WalletContent(
state: WalletScreenState,
tangemPayComponent: TangemPayMainBlockComponent,
virtualAccountComponent: VirtualAccountMainBlockComponent,
walletsListState: LazyListState,
snackbarHostState: SnackbarHostState,
isAutoScroll: State<Boolean>,
@ -231,6 +237,13 @@ private fun WalletContent(
tangemPayComponent = tangemPayComponent,
)
virtualAccountItem(
modifier = itemModifier,
state = selectedWallet,
isHidingMode = state.isHidingMode,
virtualAccountComponent = virtualAccountComponent,
)
(selectedWallet as? WalletState.SingleCurrency)?.let { walletState ->
walletState.marketPriceBlockState?.let { marketPriceBlockState ->
marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier)
@ -758,6 +771,23 @@ internal fun LazyListScope.tangemPayItem(
}
}
internal fun LazyListScope.virtualAccountItem(
state: WalletState,
isHidingMode: Boolean,
virtualAccountComponent: VirtualAccountMainBlockComponent,
modifier: Modifier = Modifier,
) {
if (state !is WalletState.MultiCurrency) return
with(virtualAccountComponent) {
virtualAccountMainContent(
modifier = modifier,
state = state.virtualAccountMainUM,
isBalanceHidden = isHidingMode,
)
}
}
@Composable
private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
if (bottomSheetConfig != null) {
@ -784,6 +814,14 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider::
) {
}
},
virtualAccountComponent = object : VirtualAccountMainBlockComponent {
override fun LazyListScope.virtualAccountMainContent(
state: VirtualAccountMainUM,
isBalanceHidden: Boolean,
modifier: Modifier,
) {
}
},
bottomSheetContent = {
Text("Markets Content")
},

View file

@ -31,7 +31,6 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -46,21 +45,22 @@ 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
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggableHeader
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshSlidingContainer
import com.tangem.core.ui.components.containers.pullToRefresh.getPullToRefreshIndicatorOffset
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.rememberIsKeyboardVisible
import com.tangem.core.ui.components.sheetscaffold.*
import com.tangem.core.ui.decompose.ComposableContentComponent
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.extensions.softLayerShadow
import com.tangem.core.ui.res.*
import com.tangem.core.ui.utils.TangemSharedTransitionLayout
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData
@ -76,6 +76,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.common.Wallet
import com.tangem.feature.wallet.presentation.wallet.ui.utils.lazyListStateMapSaver
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeTint
import kotlinx.coroutines.launch
@ -84,11 +86,14 @@ import kotlin.math.abs
private const val MARKET_HINT_THRESHOLD = 0.5f
@OptIn(ExperimentalDecomposeApi::class)
@Suppress("LongParameterList")
@Composable
internal fun WalletScreen2(
state: WalletScreenState,
tangemPayComponent: TangemPayMainBlockComponent,
virtualAccountComponent: VirtualAccountMainBlockComponent,
modifier: Modifier = Modifier,
promoBannersBlockComponent: ComposableContentComponent? = null,
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
bottomSheetHeaderHeightProvider: () -> Dp,
onBottomSheetStateChange: (BottomSheetState) -> Unit,
@ -133,6 +138,8 @@ internal fun WalletScreen2(
state = state,
walletsPagerState = walletsPagerState,
tangemPayComponent = tangemPayComponent,
promoBannersBlockComponent = promoBannersBlockComponent,
virtualAccountComponent = virtualAccountComponent,
behavior = behavior,
bottomSheetContent = bottomSheetContent,
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
@ -160,9 +167,11 @@ private fun WalletContent2(
state: WalletScreenState,
walletsPagerState: PagerState,
tangemPayComponent: TangemPayMainBlockComponent,
virtualAccountComponent: VirtualAccountMainBlockComponent,
behavior: TangemCollapsingAppBarBehavior,
listStates: Map<Int, LazyListState>,
modifier: Modifier = Modifier,
promoBannersBlockComponent: ComposableContentComponent? = null,
bottomSheetHeaderHeightProvider: () -> Dp,
onBottomSheetStateChange: (BottomSheetState) -> Unit,
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
@ -176,6 +185,7 @@ private fun WalletContent2(
state.wallets2.getOrNull(state.selectedWalletIndex)?.pullToRefreshConfig,
)
}
var subtitleBottom by remember { mutableStateOf(0.dp) }
BaseScaffoldWithMarkets(
modifier = modifier,
@ -208,7 +218,7 @@ private fun WalletContent2(
val pullToRefreshState = rememberPullToRefreshState()
Box(
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.hazeSourceTangem(zIndex = -2f),
@ -240,6 +250,7 @@ private fun WalletContent2(
pullToRefreshState = pullToRefreshState,
pullToRefreshConfig = pullToRefreshConfig,
behavior = behavior,
topOffset = subtitleBottom + TangemTheme.dimens2.x2,
)
val overlay = TangemTheme.colors2.overlay.overlayPrimary
@ -286,6 +297,11 @@ private fun WalletContent2(
val pageSlideAlpha by rememberPageAlpha(walletsPagerState, currentWalletIndex)
val pullToRefreshContentOffset = getPullToRefreshIndicatorOffset(
pullToRefreshConfig = currentWallet.pullToRefreshConfig,
pullToRefreshState = pullToRefreshState,
)
TangemSharedTransitionLayout(
modifier = Modifier
.fillMaxSize()
@ -301,11 +317,20 @@ private fun WalletContent2(
TangemCollapsingTopBar(
state = behavior.state,
collapsingPart = {
val balanceBlockHeight = with(LocalDensity.current) {
-behavior.state.heightOffsetLimit.toDp()
}
WalletBalance(
behavior = behavior,
walletBalanceUM = currentWallet.walletsBalanceUM,
buttons = currentWallet.buttons,
isBalanceHidden = state.isHidingMode,
modifier = Modifier.height(balanceBlockHeight),
onSubtitleBottomChange = { newValue ->
if (pullToRefreshContentOffset == 0.dp && newValue > subtitleBottom) {
subtitleBottom = newValue
}
},
)
},
body = {
@ -315,6 +340,8 @@ private fun WalletContent2(
isBalanceHidden = state.isHidingMode,
contentPadding = contentPadding,
tangemPayComponent = tangemPayComponent,
promoBannersBlockComponent = promoBannersBlockComponent,
virtualAccountComponent = virtualAccountComponent,
modifier = Modifier
.fillMaxSize()
.nestedScroll(behavior.nestedScrollConnection),
@ -337,11 +364,12 @@ private fun WalletContent2(
MarketsTooltip(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 24.dp)
.padding(bottom = 8.dp)
.padding(horizontal = 12.dp)
.fillMaxWidth(),
isVisible = state.showMarketsOnboarding,
availableHeight = LocalWindowSize.current.height,
availableHeight = maxHeight,
sheetTopInset = TangemTheme.dimens2.x3,
bottomSheetState = bottomSheetState,
onCloseClick = state.onDismissMarketsTooltip,
)
@ -494,26 +522,14 @@ private fun BottomSheet(
.sizeIn(maxHeight = maxHeight - statusBarHeight),
) {
Box(modifier = Modifier.fillMaxWidth()) {
BottomFade(
gradientBrush = Brush.verticalGradient(
colors = listOf(
TangemTheme.colors2.shadow.min,
TangemTheme.colors2.shadow.max,
),
),
modifier = Modifier
.offset(y = TangemTheme.dimens2.x5.unaryMinus()),
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
TangemBottomSheetDraggableHeader()
Box(
modifier = Modifier
.fillMaxWidth()
.softLayerShadow(
radius = 16.dp,
radius = 8.dp,
spread = 0.dp,
color = Color.Black.copy(alpha = if (LocalIsInDarkTheme.current) .24f else .12f),
shape = shape,
offset = DpOffset(x = 0.dp, y = (-6).dp),
@ -651,6 +667,14 @@ private fun WalletScreen2_Preview(@PreviewParameter(WalletScreen2PreviewProvider
) {
}
},
virtualAccountComponent = object : VirtualAccountMainBlockComponent {
override fun LazyListScope.virtualAccountMainContent(
state: VirtualAccountMainUM,
isBalanceHidden: Boolean,
modifier: Modifier,
) {
}
},
bottomSheetContent = {
Text("Markets Content")
},

View file

@ -10,19 +10,17 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.*
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
@ -31,12 +29,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.*
import com.tangem.core.ui.components.sheetscaffold.TangemSheetState
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -53,10 +46,11 @@ internal fun MarketsTooltip(
bottomSheetState: TangemSheetState,
isVisible: Boolean,
onCloseClick: () -> Unit,
sheetTopInset: Dp,
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current
val tooltipOffset by remember {
val tooltipOffset by remember(availableHeight, sheetTopInset) {
derivedStateOf {
val bottomSheetOffset = try {
// Can throw exception during the first composition
@ -64,8 +58,7 @@ internal fun MarketsTooltip(
} catch (e: Exception) {
0.dp
}
bottomSheetOffset - availableHeight
bottomSheetOffset + sheetTopInset - availableHeight
}
}

View file

@ -2,10 +2,16 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import com.tangem.core.ui.ds.button.TangemButton
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
internal fun LazyListScope.nftCollections2(state: WalletUM, itemModifier: Modifier) {
(state as? WalletUM.Content)?.let { content ->
@ -25,9 +31,14 @@ internal fun LazyListScope.organizeTokens2(state: WalletUM, itemModifier: Modifi
key = "OrganizeTokensButton",
contentType = "OrganizeTokensButton",
) {
val testTag = if (organizeButton.text == resourceReference(R.string.main_add_and_manage_tokens)) {
MainScreenTestTags.ADD_AND_MANAGE_BUTTON
} else {
MainScreenTestTags.ORGANIZE_TOKENS_BUTTON
}
TangemButton(
buttonUM = organizeButton,
modifier = itemModifier,
modifier = itemModifier.testTag(testTag),
)
}
}
@ -42,4 +53,15 @@ internal fun LazyListScope.tangemPay(
with(tangemPayComponent) {
tangemPayMainContent(modifier = modifier, state = tangemPayUM, isBalanceHidden = isBalanceHidden)
}
}
internal fun LazyListScope.virtualAccount(
virtualAccountComponent: VirtualAccountMainBlockComponent,
virtualAccountUM: VirtualAccountMainUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
with(virtualAccountComponent) {
virtualAccountMainContent(modifier = modifier, state = virtualAccountUM, isBalanceHidden = isBalanceHidden)
}
}

View file

@ -16,11 +16,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextOverflow
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.TextShimmer
@ -32,11 +36,7 @@ import com.tangem.core.ui.ds.placeholder.TextPlaceholder
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior
import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior
import com.tangem.core.ui.ds.topbar.collapsing.snapToExitUntilCollapsed
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.MainScreenTestTags
@ -58,14 +58,17 @@ internal fun WalletBalance(
buttons: ImmutableList<TangemButtonUM>,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
onSubtitleBottomChange: (Dp) -> Unit = {},
) {
val collapsedFraction = behavior.state.collapsedFraction
val alpha = 1f - collapsedFraction
val scale = alpha.coerceIn(MIN_SCALE, MAX_SCALE)
val density = LocalDensity.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.testTag(MainScreenTestTags.WALLET_LIST_ITEM)
.alpha(alpha)
.scale(scale)
.snapToExitUntilCollapsed(behavior)
@ -85,7 +88,15 @@ internal fun WalletBalance(
isBalanceHidden = isBalanceHidden,
)
SpacerH(TangemTheme.dimens2.x3)
SubtitleRow(walletBalanceUM = walletBalanceUM)
Box(
modifier = Modifier.onGloballyPositioned { coordinates ->
val rawBottomPx = coordinates.boundsInRoot().bottom
if (rawBottomPx <= 0f) return@onGloballyPositioned
onSubtitleBottomChange(with(density) { rawBottomPx.toDp() })
},
) {
SubtitleRow(walletBalanceUM = walletBalanceUM)
}
}
SpacerH(TangemTheme.dimens2.x2)
ActionButtons(buttons)
@ -140,8 +151,12 @@ private fun SubtitleRow(walletBalanceUM: WalletBalanceUM, modifier: Modifier = M
text = walletBalanceUM.name,
style = TangemTheme.typography2.bodyRegular14,
color = TangemTheme.colors2.text.neutral.tertiary,
modifier = Modifier.testTag(MainScreenTestTags.CARD_TITLE),
)
TangemDeviceIcon(
state = walletBalanceUM.deviceIcon,
modifier = Modifier.testTag(MainScreenTestTags.CARD_IMAGE),
)
TangemDeviceIcon(state = walletBalanceUM.deviceIcon)
}
}
}
@ -152,6 +167,7 @@ private fun SubtitleRow(walletBalanceUM: WalletBalanceUM, modifier: Modifier = M
private fun Balance(walletBalanceUM: WalletBalanceUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = walletBalanceUM,
contentKey = { it::class },
label = "Update the balance",
modifier = modifier.testTag(MainScreenTestTags.WALLET_BALANCE),
transitionSpec = {

View file

@ -337,6 +337,7 @@ private fun AdditionalInfo(
id = R.string.initial_wallet_sync_restore_progress,
formatArgs = wrappedList(animatedContent.progressPercent),
),
testTag = MainScreenTestTags.SYNC_PROGRESS_TEXT,
)
CircularProgressIndicator(
modifier = Modifier.size(TangemTheme.dimens.size16),
@ -353,14 +354,14 @@ private fun AdditionalInfo(
}
@Composable
private fun AdditionalInfoText(text: TextReference) {
private fun AdditionalInfoText(text: TextReference, testTag: String = MainScreenTestTags.DEVICES_COUNT) {
Text(
text = text.resolveReference(),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.caption2,
modifier = Modifier.testTag(MainScreenTestTags.DEVICES_COUNT),
modifier = Modifier.testTag(testTag),
)
}

View file

@ -9,12 +9,15 @@ import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.paging.compose.LazyPagingItems
import com.tangem.common.ui.notifications.notifications
import com.tangem.common.ui.notifications.notificationsCarousel
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.components.transactions.txHistoryItems
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems
@ -22,17 +25,22 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
import com.tangem.feature.wallet.presentation.wallet.ui.components.nftCollections2
import com.tangem.feature.wallet.presentation.wallet.ui.components.organizeTokens2
import com.tangem.feature.wallet.presentation.wallet.ui.components.tangemPay
import com.tangem.feature.wallet.presentation.wallet.ui.components.virtualAccount
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
import kotlinx.collections.immutable.toPersistentList
@Suppress("LongParameterList")
@Composable
internal fun WalletListContent(
currentWallet: WalletUM,
isBalanceHidden: Boolean,
listState: LazyListState,
tangemPayComponent: TangemPayMainBlockComponent,
virtualAccountComponent: VirtualAccountMainBlockComponent,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
promoBannersBlockComponent: ComposableContentComponent? = null,
) {
val containerColor = TangemTheme.colors2.surface.level1
@ -40,7 +48,7 @@ internal fun WalletListContent(
val itemModifier = movableItemModifier.padding(top = TangemTheme.dimens2.x3)
LazyColumn(
modifier = modifier,
modifier = modifier.testTag(MainScreenTestTags.SCREEN_CONTAINER),
state = listState,
contentPadding = contentPadding,
horizontalAlignment = Alignment.CenterHorizontally,
@ -57,6 +65,12 @@ internal fun WalletListContent(
notifications = currentWallet.notificationsCarousel.map { it.messageUM }.toPersistentList(),
)
promoBannersBlockComponent?.let { component ->
item(key = "PromoBannersBlock") {
component.Content(modifier = itemModifier)
}
}
tangemPay(
tangemPayComponent = tangemPayComponent,
tangemPayUM = currentWallet.tangemPayMainUM,
@ -64,6 +78,13 @@ internal fun WalletListContent(
modifier = itemModifier,
)
virtualAccount(
virtualAccountComponent = virtualAccountComponent,
virtualAccountUM = currentWallet.virtualAccountMainUM,
isBalanceHidden = isBalanceHidden,
modifier = itemModifier,
)
tokensListItems2(
walletTokensListUM = currentWallet.tokensListUM,
modifier = movableItemModifier,

View file

@ -1,9 +1,11 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.common
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
@ -16,6 +18,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.ForceDarkTheme
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.WalletNotificationTestTags
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import kotlinx.collections.immutable.ImmutableList
@ -73,26 +76,35 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
subtitle = resourceReference(R.string.tangempay_onboarding_banner_description),
)
}
else -> {
Notification(
config = item.config,
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
iconTint = when (item) {
is WalletNotification.Critical -> TangemTheme.colors.icon.warning
is WalletNotification.Informational -> TangemTheme.colors.icon.accent
is WalletNotification.RateApp -> TangemTheme.colors.icon.attention
is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1
is WalletNotification.UsedOutdatedData -> TangemTheme.colors.text.attention
else -> null
},
subtitleColor = TangemTheme.colors.text.secondary,
)
}
else -> DefaultWalletNotification(item = item, modifier = modifier)
}
},
)
}
@Composable
private fun LazyItemScope.DefaultWalletNotification(item: WalletNotification, modifier: Modifier = Modifier) {
val itemModifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
val taggedModifier = when (item) {
is WalletNotification.AssetsDiscoveryCompleted ->
itemModifier.testTag(WalletNotificationTestTags.ASSETS_DISCOVERY_BANNER)
else -> itemModifier
}
Notification(
config = item.config,
modifier = taggedModifier,
iconTint = when (item) {
is WalletNotification.Critical -> TangemTheme.colors.icon.warning
is WalletNotification.Informational -> TangemTheme.colors.icon.accent
is WalletNotification.RateApp -> TangemTheme.colors.icon.attention
is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1
is WalletNotification.UsedOutdatedData -> TangemTheme.colors.text.attention
else -> null
},
subtitleColor = TangemTheme.colors.text.secondary,
)
}
@Composable
private fun yieldBoostPromoTitle(): AnnotatedString {
val accent = TangemTheme.colors.text.accent

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.containers.pullToRefresh.getPullToRefreshIndicatorOffset
import com.tangem.core.ui.ds.TangemPagerIndicator
@ -23,7 +24,6 @@ import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior
private const val MIN_SCALE = 0.75f
private const val MAX_SCALE = 1f
private const val WALLET_INDICATOR_OFFSET = 0.6f
@Composable
internal fun WalletPagerIndicator(
@ -31,6 +31,7 @@ internal fun WalletPagerIndicator(
behavior: TangemCollapsingAppBarBehavior,
pullToRefreshConfig: PullToRefreshConfig?,
pullToRefreshState: PullToRefreshState,
topOffset: Dp,
) {
val collapsedFraction = behavior.state.collapsedFraction
val alpha = MAX_SCALE - collapsedFraction
@ -43,7 +44,6 @@ internal fun WalletPagerIndicator(
pullToRefreshConfig = pullToRefreshConfig,
pullToRefreshState = pullToRefreshState,
)
val padding = height * WALLET_INDICATOR_OFFSET
AnimatedVisibility(
visible = pagerState.pageCount > 1,
@ -64,7 +64,7 @@ internal fun WalletPagerIndicator(
TangemPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.padding(top = padding)
.padding(top = topOffset)
.scale(scaleY = 1f, scaleX = scale),
)
}

View file

@ -93,7 +93,10 @@ internal fun WalletTopBar(
),
) {
topBarConfig.endActions.forEach { action ->
TangemTopBarActionContent(action)
TangemTopBarActionContent(
action,
modifier = Modifier.testTag(MainScreenTestTags.MORE_BUTTON),
)
}
}
},

View file

@ -289,7 +289,7 @@ private fun LazyListScope.accountItem(
val portfolioModifier = modifier
.padding(top = if (index != 0) TangemTheme.dimens2.x2 else TangemTheme.dimens2.x3)
.testTag(MainScreenTestTags.TOKEN_LIST_ITEM)
.testTag(MainScreenTestTags.ACCOUNT_LIST_ITEM)
.semantics { lazyListItemPosition = index }
.roundedShapeItemDecoration(
currentIndex = 0,
@ -522,7 +522,7 @@ private fun LazyListScope.nonContentAccountItem(listItem: TokensListItemUM2.Port
@Composable
internal fun NonContentItemContentV2(textColor: Color, modifier: Modifier = Modifier, onClick: () -> Unit) {
Column(
modifier = modifier,
modifier = modifier.testTag(MainScreenTestTags.EMPTY_TOKENS_PLACEHOLDER),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
@ -545,7 +545,7 @@ internal fun NonContentItemContentV2(textColor: Color, modifier: Modifier = Modi
onClick = onClick,
size = TangemButtonSize.X8,
shape = TangemButtonShape.Rounded,
modifier = Modifier,
modifier = Modifier.testTag(MainScreenTestTags.EMPTY_TOKENS_ADD_BUTTON),
)
}
}

View file

@ -0,0 +1,190 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.google.common.truth.Truth.assertThat
import com.tangem.common.ui.tokens.TokenConverterParams
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account.CryptoPortfolio.Companion.createMainAccount
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.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import io.mockk.every
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import org.junit.jupiter.api.Test
import java.math.BigDecimal
class SetTokenListTransformerTest {
private val userWalletId = UserWalletId("00")
@Test
fun `GIVEN empty account list WHEN transform THEN tokens are Empty and buttons disabled`() {
val transformer = createTransformer(accountList = emptyAccountList())
val result = transformer.transform(walletUM(buttonEnabled = true)) as WalletUM.Content
assertThat(result.tokensListUM).isInstanceOf(WalletTokensListUM.Empty::class.java)
assertThat(result.buttons).hasSize(1)
assertThat(result.buttons.single().isEnabled).isFalse()
}
@Test
fun `GIVEN non-empty account list WHEN transform THEN tokens not Empty and buttons enabled`() {
val transformer = createTransformer(accountList = nonEmptyAccountList())
val result = transformer.transform(walletUM(buttonEnabled = false)) as WalletUM.Content
assertThat(result.tokensListUM).isNotInstanceOf(WalletTokensListUM.Empty::class.java)
assertThat(result.buttons).hasSize(1)
assertThat(result.buttons.single().isEnabled).isTrue()
}
private fun createTransformer(accountList: AccountStatusList): SetTokenListTransformer {
val userWallet = mockk<UserWallet.Hot>(relaxed = true) {
every { walletId } returns userWalletId
}
return SetTokenListTransformer(
params = TokenConverterParams.Account(
accountList = accountList,
expandedAccounts = emptySet(),
),
userWallet = userWallet,
appCurrency = AppCurrency.Default,
clickIntents = mockk<WalletClickIntents>(relaxed = true),
shouldShowMainPromo = false,
isAccountsModeEnabled = false,
isRedesignEnabled = true,
isAddAndManageTokensEnabled = false,
)
}
private fun emptyAccountList(): AccountStatusList = AccountStatusList(
userWalletId = userWalletId,
accountStatuses = listOf(mainCryptoPortfolioStatus(tokenList = TokenList.Empty)),
totalAccounts = 1,
totalArchivedAccounts = 0,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
private fun nonEmptyAccountList(): AccountStatusList {
val token = createToken()
val tokenList = TokenList.Ungrouped(
currencies = listOf(createLoadedStatus(token)),
totalFiatBalance = TotalFiatBalance.Loaded(BigDecimal.ZERO, StatusSource.ACTUAL),
sortedBy = TokensSortType.NONE,
)
return AccountStatusList(
userWalletId = userWalletId,
accountStatuses = listOf(mainCryptoPortfolioStatus(tokenList = tokenList)),
totalAccounts = 1,
totalArchivedAccounts = 0,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
}
private fun mainCryptoPortfolioStatus(tokenList: TokenList): AccountStatus.CryptoPortfolio =
AccountStatus.CryptoPortfolio(
account = createMainAccount(userWalletId),
tokenList = tokenList,
priceChangeLce = Unit.lceError(),
)
private fun walletUM(buttonEnabled: Boolean): WalletUM.Content = WalletUM.Content(
pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}),
walletsBalanceUM = WalletBalanceUM.Loading(
id = userWalletId,
name = "test",
deviceIcon = DeviceIconUM.Mobile,
),
buttons = persistentListOf(
TangemButtonUM(
text = stringReference(value = "Buy"),
type = TangemButtonType.Secondary,
onClick = {},
isEnabled = buttonEnabled,
),
),
notifications = persistentListOf(),
notificationsCarousel = persistentListOf(),
tokensListUM = WalletTokensListUM.Loading,
nftState = WalletNFTItemUM.Hidden,
type = WalletType.Hot,
tangemPayMainUM = TangemPayMainUM.Empty,
virtualAccountMainUM = VirtualAccountMainUM.Empty,
)
private fun createToken(): CryptoCurrency.Token {
val network = Network(
id = Network.ID(value = "ethereum", derivationPath = Network.DerivationPath.None),
name = "ethereum",
currencySymbol = "ETH",
derivationPath = Network.DerivationPath.None,
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = false,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
)
return CryptoCurrency.Token(
id = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(rawId = "ethereum"),
suffix = CryptoCurrency.ID.Suffix.ContractAddress(contractAddress = "0xABCDEF"),
),
network = network,
name = "Token",
symbol = "TKN",
decimals = 18,
iconUrl = null,
isCustom = false,
contractAddress = "0xABCDEF",
)
}
private fun createLoadedStatus(token: CryptoCurrency.Token): CryptoCurrencyStatus {
val networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "addr",
type = NetworkAddress.Address.Type.Primary,
),
)
val value = CryptoCurrencyStatus.Loaded(
amount = BigDecimal.ONE,
fiatAmount = BigDecimal.ZERO,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
)
return CryptoCurrencyStatus(currency = token, value = value)
}
}

View file

@ -13,7 +13,7 @@ import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import org.junit.Test
import org.junit.jupiter.api.Test
import java.math.BigDecimal
class YieldSupplyPromoBannerConverterTest {

View file

@ -23,7 +23,7 @@ import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
internal class AddAndManageModelTest {

View file

@ -14,7 +14,7 @@ import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
internal class WalletContentClickIntentsAnalyticsTest {

View file

@ -44,8 +44,8 @@ import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultPromoDeeplinkHandlerTest {
@ -76,7 +76,7 @@ class DefaultPromoDeeplinkHandlerTest {
private lateinit var messages: MutableList<UiMessage>
@Before
@BeforeEach
fun setUp() {
MockKAnnotations.init(this)
every { analyticsEventHandler.send(any()) } returns Unit

View file

@ -0,0 +1,127 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import arrow.core.Either
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.yield.supply.promo.usecase.ShouldShowYieldBoostMainBannerUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class GetWalletNotificationsCarouselFactoryTest {
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase = mockk()
private val getWalletsUseCase: GetWalletsUseCase = mockk()
private val notificationsRepository: NotificationsRepository = mockk()
private val shouldShowYieldBoostMainBannerUseCase: ShouldShowYieldBoostMainBannerUseCase = mockk()
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase = mockk()
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles = mockk()
private val clickIntents: WalletClickIntents = mockk(relaxed = true)
private val userWallet: UserWallet.Hot = mockk(relaxed = true)
private val factory = GetWalletNotificationsCarouselFactory(
isReadyToShowRateAppUseCase = isReadyToShowRateAppUseCase,
getWalletsUseCase = getWalletsUseCase,
notificationsRepository = notificationsRepository,
shouldShowYieldBoostMainBannerUseCase = shouldShowYieldBoostMainBannerUseCase,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
)
@BeforeEach
fun setup() {
clearMocks(
isReadyToShowRateAppUseCase,
getWalletsUseCase,
notificationsRepository,
shouldShowYieldBoostMainBannerUseCase,
yieldSupplyGetShouldShowMainPromoUseCase,
yieldSupplyFeatureToggles,
clickIntents,
userWallet,
)
// Defaults: only the yield boost banner can appear; every other notification is suppressed.
every { userWallet.walletId } returns WALLET_ID
every { notificationsRepository.getShouldShowNotification(any()) } returns flowOf(false)
coEvery { notificationsRepository.isUserAllowToSubscribeOnPushNotifications() } returns true
every { isReadyToShowRateAppUseCase() } returns flowOf(false)
every { getWalletsUseCase() } returns flowOf(emptyList())
every { yieldSupplyGetShouldShowMainPromoUseCase() } returns flowOf(true)
every { yieldSupplyFeatureToggles.isYieldPromoEnabled } returns true
coEvery { shouldShowYieldBoostMainBannerUseCase(any()) } returns Either.Right(true)
}
@ParameterizedTest
@MethodSource("provideTestModels")
fun `GIVEN gating conditions WHEN create THEN yield boost banner visibility matches`(model: Model) = runTest {
// Arrange
every { yieldSupplyFeatureToggles.isYieldPromoEnabled } returns model.toggleEnabled
every { yieldSupplyGetShouldShowMainPromoUseCase() } returns flowOf(model.shouldShowLocal)
coEvery { shouldShowYieldBoostMainBannerUseCase(WALLET_ID) } returns model.mainBanner
// Act
val result = factory.create(userWallet, clickIntents).first()
// Assert
assertThat(result.any { it is WalletNotificationUM.YieldBoostPromo }).isEqualTo(model.expectedShown)
}
@Test
fun `GIVEN banner shown WHEN buttons clicked THEN routes to click intents`() = runTest {
// Arrange
val banner = factory.create(userWallet, clickIntents).first()
.filterIsInstance<WalletNotificationUM.YieldBoostPromo>()
.first()
// Act
banner.onExploreClick()
banner.onLaterClick()
// Assert
verify { clickIntents.onYieldBoostBannerClick(WALLET_ID) }
verify { clickIntents.onDismissYieldBoostBanner(WALLET_ID) }
}
internal data class Model(
val toggleEnabled: Boolean,
val shouldShowLocal: Boolean,
val mainBanner: Either<Throwable, Boolean>,
val expectedShown: Boolean,
)
private fun provideTestModels() = listOf(
Model(toggleEnabled = true, shouldShowLocal = true, mainBanner = Either.Right(true), expectedShown = true),
Model(toggleEnabled = false, shouldShowLocal = true, mainBanner = Either.Right(true), expectedShown = false),
Model(toggleEnabled = true, shouldShowLocal = false, mainBanner = Either.Right(true), expectedShown = false),
Model(toggleEnabled = true, shouldShowLocal = true, mainBanner = Either.Right(false), expectedShown = false),
Model(
toggleEnabled = true,
shouldShowLocal = true,
mainBanner = Either.Left(RuntimeException("boom")),
expectedShown = false,
),
)
private companion object {
val WALLET_ID = UserWalletId("01")
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.domain
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import org.junit.Test
import org.junit.jupiter.api.Test
/**
[REDACTED_AUTHOR]

View file

@ -1,7 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import com.google.common.truth.Truth
import org.junit.Test
import org.junit.jupiter.api.Test
/**
[REDACTED_AUTHOR]

View file

@ -5,7 +5,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import io.mockk.every
import io.mockk.mockk
import org.junit.Test
import org.junit.jupiter.api.Test
import java.math.BigDecimal
internal class QrContentClassifierTest {