Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-12 16:24:37 +05:00
parent d55166393e
commit 299117f8c4
13 changed files with 252 additions and 16 deletions

View file

@ -132,6 +132,18 @@ object PreferencesKeys {
val ACCESS_CODE_SKIPPED_STATES_KEY by lazy { stringPreferencesKey(name = "accessCodeSkippedStates") }
val SHOULD_SHOW_UPGRADE_BANNER_KEY by lazy { stringPreferencesKey(name = "shouldShowUpgradeBanner") }
val SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY by lazy {
stringPreferencesKey(name = "shouldShowNextTimeUpgradeBanner")
}
val UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY by lazy { stringPreferencesKey(name = "upgradeBannerClosureTimestamp") }
val WALLET_CREATION_TIMESTAMP_KEY by lazy { stringPreferencesKey(name = "walletCreationTimestamp") }
val HAS_HAD_FIRST_TOP_UP_KEY by lazy { stringPreferencesKey(name = "hasHadFirstTopUp") }
// region Notifications
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -5,6 +5,7 @@ import androidx.annotation.ChecksSdkIntAtLeast
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
import com.tangem.domain.hotwallet.repository.HotWalletRepository
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -34,4 +35,79 @@ internal class DefaultHotWalletRepository(
)
}
}
override fun shouldShowUpgradeBanner(userWalletId: UserWalletId): Flow<Boolean> = appPreferencesStore
.getObjectMap<Boolean>(PreferencesKeys.SHOULD_SHOW_UPGRADE_BANNER_KEY)
.map { it[userWalletId.stringValue] == true }
override suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) {
appPreferencesStore.editData { mutablePreferences ->
mutablePreferences.setObjectMap(
key = PreferencesKeys.SHOULD_SHOW_UPGRADE_BANNER_KEY,
value = mutablePreferences.getObjectMap<Boolean>(PreferencesKeys.SHOULD_SHOW_UPGRADE_BANNER_KEY)
.plus(userWalletId.stringValue to shouldShow),
)
}
}
override fun shouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId): Flow<Boolean> = appPreferencesStore
.getObjectMap<Boolean>(PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY)
.map { it[userWalletId.stringValue] == true }
override suspend fun setShouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) {
appPreferencesStore.editData { mutablePreferences ->
mutablePreferences.setObjectMap(
key = PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY,
value = mutablePreferences.getObjectMap<Boolean>(
PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY,
)
.plus(userWalletId.stringValue to shouldShow),
)
}
}
override suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? {
return appPreferencesStore
.getObjectMapSync<Long>(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY)[userWalletId.stringValue]
}
override suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long) {
appPreferencesStore.editData { mutablePreferences ->
mutablePreferences.setObjectMap(
key = PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY,
value = mutablePreferences.getObjectMap<Long>(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY)
.plus(userWalletId.stringValue to timestamp),
)
}
}
override suspend fun getWalletCreationTimestamp(userWalletId: UserWalletId): Long? {
return appPreferencesStore
.getObjectMapSync<Long>(PreferencesKeys.WALLET_CREATION_TIMESTAMP_KEY)[userWalletId.stringValue]
}
override suspend fun setWalletCreationTimestamp(userWalletId: UserWalletId, timestamp: Long) {
appPreferencesStore.editData { mutablePreferences ->
mutablePreferences.setObjectMap(
key = PreferencesKeys.WALLET_CREATION_TIMESTAMP_KEY,
value = mutablePreferences.getObjectMap<Long>(PreferencesKeys.WALLET_CREATION_TIMESTAMP_KEY)
.plus(userWalletId.stringValue to timestamp),
)
}
}
override suspend fun hasHadFirstTopUp(userWalletId: UserWalletId): Boolean {
return appPreferencesStore
.getObjectMapSync<Boolean>(PreferencesKeys.HAS_HAD_FIRST_TOP_UP_KEY)[userWalletId.stringValue] == true
}
override suspend fun setHasHadFirstTopUp(userWalletId: UserWalletId, hasTopUp: Boolean) {
appPreferencesStore.editData { mutablePreferences ->
mutablePreferences.setObjectMap(
key = PreferencesKeys.HAS_HAD_FIRST_TOP_UP_KEY,
value = mutablePreferences.getObjectMap<Boolean>(PreferencesKeys.HAS_HAD_FIRST_TOP_UP_KEY)
.plus(userWalletId.stringValue to hasTopUp),
)
}
}
}

View file

@ -12,4 +12,24 @@ interface HotWalletRepository {
fun accessCodeSkipped(userWalletId: UserWalletId): Flow<Boolean>
suspend fun setAccessCodeSkipped(userWalletId: UserWalletId, skipped: Boolean)
fun shouldShowUpgradeBanner(userWalletId: UserWalletId): Flow<Boolean>
suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean)
fun shouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId): Flow<Boolean>
suspend fun setShouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean)
suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long?
suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long)
suspend fun getWalletCreationTimestamp(userWalletId: UserWalletId): Long?
suspend fun setWalletCreationTimestamp(userWalletId: UserWalletId, timestamp: Long)
suspend fun hasHadFirstTopUp(userWalletId: UserWalletId): Boolean
suspend fun setHasHadFirstTopUp(userWalletId: UserWalletId, hasTopUp: Boolean)
}

