Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-15 12:23:19 +03:00
commit b0ac045d0d
119 changed files with 2794 additions and 607 deletions

View file

@ -19,7 +19,7 @@ internal class OnrampOffersStateFactory(
return when (currentState) {
is OnrampV2MainComponentUM.InitialLoading -> currentState
is OnrampV2MainComponentUM.Content -> {
if (currentState.offersBlockState is OnrampOffersBlockUM.Loading && offers.isEmpty()) {
if (currentState.offersBlockState is OnrampOffersBlockUM.Loading) {
return currentState
}
currentState.copy(

View file

@ -64,7 +64,11 @@ internal class OnrampV2AmountStateFactory(
currencySymbol = currency.unit,
onAmountValueChanged = onrampIntents::onAmountValueChanged,
),
offersBlockState = OnrampOffersBlockUM.Loading,
offersBlockState = if (amountState.amountFieldModel.fiatValue.isEmpty()) {
OnrampOffersBlockUM.Empty
} else {
OnrampOffersBlockUM.Loading
},
)
}
@ -98,6 +102,7 @@ internal class OnrampV2AmountStateFactory(
amountBlockState = amountState.copy(secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty),
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
errorNotification = null,
offersBlockState = currentState.offersBlockState,
)
}

View file

@ -236,8 +236,17 @@ internal class OnrampV2MainComponentModel @Inject constructor(
maybeOffers.fold(
ifLeft = ::handleOnrampError,
ifRight = { offers ->
if (offers.isNotEmpty()) {
state.update { onrampOffersStateFactory.getOffersState(offers) }
val currentState = state.value
if (currentState is OnrampV2MainComponentUM.Content) {
if (currentState.amountBlockState.amountFieldModel.fiatValue.isEmpty()) {
state.update {
currentState.copy(offersBlockState = OnrampOffersBlockUM.Empty)
}
return@fold
}
state.update {
onrampOffersStateFactory.getOffersState(offers)
}
}
},
)
@ -301,7 +310,17 @@ internal class OnrampV2MainComponentModel @Inject constructor(
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
}
else -> {
state.update { amountStateFactory.getAmountSecondaryFieldResetState() }
state.update { prevState ->
val resetState = amountStateFactory.getAmountSecondaryFieldResetState()
if (prevState is OnrampV2MainComponentUM.Content &&
resetState is OnrampV2MainComponentUM.Content &&
prevState.offersBlockState is OnrampOffersBlockUM.Loading
) {
resetState.copy(offersBlockState = OnrampOffersBlockUM.Empty)
} else {
resetState
}
}
}
}
}

View file

@ -1651,8 +1651,7 @@ internal class SwapModel @Inject constructor(
private fun onTangemPaySupportClick(txId: String?) {
modelScope.launch {
val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId)
.getOrElse { error("CardInfo must be not null") }
val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch
val email = FeedbackEmailType.Visa.Withdrawal(
walletMetaInfo = metaInfo,
providerName = dataState.selectedProvider?.name.orEmpty(),

View file

@ -101,6 +101,7 @@ internal class TangemPayDetailsModel @Inject constructor(
val bottomSheetNavigation: SlotNavigation<TangemPayDetailsNavigation> = SlotNavigation()
init {
analytics.send(TangemPayAnalyticsEvents.MainScreenOpened())
handleBalanceHiding()
fetchAddToWalletBanner()
fetchBalance()
@ -212,6 +213,7 @@ internal class TangemPayDetailsModel @Inject constructor(
}
override fun onClickAddFunds() {
analytics.send(TangemPayAnalyticsEvents.AddFundsClicked())
val currentBalance = balance
val depositAddress = currentBalance?.depositAddress
if (currentBalance == null || depositAddress == null) {

View file

@ -9,8 +9,6 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverter
@ -21,10 +19,8 @@ import javax.inject.Inject
@Stable
@ModelScoped
@Suppress("LongParameterList")
internal class TangemPayTxHistoryDetailsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getUserWalletsUseCase: GetWalletsUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val urlOpener: UrlOpener,
@ -66,12 +62,7 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor(
private fun dispute() {
modelScope.launch {
val userWalletId = params.userWalletId
val userWallet = getUserWalletsUseCase.invokeSync()
.firstOrNull { it.walletId == userWalletId } ?: return@launch
val walletMetaInfo = getWalletMetaInfoUseCase.invoke(
userWallet.requireColdWallet().scanResponse,
).getOrNull() ?: return@launch
val walletMetaInfo = getWalletMetaInfoUseCase.invoke(params.userWalletId).getOrNull() ?: return@launch
sendFeedbackEmailUseCase.invoke(
FeedbackEmailType.Visa.DisputeV2(

View file

@ -234,7 +234,7 @@ internal object TangemPayTxHistoryDetailsConverter :
)
is TangemPayTxHistoryItem.Spend -> persistentListOf(
ButtonState(
text = resourceReference(R.string.tangem_pay_dispute),
text = resourceReference(R.string.tangem_pay_get_help),
onClick = this.onDisputeClick,
),
)

View file

@ -182,8 +182,7 @@ private fun TangemPayCardDetailsShownBlock(
Row(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth()
.weight(1f),
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
CardDetailsTextContainer(
@ -203,6 +202,7 @@ private fun TangemPayCardDetailsShownBlock(
onCopy = onCopyCvv,
)
}
Spacer(modifier = Modifier.weight(1f))
Row {
SpacerWMax()
TangemPayCardDetailsCustomButton(

View file

@ -6,6 +6,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -74,6 +75,7 @@ internal fun TangemPayTxHistoryDetailsContent(state: TangemPayTxHistoryDetailsUM
text = state.transactionSubtitle.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
Text(
modifier = Modifier.padding(top = 8.dp),
@ -84,7 +86,7 @@ internal fun TangemPayTxHistoryDetailsContent(state: TangemPayTxHistoryDetailsUM
state.localTransactionText?.let { localTransaction ->
Text(
modifier = Modifier.padding(top = 4.dp),
text = localTransaction,
text = localTransaction.orMaskWithStars(state.isBalanceHidden),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
@ -207,11 +209,11 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
dismiss = {},
),
TangemPayTxHistoryDetailsUM(
isBalanceHidden = false,
isBalanceHidden = true,
title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_category_24),
transactionTitle = stringReference("Starbucks"),
transactionSubtitle = stringReference("Food and drinks"),
transactionSubtitle = stringReference("Food and drinks Food and drinks Food and drinks Food and drinks"),
transactionAmount = "-$5.86",
transactionAmountColor = themedColor { TangemTheme.colors.text.primary1 },
localTransactionText = "€ 5.36",

View file

@ -340,7 +340,9 @@ private fun AddressItem(
onClick = onOpenQrCodeClick,
) {
Column(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 32.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
CurrencyIcon(

View file

@ -88,6 +88,7 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.yieldSupply)
implementation(projects.domain.yieldSupply.models)
/** Temp dependency to swap domain */
implementation(projects.features.swap.domain)

View file

@ -121,7 +121,7 @@ internal object TokenDetailsPreviewData {
selectedBalanceType = BalanceType.ALL,
onBalanceSelect = {},
displayCryptoBalance = "966,96 XLM",
displayYeildSupplyCryptoBalance = null,
displayYieldSupplyFiatBalance = null,
displayFiatBalance = "91,50$",
isBalanceSelectorEnabled = true,
isBalanceFlickering = false,

View file

@ -75,6 +75,7 @@ import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetRewardsBalanceUseCase
import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionListener
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
@ -416,7 +417,9 @@ internal class TokenDetailsModel @Inject constructor(
.saveIn(yieldSupplyBalanceJobHolder)
} else {
yieldSupplyBalanceJobHolder.cancel()
internalUiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(null)
internalUiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(
YieldSupplyRewardBalance.empty(),
)
}
}
@ -1261,14 +1264,8 @@ internal class TokenDetailsModel @Inject constructor(
}
private fun handleNavigationParam() {
when (val action = params.navigationAction) {
is NavigationAction.Staking -> openStaking()
is NavigationAction.YieldSupply -> if (action.isActive) {
modelScope.launch(dispatchers.default) {
fetchCurrencyStatusUseCase(userWalletId = userWalletId, id = cryptoCurrency.id)
}
}
else -> Unit
if (params.navigationAction is NavigationAction.Staking) {
openStaking()
}
}

View file

@ -28,7 +28,8 @@ internal sealed class TokenDetailsBalanceBlockState {
val isBalanceSelectorEnabled: Boolean,
val isBalanceFlickering: Boolean,
val yieldSupplyState: TokenDetailsYieldSupplyState = TokenDetailsYieldSupplyState.Empty,
val displayYeildSupplyCryptoBalance: String? = null,
val displayYieldSupplyFiatBalance: String? = null,
val displayYieldSupplyCryptoBalance: String? = null,
) : TokenDetailsBalanceBlockState()
data class Error(

View file

@ -98,8 +98,8 @@ internal class TokenDetailsLoadedBalanceConverter(
stakingCryptoAmount,
currentState.selectedBalanceType,
),
displayYeildSupplyCryptoBalance = (currentState as? TokenDetailsBalanceBlockState.Content)
?.displayYeildSupplyCryptoBalance,
displayYieldSupplyFiatBalance = (currentState as? TokenDetailsBalanceBlockState.Content)
?.displayYieldSupplyFiatBalance,
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
onBalanceSelect = clickIntents::onBalanceSelect,
selectedBalanceType = currentState.selectedBalanceType,

View file

@ -26,6 +26,7 @@ import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig
@ -315,13 +316,18 @@ internal class TokenDetailsStateFactory(
return balanceSelectStateConverter.convert(buttonConfig)
}
fun getStateWithUpdatedYieldSupplyDisplayBalance(displayBalance: String?): TokenDetailsState {
fun getStateWithUpdatedYieldSupplyDisplayBalance(
yieldSupplyRewardBalance: YieldSupplyRewardBalance,
): TokenDetailsState {
val state = currentStateProvider()
val balanceState = state.tokenBalanceBlockState
return state.copy(
tokenBalanceBlockState = when (balanceState) {
is TokenDetailsBalanceBlockState.Content ->
balanceState.copy(displayYeildSupplyCryptoBalance = displayBalance)
balanceState.copy(
displayYieldSupplyFiatBalance = yieldSupplyRewardBalance.fiatBalance,
displayYieldSupplyCryptoBalance = yieldSupplyRewardBalance.cryptoBalance,
)
is TokenDetailsBalanceBlockState.Error -> balanceState
is TokenDetailsBalanceBlockState.Loading -> balanceState
},

View file

@ -122,12 +122,12 @@ private fun FiatBalance(
height = TangemTheme.dimens.size32,
),
)
is TokenDetailsBalanceBlockState.Content -> if (state.displayYeildSupplyCryptoBalance != null &&
is TokenDetailsBalanceBlockState.Content -> if (state.displayYieldSupplyFiatBalance != null &&
!isBalanceHidden
) {
TextAnimatedCounter(
modifier = modifier,
text = state.displayYeildSupplyCryptoBalance,
text = state.displayYieldSupplyFiatBalance,
style = TangemTheme.typography.h2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.primary1,
@ -136,7 +136,7 @@ private fun FiatBalance(
} else {
Text(
modifier = modifier,
text = (state.displayYeildSupplyCryptoBalance ?: state.displayFiatBalance).orMaskWithStars(
text = (state.displayYieldSupplyFiatBalance ?: state.displayFiatBalance).orMaskWithStars(
isBalanceHidden,
),
style = TangemTheme.typography.h2.applyBladeBrush(
@ -184,8 +184,9 @@ private fun CryptoBalance(
tint = TangemTheme.colors.icon.inactive,
contentDescription = null,
)
Text(
text = state.displayCryptoBalance.orMaskWithStars(isBalanceHidden),
TextAnimatedCounter(
text = (state.displayYieldSupplyCryptoBalance ?: state.displayCryptoBalance)
.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.caption2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.tertiary,

View file

@ -41,7 +41,9 @@ internal class TxHistoryItemToTransactionStateConverter(
R.drawable.ic_close_24
} else {
when (type) {
is TransactionType.Approve -> R.drawable.ic_doc_24
is TransactionType.YieldSupply.DeployContract,
is TransactionType.Approve,
-> R.drawable.ic_doc_24
is TransactionType.Staking.Stake,
is TransactionType.Staking.Vote,
is TransactionType.Staking.Restake,
@ -51,11 +53,16 @@ internal class TxHistoryItemToTransactionStateConverter(
is TransactionType.Staking.Unstake,
is TransactionType.Staking.Withdraw,
-> R.drawable.ic_transaction_history_unstaking_24
is TransactionType.YieldSupply.Enter -> R.drawable.ic_connect_24
is TransactionType.YieldSupply.InitializeToken -> R.drawable.ic_gear_24
is TransactionType.YieldSupply.ReactivateToken -> R.drawable.ic_refresh_24
is TransactionType.YieldSupply.Exit -> R.drawable.ic_disconnect_24
is TransactionType.Operation,
is TransactionType.Swap,
is TransactionType.Transfer,
is TransactionType.YieldSupply,
is TransactionType.UnknownOperation,
TransactionType.YieldSupply.Send,
TransactionType.YieldSupply.Topup,
-> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24
}
}
@ -66,32 +73,76 @@ internal class TxHistoryItemToTransactionStateConverter(
is TransactionType.Operation -> stringReference(type.name)
is TransactionType.Swap -> resourceReference(R.string.common_swap)
is TransactionType.Transfer -> resourceReference(R.string.common_transfer)
is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter)
is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit)
is TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup)
is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake)
is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake)
is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote)
is TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards)
is TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw)
is TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake)
is TransactionType.YieldSupply -> when (type) {
is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter)
is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit)
TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup)
TransactionType.YieldSupply.Send -> if (isOutgoing) {
resourceReference(R.string.common_transfer)
} else {
resourceReference(R.string.yield_module_transaction_withdraw)
}
is TransactionType.YieldSupply.DeployContract -> resourceReference(
R.string
.yield_module_transaction_deploy_contract,
)
is TransactionType.YieldSupply.InitializeToken -> resourceReference(
R.string
.yield_module_transaction_initialize,
)
is TransactionType.YieldSupply.ReactivateToken -> resourceReference(
R.string
.yield_module_transaction_reactivate,
)
}
is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation)
}
private fun TxInfo.extractSubtitle(): TextReference {
return when (this.type) {
is TransactionType.YieldSupply.Enter -> {
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(R.string.yield_module_transaction_enter_subtitle, wrappedList(amount))
}
is TransactionType.YieldSupply.Topup,
-> {
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(R.string.yield_module_transaction_topup_subtitle, wrappedList(amount))
}
is TransactionType.YieldSupply.Exit -> {
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(R.string.yield_module_transaction_exit_subtitle, wrappedList(amount))
return when (val type = this.type) {
is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Coin) {
if (type == TransactionType.YieldSupply.Send) {
extractSubtitleByAddressType()
} else {
resourceReference(
R.string.transaction_history_transaction_for_address,
wrappedList(type.address?.toBriefAddressFormat().orEmpty()),
)
}
} else {
when (type) {
is TransactionType.YieldSupply.Enter -> {
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(R.string.yield_module_transaction_enter_subtitle, wrappedList(amount))
}
TransactionType.YieldSupply.Topup -> {
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(R.string.yield_module_transaction_topup_subtitle, wrappedList(amount))
}
is TransactionType.YieldSupply.Exit -> {
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(R.string.yield_module_transaction_exit_subtitle, wrappedList(amount))
}
TransactionType.YieldSupply.Send -> {
if (isOutgoing) {
extractSubtitleByAddressType()
} else {
val amount =
amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(
R.string.yield_module_transaction_exit_subtitle,
wrappedList(amount),
)
}
}
else -> extractSubtitleByAddressType()
}
}
else -> extractSubtitleByAddressType()
}
@ -133,14 +184,21 @@ internal class TxHistoryItemToTransactionStateConverter(
@Suppress("ComplexCondition")
private fun TxInfo.getAmount(): String {
if (type is TransactionType.Staking.Vote ||
type == TransactionType.Staking.ClaimRewards ||
type == TransactionType.Staking.Withdraw ||
type == TransactionType.YieldSupply.Enter ||
type == TransactionType.YieldSupply.Exit ||
type == TransactionType.YieldSupply.Topup
) {
return ""
when (type) {
is TransactionType.Staking.Vote,
TransactionType.Staking.ClaimRewards,
TransactionType.Staking.Withdraw,
-> return ""
is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Token) {
when (type) {
TransactionType.YieldSupply.Send -> if (!isOutgoing) {
return ""
}
else -> return ""
}
}
else -> Unit
}
val prefix = when {
status == TxInfo.TransactionStatus.Failed -> ""

View file

@ -537,7 +537,7 @@ internal class WalletModel @Inject constructor(
}
}
private suspend fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) {
private fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) {
walletScreenContentLoader.cancel(action.prevWalletId)
tokenListStore.remove(action.prevWalletId)
@ -585,7 +585,7 @@ internal class WalletModel @Inject constructor(
}
}
private suspend fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) {
private fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) {
if (accountsFeatureToggles.isFeatureEnabled) {
fetchWalletContent(userWallet = action.selectedWallet)
@ -725,15 +725,15 @@ internal class WalletModel @Inject constructor(
}
}
private suspend fun fetchWalletContent(userWallet: UserWallet) {
private fun fetchWalletContent(userWallet: UserWallet) {
if (userWallet.isLocked) return
/*
* Updating the balance of the current wallet is an essential part of InitializationWallets,
* so the coroutine is launched in the current context
*/
supervisorScope {
launch { walletContentFetcher(userWalletId = userWallet.walletId) }
modelScope.launch {
walletContentFetcher(userWalletId = userWallet.walletId)
}
}

View file

@ -10,11 +10,9 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.features.tangempay.TangemPayFeatureToggles
import kotlinx.coroutines.launch
@ -41,7 +39,6 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
private val onboardingRepository: OnboardingRepository,
private val produceInitialDataTangemPay: ProduceTangemPayInitialDataUseCase,
private val getWalletMetainfoUseCase: GetWalletMetaInfoUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val uiMessageSender: UiMessageSender,
@ -68,6 +65,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
val issuingBottomSheet = bottomSheetMessage {
infoBlock {
icon(com.tangem.core.ui.R.drawable.ic_clock_24) {
type = MessageBottomSheetUMV2.Icon.Type.Informative
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Informative
}
title = resourceReference(R.string.tangempay_issuing_your_card)
@ -93,7 +91,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
body = resourceReference(R.string.tangempay_failed_to_issue_card_support_description)
}
secondaryButton {
text = resourceReference(R.string.common_contact_support)
text = resourceReference(R.string.tangempay_go_to_support)
onClick {
onPaySupportClick()
closeBs()
@ -106,10 +104,8 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
override fun onPaySupportClick() {
modelScope.launch {
val userWalletId = stateHolder.getSelectedWalletId()
val userWallet = getUserWalletUseCase.invoke(userWalletId).getOrNull() ?: return@launch
val cardInfo = getWalletMetainfoUseCase.invoke(
userWallet.requireColdWallet().scanResponse,
userWalletId = stateHolder.getSelectedWalletId(),
).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(

View file

@ -22,6 +22,7 @@ import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplySetShouldShowMainPromoUseCase
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
@ -58,6 +59,8 @@ internal interface WalletContentClickIntents {
apy: String,
)
fun onYieldPromoCloseClick()
fun onAccountExpandClick(account: Account)
fun onAccountCollapseClick(account: Account)
@ -95,6 +98,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val accountDependencies: AccountDependencies,
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase,
) : BaseWalletClickIntents(), WalletContentClickIntents {
override fun onDetailsClick() {
@ -194,6 +198,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
}
}
override fun onYieldPromoCloseClick() {
modelScope.launch {
yieldSupplySetShouldShowMainPromoUseCase(false)
}
}
override fun onAccountExpandClick(account: Account) {
val userWalletId = stateHolder.getSelectedWalletId()
analyticsEventHandler.send(MainScreenAnalyticsEvent.AccountShowTokens())

View file

@ -317,6 +317,11 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
program = Program.BlackFriday,
action = PromotionBannerClicked.BannerAction.Closed(),
)
PromoId.OnePlusOne -> PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.OnePlusOne,
action = PromotionBannerClicked.BannerAction.Closed(),
)
},
)
@ -364,6 +369,16 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
)
urlOpener.openUrl(BLACK_FRIDAY_PROMO_LINK)
}
PromoId.OnePlusOne -> {
analyticsEventHandler.send(
PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.OnePlusOne,
action = PromotionBannerClicked.BannerAction.Clicked(),
),
)
urlOpener.openUrl(ONE_PLUS_ONE_PROMO_LINK)
}
}
}
@ -581,5 +596,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
"&utm_source=tangem-app-banner" +
"&utm_medium=banner" +
"&utm_campaign=BlackFriday2025"
const val ONE_PLUS_ONE_PROMO_LINK = "https://tangem.com/pricing/" +
"?cat=family" +
"&utm_source=tangem-app-banner" +
"&utm_medium=banner" +
"&utm_campaign=BOGO50"
}
}

View file

@ -27,7 +27,7 @@ internal class WalletTangemPayAnalyticsEventSender @Inject constructor(
val event = when {
customerInfo.orderStatus == OrderStatus.CANCELED -> return // ignore cancelled state on analytics
!customerInfo.info.isKycApproved -> return // ignore kyc not approved state on analytics
cardInfo != null && productInstance != null -> TangemPayAnalyticsEvents.MainScreenOpened()
cardInfo != null && productInstance != null -> return
else -> TangemPayAnalyticsEvents.IssuingBannerDisplayed()
}

View file

@ -62,6 +62,10 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
source = AnalyticsParam.ScreensSources.Main,
program = Program.BlackFriday,
)
is WalletNotification.OnePlusOnePromo -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.OnePlusOne,
)
is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo()
is WalletNotification.VisaPresalePromo -> VisaWaitlistPromo()
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]

View file

@ -2,7 +2,6 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import arrow.core.getOrElse
import com.tangem.common.ui.notifications.NotificationId
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.decompose.di.ModelScoped
@ -21,12 +20,9 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.onramp.GetOnrampCountryUseCase
import com.tangem.domain.onramp.OnrampSepaAvailableUseCase
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
@ -37,7 +33,6 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.extensions.addIf
import com.tangem.utils.extensions.isPositive
import com.tangem.utils.extensions.orZero
@ -47,7 +42,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList", "LargeClass")
@ -60,9 +54,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val backupValidator: BackupValidator,
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase,
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
private val onrampSepaAvailableUseCase: OnrampSepaAvailableUseCase,
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
private val notificationsRepository: NotificationsRepository,
private val accountDependencies: AccountDependencies,
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
@ -99,11 +90,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
isReadyToShowRateAppUseCase().distinctUntilChanged(),
isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(),
seedPhraseNotificationUseCase(userWalletId = userWallet.walletId).distinctUntilChanged(),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.VisaPresale)
.distinctUntilChanged(),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa)
.distinctUntilChanged(),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.BlackFriday)
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.OnePlusOne)
.distinctUntilChanged(),
notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key)
.distinctUntilChanged(),
@ -117,11 +104,9 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val isReadyToShowRating = array[1] as Boolean
val isNeedToBackup = array[2] as Boolean
val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus
val shouldShowVisaPromo = array[4] as Boolean
val shouldShowSepaBanner = array[5] as Boolean
val shouldShowBlackFridayPromo = array[6] as Boolean
val shouldShowEnablePushesReminderNotification = array[7] as Boolean
val shouldAccessCodeSkipped = array[8] as Boolean
val shouldShowOnePlusOnePromo = array[4] as Boolean
val shouldShowEnablePushesReminderNotification = array[5] as Boolean
val shouldAccessCodeSkipped = array[6] as Boolean
buildList {
addUsedOutdatedDataNotification(totalFiatBalance)
@ -135,16 +120,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
shouldAccessCodeSkipped = shouldAccessCodeSkipped,
)
addBlackFridayPromoNotification(clickIntents, shouldShowBlackFridayPromo)
addVisaPresalePromoNotification(clickIntents, shouldShowVisaPromo)
addSepaPromoNotification(
userWallet = userWallet,
flattenCurrencies = flattenCurrencies,
clickIntents = clickIntents,
shouldShowSepaPromo = shouldShowSepaBanner,
)
addOnePlusOnePromoNotification(clickIntents, shouldShowOnePlusOnePromo)
addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents)
@ -297,72 +273,19 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
.map(CryptoCurrencyStatus::currency)
}
private fun MutableList<WalletNotification>.addVisaPresalePromoNotification(
private fun MutableList<WalletNotification>.addOnePlusOnePromoNotification(
clickIntents: WalletClickIntents,
shouldShowPromo: Boolean,
) {
addIf(
element = WalletNotification.VisaPresalePromo(
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.VisaPresale) },
onClick = { clickIntents.onPromoClick(promoId = PromoId.VisaPresale) },
element = WalletNotification.OnePlusOnePromo(
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.OnePlusOne) },
onClick = { clickIntents.onPromoClick(promoId = PromoId.OnePlusOne) },
),
condition = shouldShowPromo,
)
}
private fun MutableList<WalletNotification>.addBlackFridayPromoNotification(
clickIntents: WalletClickIntents,
shouldShowPromo: Boolean,
) {
addIf(
element = WalletNotification.BlackFridayPromo(
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.BlackFriday) },
onClick = { clickIntents.onPromoClick(promoId = PromoId.BlackFriday) },
),
condition = shouldShowPromo,
)
}
private suspend fun MutableList<WalletNotification>.addSepaPromoNotification(
userWallet: UserWallet,
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
clickIntents: WalletClickIntents,
shouldShowSepaPromo: Boolean,
) {
val currencies = if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) {
flattenCurrencies.map { statuses -> statuses.map { it.currency } }.getOrNull() ?: run {
Timber.e("Error on getting crypto currency list")
return
}
} else {
getCryptoCurrenciesUseCase(userWalletId = userWallet.walletId).getOrElse {
Timber.e("Error on getting crypto currency list")
return
}
}
val bitcoinCurrency = currencies.find { isBitcoin(it.network.rawId) } ?: return
val country = getOnrampCountryUseCase.invokeSync(userWallet).getOrElse {
Timber.e("Error on getting onramp country")
return
}
val isSepaAvailable = onrampSepaAvailableUseCase(
userWallet = userWallet,
country = country,
cryptoCurrency = bitcoinCurrency,
)
addIf(
element = WalletNotification.Sepa(
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.Sepa) },
onClick = { clickIntents.onPromoClick(promoId = PromoId.Sepa, bitcoinCurrency) },
),
condition = shouldShowSepaPromo && isSepaAvailable,
)
}
// private fun MutableList<WalletNotification>.addYieldSupplyNotifications(
// flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
// ) {

View file

@ -11,6 +11,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
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.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -45,6 +46,7 @@ internal class MultiWalletContentLoader(
private val currenciesRepository: CurrenciesRepository,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory,
@ -63,6 +65,7 @@ internal class MultiWalletContentLoader(
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
).let(::add)
WalletNFTListSubscriber(

View file

@ -11,6 +11,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
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.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -44,6 +45,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
private val currenciesRepository: CurrenciesRepository,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory,
@ -72,6 +74,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
hotWalletFeatureToggles = hotWalletFeatureToggles,
tangemPayFeatureToggles = tangemPayFeatureToggles,
tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
)
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
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.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -35,6 +36,7 @@ internal class SingleWalletWithTokenContentLoader(
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
) : WalletContentLoader(id = userWallet.walletId) {
override fun create(): List<WalletSubscriber> {
@ -49,6 +51,7 @@ internal class SingleWalletWithTokenContentLoader(
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
).let(::add)
MultiWalletWarningsSubscriber(
userWallet = userWallet,

View file

@ -7,6 +7,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
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.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -36,6 +37,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
) {
fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader {
@ -55,6 +57,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
hotWalletFeatureToggles = hotWalletFeatureToggles,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
)
}
}

View file

@ -376,6 +376,23 @@ sealed class WalletNotification(val config: NotificationConfig) {
),
)
data class OnePlusOnePromo(
val onCloseClick: () -> Unit,
val onClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.notification_one_plus_one_title),
subtitle = resourceReference(R.string.notification_one_plus_one_text),
iconResId = R.drawable.img_one_plus_one_promo,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.notification_one_plus_one_button),
onClick = onClick,
),
iconSize = 54.dp,
),
)
data class PushNotifications(
val onCloseClick: () -> Unit,
val onEnabledClick: () -> Unit,

View file

@ -11,14 +11,16 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.converte
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter
import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons
import timber.log.Timber
import java.math.BigDecimal
internal class SetTokenListTransformer(
private val params: TokenConverterParams,
private val userWallet: UserWallet,
private val appCurrency: AppCurrency,
private val clickIntents: WalletClickIntents,
private val yieldSupplyApyMap: Map<String, String> = emptyMap(),
private val yieldSupplyApyMap: Map<String, BigDecimal> = emptyMap(),
private val stakingApyMap: Map<String, List<Yield.Validator>> = emptyMap(),
private val shouldShowMainPromo: Boolean,
) : WalletStateTransformer(userWallet.walletId) {
override fun transform(prevState: WalletState): WalletState {
@ -63,6 +65,7 @@ internal class SetTokenListTransformer(
clickIntents = clickIntents,
yieldModuleApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
shouldShowMainPromo = shouldShowMainPromo,
).convert(value = this)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.txhistory.models.TxHistoryStateError
import com.tangem.domain.models.wallet.UserWallet
@ -17,6 +18,7 @@ internal class SetTxHistoryCountErrorTransformer(
private val error: TxHistoryStateError,
private val pendingTransactions: Set<TxInfo>,
private val clickIntents: WalletClickIntents,
private val currency: CryptoCurrency,
) : WalletStateTransformer(userWallet.walletId) {
private val txHistoryItemConverter by lazy {
@ -26,6 +28,7 @@ internal class SetTxHistoryCountErrorTransformer(
}
TxHistoryItemStateConverter(
currency = currency,
symbol = blockchain.currency,
decimals = blockchain.decimals(),
clickIntents = clickIntents,

View file

@ -28,17 +28,25 @@ import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig
@Suppress("LongParameterList")
internal class TokenListStateConverter(
private val appCurrency: AppCurrency,
private val params: TokenConverterParams,
private val selectedWallet: UserWallet,
private val clickIntents: WalletClickIntents,
private val yieldModuleApyMap: Map<String, String>,
private val yieldModuleApyMap: Map<String, BigDecimal>,
private val stakingApyMap: Map<String, List<Yield.Validator>>,
private val shouldShowMainPromo: Boolean,
) : Converter<WalletTokensListState, WalletTokensListState> {
private val yieldSupplyPromoBannerKeyConverter = YieldSupplyPromoBannerKeyConverter(
yieldModuleApyMap,
shouldShowMainPromo,
)
private val onTokenClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit =
{ accountId, currencyStatus ->
clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus)
@ -63,10 +71,12 @@ internal class TokenListStateConverter(
private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter(
appCurrency = appCurrency,
yieldModuleApyMap = yieldModuleApyMap,
yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKeyConverter.convert(params),
stakingApyMap = stakingApyMap,
onItemClick = { _, status -> onTokenClick(accountId, status) },
onItemLongClick = { _, status -> onTokenLongClick(accountId, status) },
onApyLabelClick = { status, apySource, apy -> onApyLabelClick(status, apySource, apy) },
onYieldPromoCloseClick = clickIntents::onYieldPromoCloseClick,
)
override fun convert(value: WalletTokensListState): WalletTokensListState {

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.network.TxInfo.TransactionStatus
import com.tangem.domain.models.network.TxInfo.TransactionType
@ -20,6 +21,7 @@ import com.tangem.utils.converter.Converter
import com.tangem.utils.toBriefAddressFormat
internal class TxHistoryItemStateConverter(
private val currency: CryptoCurrency,
private val symbol: String,
private val decimals: Int,
private val clickIntents: WalletClickIntents,
@ -49,7 +51,9 @@ internal class TxHistoryItemStateConverter(
R.drawable.ic_close_24
} else {
when (type) {
is TransactionType.Approve -> R.drawable.ic_doc_24
is TransactionType.YieldSupply.DeployContract,
is TransactionType.Approve,
-> R.drawable.ic_doc_24
is TransactionType.Staking.Stake,
is TransactionType.Staking.Vote,
is TransactionType.Staking.Restake,
@ -59,11 +63,16 @@ internal class TxHistoryItemStateConverter(
is TransactionType.Staking.Unstake,
is TransactionType.Staking.Withdraw,
-> R.drawable.ic_transaction_history_unstaking_24
is TransactionType.YieldSupply.Enter -> R.drawable.ic_connect_24
is TransactionType.YieldSupply.InitializeToken -> R.drawable.ic_gear_24
is TransactionType.YieldSupply.ReactivateToken -> R.drawable.ic_refresh_24
is TransactionType.YieldSupply.Exit -> R.drawable.ic_disconnect_24
is TransactionType.Operation,
is TransactionType.Swap,
is TransactionType.Transfer,
is TransactionType.YieldSupply,
is TransactionType.UnknownOperation,
TransactionType.YieldSupply.Send,
TransactionType.YieldSupply.Topup,
-> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24
}
}
@ -74,32 +83,81 @@ internal class TxHistoryItemStateConverter(
is TransactionType.Operation -> stringReference(type.name)
is TransactionType.Swap -> resourceReference(R.string.common_swap)
is TransactionType.Transfer -> resourceReference(R.string.common_transfer)
is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter)
is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit)
is TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup)
is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake)
is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake)
is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote)
is TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards)
is TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw)
is TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake)
is TransactionType.YieldSupply -> when (type) {
is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter)
is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit)
TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup)
TransactionType.YieldSupply.Send -> if (isOutgoing) {
resourceReference(R.string.common_transfer)
} else {
resourceReference(R.string.yield_module_transaction_withdraw)
}
is TransactionType.YieldSupply.DeployContract -> resourceReference(
R.string
.yield_module_transaction_deploy_contract,
)
is TransactionType.YieldSupply.InitializeToken -> resourceReference(
R.string
.yield_module_transaction_initialize,
)
is TransactionType.YieldSupply.ReactivateToken -> resourceReference(
R.string
.yield_module_transaction_reactivate,
)
}
is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation)
}
private fun TxInfo.extractSubtitle(): TextReference {
return when (this.type) {
is TransactionType.YieldSupply.Enter -> {
val amount = amount.format { crypto(symbol = symbol, decimals = decimals) }
resourceReference(R.string.yield_module_transaction_enter_subtitle, wrappedList(amount))
}
is TransactionType.YieldSupply.Topup,
-> {
val amount = amount.format { crypto(symbol = symbol, decimals = decimals) }
resourceReference(R.string.yield_module_transaction_topup_subtitle, wrappedList(amount))
}
is TransactionType.YieldSupply.Exit -> {
val amount = amount.format { crypto(symbol = symbol, decimals = decimals) }
resourceReference(R.string.yield_module_transaction_exit_subtitle, wrappedList(amount))
return when (val type = this.type) {
is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Coin) {
resourceReference(
R.string.transaction_history_transaction_for_address,
wrappedList(type.address?.toBriefAddressFormat().orEmpty()),
)
} else {
when (type) {
is TransactionType.YieldSupply.Enter -> {
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(
R.string.yield_module_transaction_enter_subtitle,
wrappedList(amount),
)
}
TransactionType.YieldSupply.Topup -> {
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(
R.string.yield_module_transaction_topup_subtitle,
wrappedList(amount),
)
}
TransactionType.YieldSupply.Send -> {
if (isOutgoing) {
extractSubtitleByAddressType()
} else {
val amount =
amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(
R.string.yield_module_transaction_exit_subtitle,
wrappedList(amount),
)
}
}
is TransactionType.YieldSupply.Exit -> {
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(
R.string.yield_module_transaction_exit_subtitle,
wrappedList(amount),
)
}
else -> extractSubtitleByAddressType()
}
}
else -> extractSubtitleByAddressType()
}
@ -147,14 +205,18 @@ internal class TxHistoryItemStateConverter(
@Suppress("ComplexCondition")
private fun TxInfo.getAmount(): String {
if (type is TransactionType.Staking.Vote ||
type == TransactionType.Staking.ClaimRewards ||
type == TransactionType.Staking.Withdraw ||
type == TransactionType.YieldSupply.Enter ||
type == TransactionType.YieldSupply.Exit ||
type == TransactionType.YieldSupply.Topup
) {
return ""
when (type) {
is TransactionType.Staking.Vote,
TransactionType.Staking.ClaimRewards,
TransactionType.Staking.Withdraw,
-> return ""
is TransactionType.YieldSupply -> {
if (currency is CryptoCurrency.Token && type == TransactionType.YieldSupply.Send && !isOutgoing) {
return ""
}
}
else -> Unit
}
val prefix = when {
status == TransactionStatus.Failed -> ""

View file

@ -0,0 +1,48 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
internal class YieldSupplyPromoBannerKeyConverter(
private val yieldModuleApyMap: Map<String, BigDecimal>,
private val shouldShowMainPromo: Boolean,
) : Converter<TokenConverterParams, String?> {
override fun convert(value: TokenConverterParams): String? {
if (!shouldShowMainPromo) return null
val currencies = when (value) {
is TokenConverterParams.Wallet -> value.tokenList.flattenCurrencies()
is TokenConverterParams.Account -> value.accountList.flattenCurrencies()
}.filter { status ->
status.value is CryptoCurrencyStatus.Loaded ||
status.value is CryptoCurrencyStatus.Custom
}
val tokens = currencies.filter { it.currency is CryptoCurrency.Token }
if (tokens.any { it.value.yieldSupplyStatus?.isActive == true }) return null
if (yieldModuleApyMap.isEmpty()) return null
val max = tokens.asSequence()
.mapNotNull { status ->
val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null
val tokenKey = token.yieldSupplyKey()
val shouldIgnoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId)
val matchedKey = yieldModuleApyMap.keys.firstOrNull { mapKey ->
mapKey.equals(tokenKey, shouldIgnoreCase)
} ?: return@mapNotNull null
status to matchedKey
}
.maxByOrNull { (status, _) -> status.value.amount ?: BigDecimal.ZERO }
return max?.second
}
}

View file

@ -5,16 +5,18 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.utils.coroutines.combine6
import com.tangem.utils.coroutines.combine7
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import java.math.BigDecimal
/**
* Subscriber that monitors account list related data and updates the wallet state accordingly.
@ -30,19 +32,21 @@ internal class AccountListSubscriber @AssistedInject constructor(
override val clickIntents: WalletClickIntents,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
) : BasicAccountListSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<*> = combine6(
override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7(
flow1 = getAccountStatusListFlow(),
flow2 = getAppCurrencyFlow(),
flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet),
flow4 = accountDependencies.isAccountsModeEnabledUseCase(),
flow5 = yieldSupplyApyFlow(),
flow6 = stakingApyFlow(),
flow7 = yieldSupplyGetShouldShowMainPromoFlow(),
transform = ::updateState,
)
private fun yieldSupplyApyFlow(): Flow<Map<String, String>> {
private fun yieldSupplyApyFlow(): Flow<Map<String, BigDecimal>> {
return yieldSupplyApyFlowUseCase().distinctUntilChanged()
}
@ -50,6 +54,10 @@ internal class AccountListSubscriber @AssistedInject constructor(
return stakingApyFlowUseCase().distinctUntilChanged()
}
private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow<Boolean> {
return yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged()
}
@AssistedFactory
interface Factory {
fun create(userWallet: UserWallet): AccountListSubscriber

View file

@ -20,6 +20,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenCon
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import timber.log.Timber
import java.math.BigDecimal
/**
* Basic implementation of [WalletSubscriber] for wallet with accounts.
@ -46,8 +47,9 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
appCurrency: AppCurrency,
expandedAccounts: Set<AccountId>,
isAccountMode: Boolean,
yieldSupplyApyMap: Map<String, String> = emptyMap(),
yieldSupplyApyMap: Map<String, BigDecimal> = emptyMap(),
stakingApyMap: Map<String, List<Yield.Validator>> = emptyMap(),
shouldShowMainPromo: Boolean = false,
) {
val mainAccount = accountList.mainAccount
@ -66,11 +68,18 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
portfolioId = PortfolioId(mainAccount.accountId),
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
shouldShowMainPromo = shouldShowMainPromo,
)
}
isAccountMode -> {
val convertParams = TokenConverterParams.Account(accountList, expandedAccounts)
updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap)
updateContent(
params = convertParams,
appCurrency = appCurrency,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
shouldShowMainPromo = shouldShowMainPromo,
)
}
}
}
@ -79,8 +88,9 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
maybeTokenList: Lce<TokenListError, TokenList>,
appCurrency: AppCurrency,
portfolioId: PortfolioId,
yieldSupplyApyMap: Map<String, String> = emptyMap(),
yieldSupplyApyMap: Map<String, BigDecimal> = emptyMap(),
stakingApyMap: Map<String, List<Yield.Validator>> = emptyMap(),
shouldShowMainPromo: Boolean,
) {
val tokenList = maybeTokenList.getOrElse(
ifLoading = { maybeContent ->
@ -110,14 +120,16 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
appCurrency = appCurrency,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
shouldShowMainPromo = shouldShowMainPromo,
)
}
private fun updateContent(
params: TokenConverterParams,
appCurrency: AppCurrency,
yieldSupplyApyMap: Map<String, String> = emptyMap(),
yieldSupplyApyMap: Map<String, BigDecimal> = emptyMap(),
stakingApyMap: Map<String, List<Yield.Validator>> = emptyMap(),
shouldShowMainPromo: Boolean,
) {
stateController.update(
SetTokenListTransformer(
@ -127,6 +139,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
clickIntents = clickIntents,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
shouldShowMainPromo = shouldShowMainPromo,
),
)
}

View file

@ -15,6 +15,7 @@ import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
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.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
@ -28,6 +29,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
@Deprecated("Use AccountListSubscriber instead")
@Suppress("LongParameterList")
@ -40,6 +42,7 @@ internal abstract class BasicTokenListSubscriber(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
) : WalletSubscriber() {
private val sendAnalyticsJobHolder = JobHolder()
@ -69,7 +72,8 @@ internal abstract class BasicTokenListSubscriber(
flow2 = appCurrencyFlow(),
flow3 = yieldSupplyApyFlow(),
flow4 = stakingApyFlow(),
transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap ->
flow5 = yieldSupplyGetShouldShowMainPromoFlow(),
transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap, shouldShowMainPromo ->
val tokenList = maybeTokenList.getOrElse(
ifLoading = { maybeContent ->
val isRefreshing = stateHolder.getWalletState(userWallet.walletId)
@ -98,6 +102,7 @@ internal abstract class BasicTokenListSubscriber(
appCurrency = appCurrency,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
shouldShowMainPromo = shouldShowMainPromo,
)
walletWithFundsChecker.check(tokenList)
@ -122,8 +127,9 @@ internal abstract class BasicTokenListSubscriber(
private fun updateContent(
params: TokenConverterParams,
appCurrency: AppCurrency,
yieldSupplyApyMap: Map<String, String>,
yieldSupplyApyMap: Map<String, BigDecimal>,
stakingApyMap: Map<String, List<Yield.Validator>>,
shouldShowMainPromo: Boolean,
) {
stateHolder.update(
SetTokenListTransformer(
@ -133,6 +139,7 @@ internal abstract class BasicTokenListSubscriber(
clickIntents = clickIntents,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
shouldShowMainPromo = shouldShowMainPromo,
),
)
}
@ -146,9 +153,12 @@ internal abstract class BasicTokenListSubscriber(
}
.distinctUntilChanged()
private fun yieldSupplyApyFlow(): Flow<Map<String, String>> = yieldSupplyApyFlowUseCase()
private fun yieldSupplyApyFlow(): Flow<Map<String, BigDecimal>> = yieldSupplyApyFlowUseCase()
.distinctUntilChanged()
private fun stakingApyFlow(): Flow<Map<String, List<Yield.Validator>>> = stakingApyFlowUseCase()
.distinctUntilChanged()
private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow<Boolean> = yieldSupplyGetShouldShowMainPromoUseCase()
.distinctUntilChanged()
}

View file

@ -12,6 +12,7 @@ import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
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.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
@ -32,6 +33,7 @@ internal class MultiWalletTokenListSubscriber(
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
stakingApyFlowUseCase: StakingApyFlowUseCase,
yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
) : BasicTokenListSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
@ -41,6 +43,7 @@ internal class MultiWalletTokenListSubscriber(
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
) {
override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList> {

View file

@ -8,6 +8,7 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
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.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
@ -27,6 +28,7 @@ internal class SingleWalletWithTokenListSubscriber(
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
stakingApyFlowUseCase: StakingApyFlowUseCase,
yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
) : BasicTokenListSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
@ -36,6 +38,7 @@ internal class SingleWalletWithTokenListSubscriber(
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
) {
override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList> {

View file

@ -5,6 +5,7 @@ import androidx.paging.cachedIn
import androidx.paging.map
import arrow.core.Either
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.wallet.UserWallet
@ -58,7 +59,7 @@ internal class TxHistorySubscriber(
refresh = isRefresh,
).map { it.cachedIn(coroutineScope) }
setLoadedTxHistoryState(maybeTxHistoryItems)
setLoadedTxHistoryState(maybeTxHistoryItems, status.currency)
}
}
}
@ -67,18 +68,19 @@ internal class TxHistorySubscriber(
private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) {
stateHolder.update(
maybeTxHistoryItemCount.fold(
ifLeft = {
ifLeft = { error ->
SetTxHistoryCountErrorTransformer(
userWallet = userWallet,
error = it,
error = error,
pendingTransactions = status.value.pendingTransactions,
currency = status.currency,
clickIntents = clickIntents,
)
},
ifRight = {
ifRight = { txCount ->
SetTxHistoryCountTransformer(
userWalletId = userWallet.walletId,
transactionsCount = it,
transactionsCount = txCount,
clickIntents = clickIntents,
)
},
@ -86,7 +88,7 @@ internal class TxHistorySubscriber(
)
}
private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems) {
private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems, currency: CryptoCurrency) {
stateHolder.update(
maybeTxHistoryItems.fold(
ifLeft = {
@ -102,6 +104,7 @@ internal class TxHistorySubscriber(
symbol = blockchain.currency,
decimals = blockchain.decimals(),
clickIntents = clickIntents,
currency = currency,
)
SetTxHistoryItemsTransformer(

View file

@ -5,6 +5,7 @@ import androidx.paging.cachedIn
import androidx.paging.map
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.wallet.UserWallet
@ -51,7 +52,7 @@ internal class TxHistorySubscriberV2(
refresh = isRefresh,
).map { it.cachedIn(coroutineScope) }
setLoadedTxHistoryState(maybeTxHistoryItems)
setLoadedTxHistoryState(maybeTxHistoryItems, currency = status.currency)
}
}
}
@ -60,18 +61,19 @@ internal class TxHistorySubscriberV2(
private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) {
stateController.update(
maybeTxHistoryItemCount.fold(
ifLeft = {
ifLeft = { error ->
SetTxHistoryCountErrorTransformer(
userWallet = userWallet,
error = it,
error = error,
pendingTransactions = status.value.pendingTransactions,
clickIntents = clickIntents,
currency = status.currency,
)
},
ifRight = {
ifRight = { txCount ->
SetTxHistoryCountTransformer(
userWalletId = userWallet.walletId,
transactionsCount = it,
transactionsCount = txCount,
clickIntents = clickIntents,
)
},
@ -79,7 +81,7 @@ internal class TxHistorySubscriberV2(
)
}
private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems) {
private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems, currency: CryptoCurrency) {
stateController.update(
maybeTxHistoryItems.fold(
ifLeft = {
@ -92,6 +94,7 @@ internal class TxHistorySubscriberV2(
ifRight = { itemsFlow ->
val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
val itemConverter = TxHistoryItemStateConverter(
currency = currency,
symbol = blockchain.currency,
decimals = blockchain.decimals(),
clickIntents = clickIntents,

View file

@ -0,0 +1,208 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.yieldSupplyKey
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.UserWalletId
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
import org.junit.Test
import java.math.BigDecimal
class YieldSupplyPromoBannerKeyConverterTest {
@Test
fun `GIVEN promo disabled WHEN convert THEN return null`() {
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xABCDEF")
val status = createStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false)
val tokenList = ungroupedTokenList(status)
val params = TokenConverterParams.Wallet(
portfolioId = PortfolioId.Wallet(UserWalletId("00")),
tokenList = tokenList,
)
val converter = YieldSupplyPromoBannerKeyConverter(
yieldModuleApyMap = mapOf(token.yieldSupplyKey() to BigDecimal("0.10")),
shouldShowMainPromo = false,
)
val result = converter.convert(params)
assertThat(result).isNull()
}
@Test
fun `GIVEN empty apy map WHEN convert THEN return null`() {
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xA1")
val status = createStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false)
val params = TokenConverterParams.Wallet(
portfolioId = PortfolioId.Wallet(UserWalletId("00")),
tokenList = ungroupedTokenList(status),
)
val converter = YieldSupplyPromoBannerKeyConverter(
yieldModuleApyMap = emptyMap(),
shouldShowMainPromo = true,
)
val result = converter.convert(params)
assertThat(result).isNull()
}
@Test
fun `GIVEN active yield token present WHEN convert THEN return null`() {
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xAA")
val statusActive = createStatus(token = token, amount = BigDecimal("5"), isYieldActive = true)
val params = TokenConverterParams.Wallet(
portfolioId = PortfolioId.Wallet(UserWalletId("00")),
tokenList = ungroupedTokenList(statusActive),
)
val converter = YieldSupplyPromoBannerKeyConverter(
yieldModuleApyMap = mapOf(token.yieldSupplyKey() to BigDecimal("0.12")),
shouldShowMainPromo = true,
)
val result = converter.convert(params)
assertThat(result).isNull()
}
@Test
fun `GIVEN multiple candidates EVM case insensitive WHEN convert THEN return key of max amount`() {
val evmNetworkId = "ETH"
val tokenSmall = createToken(networkId = evmNetworkId, backendId = evmNetworkId, contract = "0xAbCd")
val tokenBig = createToken(networkId = evmNetworkId, backendId = evmNetworkId, contract = "0xBEEF")
val statusSmall = createStatus(token = tokenSmall, amount = BigDecimal("1.00"), isYieldActive = false)
val statusBig = createStatus(token = tokenBig, amount = BigDecimal("10.00"), isYieldActive = false)
val apyMap = mapOf(
"${tokenSmall.network.backendId}_${tokenSmall.contractAddress.lowercase()}" to BigDecimal("0.05"),
"${tokenBig.network.backendId}_${tokenBig.contractAddress.uppercase()}" to BigDecimal("0.15"),
)
val params = TokenConverterParams.Wallet(
portfolioId = PortfolioId.Wallet(UserWalletId("00")),
tokenList = ungroupedTokenList(statusSmall, statusBig),
)
val converter = YieldSupplyPromoBannerKeyConverter(
yieldModuleApyMap = apyMap,
shouldShowMainPromo = true,
)
val result = converter.convert(params)
val expectedKey = "${tokenBig.network.backendId}_${tokenBig.contractAddress.uppercase()}"
assertThat(result).isEqualTo(expectedKey)
}
@Test
fun `GIVEN non evm case sensitive mismatch WHEN convert THEN return null`() {
val nonEvmId = "xrp"
val token = createToken(networkId = nonEvmId, backendId = nonEvmId, contract = "rAbC123")
val status = createStatus(token = token, amount = BigDecimal("3"), isYieldActive = false)
val mismatchedKey = "${token.network.backendId}_${token.contractAddress.lowercase()}"
val apyMap = mapOf(mismatchedKey to BigDecimal("0.07"))
val params = TokenConverterParams.Wallet(
portfolioId = PortfolioId.Wallet(UserWalletId("00")),
tokenList = ungroupedTokenList(status),
)
val converter = YieldSupplyPromoBannerKeyConverter(
yieldModuleApyMap = apyMap,
shouldShowMainPromo = true,
)
val result = converter.convert(params)
assertThat(result).isNull()
}
private fun ungroupedTokenList(vararg statuses: CryptoCurrencyStatus): TokenList.Ungrouped {
return TokenList.Ungrouped(
totalFiatBalance = com.tangem.domain.models.TotalFiatBalance.Loaded(
amount = BigDecimal.ZERO,
source = StatusSource.ACTUAL,
),
sortedBy = com.tangem.domain.models.TokensSortType.NONE,
currencies = statuses.toList(),
)
}
private fun createStatus(
token: CryptoCurrency.Token,
amount: BigDecimal,
isYieldActive: Boolean,
): CryptoCurrencyStatus {
val networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "addr",
type = NetworkAddress.Address.Type.Primary,
),
)
val value = CryptoCurrencyStatus.Custom(
amount = amount,
fiatAmount = null,
fiatRate = null,
priceChange = null,
stakingBalance = null,
yieldSupplyStatus = if (isYieldActive) {
YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = null,
)
} else {
null
},
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
)
return CryptoCurrencyStatus(
currency = token,
value = value,
)
}
private fun createToken(networkId: String, backendId: String, contract: String): CryptoCurrency.Token {
val network = Network(
id = Network.ID(value = networkId, derivationPath = Network.DerivationPath.None),
backendId = backendId,
name = backendId,
currencySymbol = "SYM",
derivationPath = Network.DerivationPath.None,
isTestnet = false,
standardType = when (backendId) {
"ethereum" -> Network.StandardType.ERC20
else -> Network.StandardType.Unspecified("UNSPEC")
},
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(networkId),
suffix = CryptoCurrency.ID.Suffix.ContractAddress(contract),
),
network = network,
name = "Token",
symbol = "TKN",
decimals = 18,
iconUrl = null,
isCustom = false,
contractAddress = contract,
)
}
}

View file

@ -20,4 +20,8 @@ internal interface WcCommonTransactionModel {
fun showSuccessSignMessage(message: TextReference = resourceReference(R.string.wc_successfully_signed)) {
messageSender.send(ToastMessage(message = message))
}
fun showSuccessAddedMessage(message: TextReference = resourceReference(R.string.common_added)) {
messageSender.send(ToastMessage(message = message))
}
}

View file

@ -111,7 +111,7 @@ internal class WcAddNetworkModel @Inject constructor(
modelScope.launch {
_uiState.update { it?.copy(transaction = it.transaction.copy(isLoading = true)) }
useCase.approve().getOrNull()?.let {
showSuccessSignMessage()
showSuccessAddedMessage()
router.pop()
} ?: run {
_uiState.update { it?.copy(transaction = it.transaction.copy(isLoading = false)) }

View file

@ -58,6 +58,7 @@ internal class YieldSupplyActiveModel @Inject constructor(
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val urlOpener: UrlOpener,
private val appRouter: AppRouter,
private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase,
) : Model(), YieldSupplyStopEarningComponent.ModelCallback,
YieldSupplyApproveComponent.ModelCallback {
@ -241,11 +242,13 @@ internal class YieldSupplyActiveModel @Inject constructor(
userWalletId,
cryptoCurrencyStatusFlow.value,
).onRight { minAmount ->
val dustAmount = yieldSupplyGetDustMinAmountUseCase(minAmount = minAmount, appCurrency = appCurrency)
uiState.update(
YieldSupplyActiveMinAmountTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value,
appCurrency = appCurrency,
minAmount = minAmount,
dustMinAmount = dustAmount,
analyticsHandler = analyticsHandler,
onApprove = ::onApprove,
),

View file

@ -28,11 +28,12 @@ import java.math.BigDecimal
* - Builds the fee policy note text using the minimum amount.
* - Adds contextual notifications:
* - Approval required notification when spending is not yet allowed (emits analytics on CTA).
* - "Not all amount supplied" info when wallet balance exceeds the supplied balance by more than [minAmount].
* - "Not all amount supplied" info when the not-supplied balance exceeds the dust threshold [dustMinAmount].
*
* @property cryptoCurrencyStatus Current currency status used to calculate values and flags.
* @property appCurrency Preferred fiat currency for formatting.
* @property minAmount Protocol-required minimal amount to deposit/supply (in crypto units).
* @property dustMinAmount Threshold used to detect dust/not-supplied balance (in crypto units).
* @property analyticsHandler Analytics reporter for user actions.
* @property onApprove Action invoked when the "Approve" notification button is tapped.
*/
@ -40,6 +41,7 @@ internal class YieldSupplyActiveMinAmountTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
private val minAmount: BigDecimal,
private val dustMinAmount: BigDecimal,
private val analyticsHandler: AnalyticsEventHandler,
private val onApprove: () -> Unit,
) : Transformer<YieldSupplyActiveContentUM> {
@ -89,7 +91,7 @@ internal class YieldSupplyActiveMinAmountTransformer(
}
private fun getNotSuppliedNotification(cryptoCurrencyStatus: CryptoCurrencyStatus): NotificationUM? {
return if (cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(minAmount)) {
return if (cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustMinAmount)) {
val cryptoCurrency = cryptoCurrencyStatus.currency
val notDepositedAmount = cryptoCurrencyStatus.notSuppliedAmountOrNull()
val formattedAmount =

View file

@ -1,5 +1,6 @@
package com.tangem.features.yield.supply.impl.main.model
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import android.os.SystemClock
import com.tangem.common.routing.AppRoute.YieldSupplyPromo
@ -12,6 +13,8 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -51,6 +54,7 @@ internal class YieldSupplyModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val appRouter: AppRouter,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
@ -61,6 +65,7 @@ internal class YieldSupplyModel @Inject constructor(
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
private val yieldSupplyRepository: YieldSupplyRepository,
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase,
) : Model(), YieldSupplyClickIntents {
private val params = paramsContainer.require<YieldSupplyComponent.Params>()
@ -69,6 +74,7 @@ internal class YieldSupplyModel @Inject constructor(
field = MutableStateFlow<YieldSupplyUM>(YieldSupplyUM.Initial)
private val cryptoCurrency = params.cryptoCurrency
private var appCurrency: AppCurrency = AppCurrency.Default
var userWallet: UserWallet by Delegates.notNull()
private val fetchCurrencyJobHolder = JobHolder()
@ -81,10 +87,17 @@ internal class YieldSupplyModel @Inject constructor(
}
private fun checkIfYieldSupplyIsAvailable() {
modelScope.launch(dispatchers.io) {
modelScope.launch(dispatchers.default) {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
val isAvailable = yieldSupplyIsAvailableUseCase(params.userWalletId, params.cryptoCurrency)
if (isAvailable) {
subscribeOnCurrencyStatusUpdates()
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params(
userWalletId = params.userWalletId,
network = cryptoCurrency.network,
),
)
}
}
}
@ -334,7 +347,11 @@ internal class YieldSupplyModel @Inject constructor(
val minAmount = yieldSupplyMinAmountUseCase(userWalletId = userWallet.walletId, cryptoCurrencyStatus)
.getOrNull()
if (minAmount != null) {
cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(minAmount)
val dustAmount = yieldSupplyGetDustMinAmountUseCase(
minAmount = minAmount,
appCurrency = appCurrency,
)
cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustAmount)
} else {
false
}