Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-12 21:01:59 +03:00
parent 9aa840f892
commit b654ab08a7
15 changed files with 204 additions and 36 deletions

View file

@ -1,9 +1,11 @@
package com.tangem.tap.di.domain
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -83,6 +85,9 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
swapRepository: SwapRepository,
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyWarningsUseCase {
return GetCurrencyWarningsUseCase(
@ -90,6 +95,9 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
swapRepository = swapRepository,
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
dispatchers = dispatchers,
)
}

View file

@ -43,12 +43,13 @@ class DefaultSwapPromoRepository(
private fun checkPromoPeriod(): Boolean {
val currentDate = Calendar.getInstance()
return currentDate.get(Calendar.DATE) in START_DATE_KEY..END_DATE_KEY &&
currentDate.get(Calendar.MONTH) == MONTH_KEY
currentDate.get(Calendar.MONTH) == MONTH_KEY && currentDate.get(Calendar.YEAR) == YEAR_KEY
}
companion object {
private const val START_DATE_KEY = 15
private const val END_DATE_KEY = 31
private const val MONTH_KEY = 11 // December
private const val YEAR_KEY = 2023 // just in case
}
}

View file

@ -18,6 +18,9 @@ dependencies {
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.settings)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
/** Project - Other */
implementation(projects.core.utils)

View file

@ -31,4 +31,6 @@ sealed class CryptoCurrencyWarning {
data class Rent(val rent: BigDecimal, val exemptionAmount: BigDecimal) : CryptoCurrencyWarning()
data class HasPendingTransactions(val blockchainSymbol: String) : CryptoCurrencyWarning()
object SwapPromo : CryptoCurrencyWarning()
}

View file

@ -1,24 +1,33 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import java.math.BigDecimal
@Suppress("LongParameterList")
class GetCurrencyWarningsUseCase(
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val swapRepository: SwapRepository,
private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -29,20 +38,32 @@ class GetCurrencyWarningsUseCase(
isSingleWalletWithTokens: Boolean,
): Flow<Set<CryptoCurrencyWarning>> {
val currency = currencyStatus.currency
val operations = CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
userWalletId = userWalletId,
)
return combine(
getCoinRelatedWarnings(
userWalletId = userWalletId,
operations = operations,
networkId = currency.network.id,
currencyId = currency.id,
derivationPath = derivationPath,
isSingleWalletWithTokens = isSingleWalletWithTokens,
),
operations.getCurrenciesStatusesFlow().conflate(),
flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)),
flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)),
flowOf(getNetworkUnavailableWarning(currencyStatus)),
flowOf(getNetworkNoAccountWarning(currencyStatus)),
) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeNetworkUnavailable, maybeNetworkNoAccount ->
showSwapPromoTokenUseCase().conflate(),
) { coinRelatedWarnings, cryptoStatuses, maybeRentWarning, maybeEdWarning, shouldShowSwapPromo ->
setOfNotNull(
getSwapPromoNotificationWarning(
shouldShowSwapPromo = shouldShowSwapPromo,
userWalletId = userWalletId,
currencyStatus = currencyStatus,
cryptoStatuses = cryptoStatuses,
),
maybeRentWarning,
maybeEdWarning?.let {
CryptoCurrencyWarning.ExistentialDeposit(
@ -51,27 +72,69 @@ class GetCurrencyWarningsUseCase(
)
},
*coinRelatedWarnings.toTypedArray(),
maybeNetworkUnavailable,
maybeNetworkNoAccount,
getNetworkUnavailableWarning(currencyStatus),
getNetworkNoAccountWarning(currencyStatus),
)
}.flowOn(dispatchers.io)
}
private suspend fun getSwapPromoNotificationWarning(
shouldShowSwapPromo: Boolean,
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
cryptoStatuses: Either<CurrenciesStatusesOperations.Error, List<CryptoCurrencyStatus>>,
): CryptoCurrencyWarning? {
val currency = currencyStatus.currency
return if (shouldShowSwapPromo && marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)) {
cryptoStatuses.fold(
ifLeft = { null },
ifRight = { cryptoCurrencyStatuses ->
val pairs = swapRepository.getPairs(
LeastTokenInfo(
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0",
network = currency.network.backendId,
),
cryptoCurrencyStatuses.map { it.currency },
)
val filteredCurrencies = cryptoCurrencyStatuses.filterNot {
it.currency.network.backendId == currency.network.backendId
}
val currencyPairs = pairs.pairs.filter {
it.from.network == currency.network.backendId ||
it.to.network == currency.network.backendId
}
val isExchangeable = currencyPairs.any { pair ->
val availablePair = if (currencyStatus.value.amount.isZero()) {
filteredCurrencies.filterNot { it.value.amount.isZero() }
} else {
filteredCurrencies
}
availablePair
.any {
it.currency.network.backendId == pair.to.network ||
it.currency.network.backendId == pair.from.network
}
}
if (isExchangeable) {
CryptoCurrencyWarning.SwapPromo
} else {
null
}
},
)
} else {
null
}
}
@Suppress("LongParameterList")
private suspend fun getCoinRelatedWarnings(
userWalletId: UserWalletId,
operations: CurrenciesStatusesOperations,
networkId: Network.ID,
currencyId: CryptoCurrency.ID,
derivationPath: Network.DerivationPath,
isSingleWalletWithTokens: Boolean,
): Flow<List<CryptoCurrencyWarning>> {
val operations = CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
userWalletId = userWalletId,
)
val currencyFlow = if (isSingleWalletWithTokens) {
operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId)
} else {

View file

@ -39,6 +39,23 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
),
)
data class SwapPromo(
val onSwapClick: () -> Unit,
val onCloseClick: () -> Unit,
) : TokenDetailsNotification(
config = NotificationConfig(
title = resourceReference(id = R.string.token_swap_promotion_title),
subtitle = resourceReference(id = R.string.token_swap_promotion_message),
iconResId = R.drawable.ic_swap_promo_34,
backgroundResId = R.drawable.img_swap_promo_banner_background,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(id = com.tangem.core.ui.R.string.token_swap_promotion_button),
onClick = onSwapClick,
),
),
)
object NetworksUnreachable : Warning(
title = resourceReference(R.string.warning_network_unreachable_title),
subtitle = resourceReference(R.string.warning_network_unreachable_message),

View file

@ -45,6 +45,10 @@ internal class TokenDetailsNotificationConverter(
is CryptoCurrencyWarning.HasPendingTransactions -> TokenDetailsNotification.HasPendingTransactions(
coinSymbol = warning.blockchainSymbol,
)
is CryptoCurrencyWarning.SwapPromo -> TokenDetailsNotification.SwapPromo(
onSwapClick = clickIntents::onSwapClick,
onCloseClick = clickIntents::onSwapPromoDismiss,
)
}
}
}