View file

@ -17,6 +17,7 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.hotwallet.repository.HotWalletRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
@ -98,6 +99,10 @@ internal interface WalletWarningsClickIntents {
fun onFinishWalletActivationClick(isBackupExists: Boolean)
fun onYieldPromoTermsAndConditionsClick()
fun onUpgradeHotWalletClick(userWalletId: UserWalletId)
fun onCloseUpgradeBannerClick(userWalletId: UserWalletId)
}
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@ -130,6 +135,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase,
private val uiMessageSender: UiMessageSender,
private val reviewManager: ReviewManager,
private val hotWalletRepository: HotWalletRepository,
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
override fun onAddBackupCardClick() {
@ -544,6 +550,34 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
urlOpener.openUrl(YIELD_PROMO_TERMS_LINK)
}
override fun onUpgradeHotWalletClick(userWalletId: UserWalletId) {
modelScope.launch(dispatchers.main) {
val userWallet = getUserWalletUseCase(userWalletId).getOrNull()
if (userWallet is UserWallet.Hot) {
hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, false)
hotWalletRepository.setShouldShowNextTimeUpgradeBanner(userWalletId, false)
appRouter.push(UpgradeWallet(userWalletId))
}
}
}
override fun onCloseUpgradeBannerClick(userWalletId: UserWalletId) {
modelScope.launch(dispatchers.main) {
val userWallet = getUserWalletUseCase(userWalletId).getOrNull()
if (userWallet is UserWallet.Hot) {
val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(userWalletId)
val currentTime = System.currentTimeMillis()
if (hasHadFirstTopUp) {
hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, false)
hotWalletRepository.setUpgradeBannerClosureTimestamp(userWalletId, currentTime)
} else {
hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, true)
}
}
}
}
private companion object {
const val VISA_PROMO_LINK = "https://tangem.com/en/cardwaitlist/?utm_source=tangem-app-banner" +
"&utm_medium=banner" +

View file

@ -6,8 +6,9 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.*
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen.*
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.PushBannerPromo.*
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
@ -40,16 +41,16 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
@Suppress("CyclomaticComplexMethod")
private fun getEvent(warning: WalletNotification): AnalyticsEvent? {
return when (warning) {
is WalletNotification.Critical.DevCard -> MainScreen.DevelopmentCard()
is WalletNotification.Critical.FailedCardValidation -> MainScreen.ProductSampleCard()
is WalletNotification.Warning.MissingBackup -> MainScreen.BackupYourWallet()
is WalletNotification.Warning.NumberOfSignedHashesIncorrect -> MainScreen.CardSignedTransactions()
is WalletNotification.Warning.TestNetCard -> MainScreen.TestnetCard()
is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard()
is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses()
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem()
is WalletNotification.Critical.BackupError -> MainScreen.BackupError()
is WalletNotification.NoteMigration -> MainScreen.NotePromo()
is WalletNotification.Critical.DevCard -> DevelopmentCard()
is WalletNotification.Critical.FailedCardValidation -> ProductSampleCard()
is WalletNotification.Warning.MissingBackup -> BackupYourWallet()
is WalletNotification.Warning.NumberOfSignedHashesIncorrect -> CardSignedTransactions()
is WalletNotification.Warning.TestNetCard -> TestnetCard()
is WalletNotification.Informational.DemoCard -> DemoCard()
is WalletNotification.Informational.MissingAddresses -> MissingAddresses()
is WalletNotification.RateApp -> HowDoYouLikeTangem()
is WalletNotification.Critical.BackupError -> BackupError()
is WalletNotification.NoteMigration -> NotePromo()
is WalletNotification.SwapPromo -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.Empty, // Use it on new promo action
@ -92,16 +93,17 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
WalletActivationBannerType.Attention -> AnalyticsParam.EmptyFull.Empty
WalletActivationBannerType.Warning -> AnalyticsParam.EmptyFull.Full
}
MainScreen.NoticeFinishActivation(
NoticeFinishActivation(
activationState = activationState,
balanceState = balanceState,
)
}
is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport()
is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond()
is WalletNotification.PushNotifications -> WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner()
is WalletNotification.Critical.SeedPhraseNotification -> NoticeSeedPhraseSupport()
is WalletNotification.Critical.SeedPhraseSecondNotification -> NoticeSeedPhraseSupportSecond()
is WalletNotification.PushNotifications -> PushBanner()
is WalletNotification.Warning.TangemPayRefreshNeeded -> null
WalletNotification.Warning.TangemPayUnreachable -> null
is WalletNotification.Warning.TangemPayUnreachable -> null
is WalletNotification.UpgradeHotWalletPromo -> null
}
}
}

View file

