Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-23 18:04:31 +04:00
parent ad65a62b3f
commit 97a13da0d7
19 changed files with 176 additions and 56 deletions

View file

@ -80,6 +80,7 @@ dependencies {
api(projects.domain.analytics)
api(projects.domain.appCurrency)
api(projects.domain.appTheme)
api(projects.domain.appUpdate)
api(projects.domain.assetsdiscovery)
api(projects.domain.balanceHiding)
api(projects.domain.card)

View file

@ -12,6 +12,7 @@ import com.tangem.core.analytics.models.event.AssetsDiscoveryAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.review.ReviewManager
import com.tangem.core.navigation.url.AppStoreOpener
import com.tangem.domain.assetsdiscovery.usecase.AcknowledgeAssetsDiscoveryCompletionUseCase
import com.tangem.domain.card.SetCardWasScannedUseCase
import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -73,6 +74,8 @@ internal interface WalletWarningsClickIntents {
fun onSupportClick()
fun onSoftUpdateClick()
fun onBackupErrorClick()
fun onNoteMigrationButtonClick(url: String)
@ -111,6 +114,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val appStoreOpener: AppStoreOpener,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
@ -248,6 +252,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
}
override fun onSoftUpdateClick() {
appStoreOpener.openStorePage()
}
override fun onBackupErrorClick() {
val userWallet = getSelectedUserWallet() ?: return

View file

@ -145,6 +145,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
WalletNotificationUM.TangemPayUnreachable,
is WalletNotificationUM.YieldBoostPromo,
is WalletNotificationUM.AssetsDiscoveryCompleted,
is WalletNotificationUM.SoftUpdateAvailable,
-> null
}
}

View file

@ -5,6 +5,8 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.ui.ds.message.TangemMessageEffect
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.appupdate.model.AppUpdateState
import com.tangem.domain.appupdate.usecase.GetAppUpdateStateUseCase
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase
import com.tangem.domain.card.CardTypesResolver
@ -49,6 +51,7 @@ internal class GetWalletNotificationsFactory @Inject constructor(
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase,
private val observeAssetsDiscoveryUseCase: ObserveAssetsDiscoveryUseCase,
private val getAppUpdateStateUseCase: GetAppUpdateStateUseCase,
) {
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotificationUM>> {
val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver
@ -68,7 +71,8 @@ internal class GetWalletNotificationsFactory @Inject constructor(
flow2 = isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(),
flow3 = getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(),
flow4 = assetsDiscoveryProgressFlow,
) { accountList, isNeedToBackup, shouldAccessCodeSkipped, assetsDiscoveryProgress ->
flow5 = getAppUpdateStateUseCase.getBannerStateFlow(),
) { accountList, isNeedToBackup, shouldAccessCodeSkipped, assetsDiscoveryProgress, appUpdateState ->
val totalFiatBalance = accountList.totalFiatBalance
val flattenCurrencies = accountList.flattenCurrencies()
@ -126,10 +130,22 @@ internal class GetWalletNotificationsFactory @Inject constructor(
walletClickIntents = clickIntents,
)
}
addSoftUpdateBanner(appUpdateState, clickIntents)
}.sortedBy { it.type.ordinal }.toImmutableList()
}
}
private fun MutableList<WalletNotificationUM>.addSoftUpdateBanner(
appUpdateState: AppUpdateState,
clickIntents: WalletClickIntents,
) {
addIf(
element = WalletNotificationUM.SoftUpdateAvailable(onUpdateClick = clickIntents::onSoftUpdateClick),
condition = appUpdateState == AppUpdateState.OptionalUpdate,
)
}
private fun MutableList<WalletNotificationUM>.addUsedOutdatedDataNotification(totalFiatBalance: TotalFiatBalance) {
addIf(
element = WalletNotificationUM.UsedOutdatedData,

View file

@ -352,6 +352,27 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
),
type = WalletNotificationType.Warning,
)
data class SoftUpdateAvailable(val onUpdateClick: () -> Unit) : WalletNotificationUM(
messageUM = TangemMessageUM(
id = "SoftUpdateAvailableNotification",
title = resourceReference(id = CoreResR.string.force_update_banner_title),
subtitle = resourceReference(id = CoreResR.string.force_update_banner_message),
iconUM = TangemIconUM.Icon(
iconRes = R.drawable.ic_attention_default_24,
tintReference = { TangemTheme.colors2.graphic.status.attention },
),
messageEffect = TangemMessageEffect.Warning,
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(id = CoreResR.string.force_update_action),
type = TangemButtonType.Primary,
onClick = onUpdateClick,
),
),
),
type = WalletNotificationType.Warning,
)
// endregion
// region Promo