View file

@ -28,6 +28,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationWithBackground
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.components.transactions.txHistoryItems
import com.tangem.core.ui.event.EventEffect
@ -104,14 +105,21 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
key = { it::class.java },
contentType = { it.config::class.java },
itemContent = {
Notification(
modifier = itemModifier.animateItemPlacement(),
config = it.config,
iconTint = when (it) {
is TokenDetailsNotification.Warning -> null
is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent
},
)
if (it is TokenDetailsNotification.SwapPromo) {
NotificationWithBackground(
config = it.config,
modifier = itemModifier.animateItemPlacement(),
)
} else {
Notification(
modifier = itemModifier.animateItemPlacement(),
config = it.config,
iconTint = when (it) {
is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent
else -> null
},
)
}
},
)
if (state.isMarketPriceAvailable) {

View file

@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.domain.tokens.model.CryptoCurrency
@Suppress("TooManyFunctions")
interface TokenDetailsClickIntents {
fun onBackClick()
@ -42,4 +43,6 @@ interface TokenDetailsClickIntents {
fun onSwapTransactionClick(txId: String)
fun onGoToProviderClick(url: String)
fun onSwapPromoDismiss()
}

View file

@ -20,6 +20,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
@ -35,8 +36,8 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
@ -74,6 +75,7 @@ internal class TokenDetailsViewModel @Inject constructor(
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
private val swapRepository: SwapRepository,
private val swapTransactionRepository: SwapTransactionRepository,
private val swapTransactionStatusStore: SwapTransactionStatusStore,
@ -547,6 +549,12 @@ internal class TokenDetailsViewModel @Inject constructor(
router.openUrl(url)
}
override fun onSwapPromoDismiss() {
viewModelScope.launch(dispatchers.main) {
shouldShowSwapPromoTokenUseCase.neverToShow()
}
}
private companion object {
const val EXCHANGE_STATUS_UPDATE_DELAY = 10_000L
}

View file

@ -166,4 +166,16 @@ sealed class WalletNotification(val config: NotificationConfig) {
onCloseClick = onCloseClick,
),
)
data class SwapPromo(
val onCloseClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(id = R.string.main_swap_promotion_title),
subtitle = resourceReference(id = R.string.main_swap_promotion_message),
iconResId = R.drawable.ic_swap_promo_34,
backgroundResId = R.drawable.img_swap_promo_banner_background,
onCloseClick = onCloseClick,
),
)
}

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationWithBackground
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
import kotlinx.collections.immutable.ImmutableList
@ -24,17 +25,24 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
key = { it::class.java },
contentType = { it::class.java },
itemContent = {
Notification(
config = it.config,
modifier = modifier.animateItemPlacement(),
iconTint = when (it) {
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.Warning -> null
},
)
if (it is WalletNotification.SwapPromo) {
NotificationWithBackground(
config = it.config,
modifier = modifier.animateItemPlacement(),
)
} else {
Notification(
config = it.config,
modifier = modifier.animateItemPlacement(),
iconTint = when (it) {
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
else -> null
},
)
}
},
)
}