@ -14,6 +14,7 @@ import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase
import com.tangem.domain.hotwallet.repository.HotWalletRepository
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrency
@ -43,6 +44,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@Suppress("LongParameterList", "LargeClass")
@ -58,6 +60,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val notificationsRepository: NotificationsRepository,
private val accountDependencies: AccountDependencies,
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
private val hotWalletRepository: HotWalletRepository,
) {
@Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod")
@ -98,6 +101,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo)
.distinctUntilChanged(),
hotWalletRepository.shouldShowUpgradeBanner(userWallet.walletId).distinctUntilChanged(),
hotWalletRepository.shouldShowNextTimeUpgradeBanner(userWallet.walletId).distinctUntilChanged(),
) { array -> array }
.combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) }
.map { array ->
@ -111,12 +116,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val shouldShowEnablePushesReminderNotification = array[5] as Boolean
val shouldAccessCodeSkipped = array[6] as Boolean
val shouldShowYieldPromo = array[7] as Boolean
val shouldShowUpgradeBanner = array[8] as Boolean
val shouldShowNextTimeUpgradeBanner = array[9] as Boolean
buildList {
addUsedOutdatedDataNotification(totalFiatBalance)
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
addUpgradeHotWalletPromoNotification(
userWallet = userWallet,
flattenCurrencies = flattenCurrencies,
clickIntents = clickIntents,
shouldShowUpgradeBanner = shouldShowUpgradeBanner,
shouldShowNextTimeUpgradeBanner = shouldShowNextTimeUpgradeBanner,
)
addFinishWalletActivationNotification(
userWallet = userWallet,
flattenCurrencies = flattenCurrencies,
@ -449,7 +464,57 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
@Suppress("CyclomaticComplexMethod")
private suspend fun MutableList<WalletNotification>.addUpgradeHotWalletPromoNotification(
userWallet: UserWallet,
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
clickIntents: WalletClickIntents,
shouldShowUpgradeBanner: Boolean,
shouldShowNextTimeUpgradeBanner: Boolean,
) {
if (userWallet !is UserWallet.Hot) return
val currentTime = System.currentTimeMillis()
val creationTimestamp = hotWalletRepository.getWalletCreationTimestamp(userWallet.walletId)
val closureTimestamp = hotWalletRepository.getUpgradeBannerClosureTimestamp(userWallet.walletId)
val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(userWallet.walletId)
if (creationTimestamp == null) {
hotWalletRepository.setWalletCreationTimestamp(userWallet.walletId, currentTime)
return
}
val daysSinceCreation = TimeUnit.MILLISECONDS.toDays(currentTime - creationTimestamp)
val daysSinceClosure = closureTimestamp?.let { TimeUnit.MILLISECONDS.toDays(currentTime - it) }
val currencies = flattenCurrencies.getOrNull(isPartialContentAccepted = true).orEmpty()
val hasBalance = currencies.any { it.value.amount.orZero().isPositive() }
if (hasBalance && !hasHadFirstTopUp) {
hotWalletRepository.setHasHadFirstTopUp(userWallet.walletId, true)
}
val shouldShow = when {
shouldShowUpgradeBanner && hasBalance -> true
shouldShowNextTimeUpgradeBanner && daysSinceClosure != null && daysSinceClosure >= UPGRADE_BANNER_RESHOW_DAYS -> true
!shouldShowUpgradeBanner && !shouldShowNextTimeUpgradeBanner && hasHadFirstTopUp && daysSinceCreation >= UPGRADE_BANNER_RESHOW_DAYS -> {
hotWalletRepository.setShouldShowNextTimeUpgradeBanner(userWallet.walletId, true)
true
}
else -> false
}
addIf(
element = WalletNotification.UpgradeHotWalletPromo(
onLaterClick = { clickIntents.onCloseUpgradeBannerClick(userWallet.walletId) },
onUpgradeClick = { clickIntents.onUpgradeHotWalletClick(userWallet.walletId) },
),
condition = shouldShow,
)
}
private companion object {
const val MAX_REMAINING_SIGNATURES_COUNT = 10
const val UPGRADE_BANNER_RESHOW_DAYS = 30
}
}

View file

@ -445,4 +445,22 @@ sealed class WalletNotification(val config: NotificationConfig) {
),
),
)
data class UpgradeHotWalletPromo(
val onLaterClick: () -> Unit,
val onUpgradeClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.hw_upgrade_to_cold_banner_title),
subtitle = resourceReference(R.string.hw_upgrade_to_cold_banner_description),
iconResId = R.drawable.img_tangem_wallet_72,
buttonsState = ButtonsState.PairButtonsConfig(
primaryText = resourceReference(R.string.hw_upgrade),
onPrimaryClick = onUpgradeClick,
secondaryText = resourceReference(R.string.common_later),
onSecondaryClick = onLaterClick,
),
iconSize = 72.dp,
),
)
}

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.notifications.NoteMigrationNotification
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.ForceDarkTheme
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import kotlinx.collections.immutable.ImmutableList
@ -40,6 +41,14 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
)
}
is WalletNotification.UpgradeHotWalletPromo -> {
ForceDarkTheme {
Notification(
config = item.config,
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
)
}
}
else -> {
Notification(
config = item.config,