View file

@ -5,6 +5,8 @@ import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.appupdate.model.AppUpdateState
import com.tangem.domain.appupdate.usecase.GetAppUpdateStateUseCase
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase
import com.tangem.domain.card.CardTypesResolver
@ -56,6 +58,7 @@ internal class GetWalletNotificationsFactoryTest {
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase = mockk()
private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase = mockk()
private val observeAssetsDiscoveryUseCase: ObserveAssetsDiscoveryUseCase = mockk()
private val getAppUpdateStateUseCase: GetAppUpdateStateUseCase = mockk()
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk()
private val clickIntents: WalletClickIntents = mockk(relaxed = true)
@ -73,6 +76,7 @@ internal class GetWalletNotificationsFactoryTest {
getAccessCodeSkippedUseCase = getAccessCodeSkippedUseCase,
hasSingleWalletSignedHashesUseCase = hasSingleWalletSignedHashesUseCase,
observeAssetsDiscoveryUseCase = observeAssetsDiscoveryUseCase,
getAppUpdateStateUseCase = getAppUpdateStateUseCase,
)
@BeforeEach
@ -85,6 +89,7 @@ internal class GetWalletNotificationsFactoryTest {
getAccessCodeSkippedUseCase,
hasSingleWalletSignedHashesUseCase,
observeAssetsDiscoveryUseCase,
getAppUpdateStateUseCase,
singleAccountStatusListSupplier,
clickIntents,
coldResolver,
@ -100,6 +105,7 @@ internal class GetWalletNotificationsFactoryTest {
every { isNeedToBackupUseCase(any()) } returns flowOf(false)
every { getAccessCodeSkippedUseCase(any()) } returns flowOf(true)
every { observeAssetsDiscoveryUseCase(any()) } returns flowOf(AssetsDiscoveryProgress.Idle)
every { getAppUpdateStateUseCase.getBannerStateFlow() } returns flowOf(AppUpdateState.NoUpdate)
every { hasSingleWalletSignedHashesUseCase(any(), any()) } returns flowOf(false)
// Balance is loaded and non-zero, so both the outdated-data and add-funds banners stay hidden.
stubAccountStatusList(balance = LOADED_NON_ZERO)
@ -648,6 +654,42 @@ internal class GetWalletNotificationsFactoryTest {
}
// endregion
// region SoftUpdate
@Test
fun `GIVEN optional update WHEN create THEN soft-update banner is shown`() = runTest {
// Arrange
every { getAppUpdateStateUseCase.getBannerStateFlow() } returns flowOf(AppUpdateState.OptionalUpdate)
// Act
val result = factory.create(coldWallet, clickIntents).first()
// Assert
assertThat(result.any { it is WalletNotificationUM.SoftUpdateAvailable }).isTrue()
}
@ParameterizedTest
@MethodSource("provideNonOptionalUpdateStates")
fun `GIVEN non-optional update state WHEN create THEN soft-update banner is hidden`(
state: AppUpdateState,
) = runTest {
// Arrange
every { getAppUpdateStateUseCase.getBannerStateFlow() } returns flowOf(state)
// Act
val result = factory.create(coldWallet, clickIntents).first()
// Assert
assertThat(result.none { it is WalletNotificationUM.SoftUpdateAvailable }).isTrue()
}
private fun provideNonOptionalUpdateStates() = listOf(
AppUpdateState.NoUpdate,
AppUpdateState.ForceUpdate,
AppUpdateState.Brick,
AppUpdateState.OsTooOld,
)
// endregion
// region TangemPay warnings
@ParameterizedTest
@MethodSource("provideTangemPayModels")