View file

@ -72,4 +72,6 @@ internal interface WalletClickIntents {
fun onExploreClick()
fun onTransactionClick(txHash: String)
fun onCloseSwapPromoNotificationClick()
}

View file

@ -5,6 +5,7 @@ import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
import com.tangem.domain.tokens.GetMissedAddressesCryptoCurrenciesUseCase
import com.tangem.domain.tokens.error.GetCurrenciesError
import com.tangem.domain.tokens.model.CryptoCurrency
@ -26,16 +27,21 @@ import kotlinx.coroutines.flow.flowOf
* @property isDemoCardUseCase use case that checks if card is demo
* @property isReadyToShowRateAppUseCase use case that checks if card is user already rate app
* @property isNeedToBackupUseCase use case that checks if wallet need backup cards
* @property getMissedAddressCryptoCurrenciesUseCase use case that gets missed address crypto currencies
* @property hasSingleWalletSignedHashesUseCase use case that checks if single wallet signed hashes
* @property shouldShowSwapPromoWalletUseCase use case that checks if should show swap promo
* @property clickIntents screen click intents
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
internal class WalletNotificationsListFactory(
private val isDemoCardUseCase: IsDemoCardUseCase,
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
private val getMissedAddressCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase,
private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase,
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
private val clickIntents: WalletClickIntents,
) {
@ -51,9 +57,12 @@ internal class WalletNotificationsListFactory(
flow2 = isReadyToShowRateAppUseCase().conflate(),
flow3 = isNeedToBackupUseCase(selectedWallet.walletId).conflate(),
flow4 = getMissedAddressCryptoCurrenciesUseCase(selectedWallet.walletId).conflate(),
) { hasSignedHashes, isReadyToShowRating, isNeedToBackup, maybeMissedAddressCurrencies ->
flow5 = shouldShowSwapPromoWalletUseCase().conflate(),
) { hasSignedHashes, isReadyToShowRating, isNeedToBackup, maybeMissedAddressCurrencies, isShowSwapPromo ->
readyForRateAppNotification = true
buildList {
addSwapPromoNotification(isShowSwapPromo, cardTypesResolver)
addCriticalNotifications(cardTypesResolver)
addInformationalNotifications(cardTypesResolver, maybeMissedAddressCurrencies)
@ -77,6 +86,18 @@ internal class WalletNotificationsListFactory(
}
}
private fun MutableList<WalletNotification>.addSwapPromoNotification(
showSwapPromo: Boolean,
cardTypesResolver: CardTypesResolver,
) {
addIf(
element = WalletNotification.SwapPromo(
clickIntents::onCloseSwapPromoNotificationClick,
),
condition = showSwapPromo && cardTypesResolver.isMultiwalletAllowed(),
)
}
private fun MutableList<WalletNotification>.addCriticalNotifications(cardTypesResolver: CardTypesResolver) {
addIf(
element = WalletNotification.Critical.DevCard,

View file

@ -128,6 +128,7 @@ internal class WalletViewModel @Inject constructor(
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val scanCardToUnlockWalletUseCase: ScanCardToUnlockWalletClickHandler,
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
isNeedToBackupUseCase: IsNeedToBackupUseCase,
getMissedAddressesCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase,
@ -146,6 +147,7 @@ internal class WalletViewModel @Inject constructor(
isNeedToBackupUseCase = isNeedToBackupUseCase,
getMissedAddressCryptoCurrenciesUseCase = getMissedAddressesCryptoCurrenciesUseCase,
hasSingleWalletSignedHashesUseCase = hasSingleWalletSignedHashesUseCase,
shouldShowSwapPromoWalletUseCase = shouldShowSwapPromoWalletUseCase,
clickIntents = this,
)
@ -774,6 +776,12 @@ internal class WalletViewModel @Inject constructor(
refreshSingleCurrencyContent(selectedWalletIndex)
}
override fun onCloseSwapPromoNotificationClick() {
viewModelScope.launch(dispatchers.main) {
shouldShowSwapPromoWalletUseCase.neverToShow()
}
}
// FIXME: refreshSingleCurrencyContent mustn't update the TxHistory and Buttons. It only must fetch primary
// currency. Now it not works because GetPrimaryCurrency's subscriber uses .distinctUntilChanged()
private fun refreshSingleCurrencyContent(walletIndex: Int) {