From 54c160f4f2b736a05c252ce58d38b87bddb348cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Feb 2026 20:07:31 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../com/tangem/common/TangemSiteUrlBuilder.kt | 2 + .../common/ui/notifications/Notifications.kt | 2 +- .../core/ui/ds/message/TangemMessage.kt | 17 +- .../utils/WalletWarningsAnalyticsSender.kt | 73 +++- .../utils/WalletWarningsSingleEventSender.kt | 48 ++- .../GetWalletNotificationsCarouselFactory.kt | 131 +++++++ .../wallet/domain/GetWalletWarningsFactory.kt | 326 +++++++++++++++ .../wallet/state/model/WalletBalanceUM.kt | 13 +- .../state/model/WalletNotificationUM.kt | 371 +++++++++++------- .../wallet/state/model/WalletUM.kt | 4 +- .../transformers/SetWarningsTransformer.kt | 15 +- .../MultiWalletWarningsSubscriber.kt | 15 +- .../MultiWalletWarningsSubscriberV2.kt | 71 ++++ .../SingleWalletNotificationsSubscriber.kt | 3 +- 14 files changed, 914 insertions(+), 177 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt diff --git a/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt index 6dc7f0d926..ab45c7159d 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt @@ -9,6 +9,8 @@ import kotlin.coroutines.suspendCoroutine object TangemSiteUrlBuilder { + const val NOTE_MIGRATION_URL = "https://tangem.com/en/?promocode=Note10" + suspend fun getUtmTags(campaign: String?): String { val langCode = Locale.getDefault().language val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty() diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt index 1c3870ab40..39c4d36faa 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt @@ -124,7 +124,7 @@ fun LazyListScope.notifications( contentColor = contentColor, modifier = modifier .padding(top = topPadding) - .animateItem(), + .animateItem(null, null, null), ) }, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt index 3591920330..86d955f2d9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt @@ -22,11 +22,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.R -import com.tangem.core.ui.components.flicker import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.ds.button.* import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -58,7 +58,15 @@ fun TangemMessage( if (messageUM.iconUM != null) { TangemIcon( tangemIconUM = messageUM.iconUM, - modifier = Modifier.size(TangemTheme.dimens2.x8), + modifier = Modifier + .align( + if (messageUM.buttonsUM.isEmpty()) { + Alignment.CenterVertically + } else { + Alignment.Top + }, + ) + .size(TangemTheme.dimens2.x7), ) } }, @@ -342,6 +350,7 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider) { + if (screenLifecycleProvider.isBackgroundState.value) return + if (newNotifications.isEmpty()) return + if (displayedWalletUM == null || displayedWalletUM.pullToRefreshConfig.isRefreshing) return + + val totalNotifications = displayedWalletUM.notifications + displayedWalletUM.notificationsCarousel + val notificationsDiff = newNotifications.filter { it !in totalNotifications } + + val eventsToSend = getEvents2(notificationsDiff) + + eventsToSend.forEach { event -> + analyticsEventHandler.send(event) + } + } + private fun getEvents(warnings: List): Set { return warnings.mapNotNullTo(mutableSetOf(), ::getEvent) } + private fun getEvents2(notifications: List): Set { + return notifications.mapNotNullTo(mutableSetOf(), ::getEvent2) + } + @Suppress("CyclomaticComplexMethod") private fun getEvent(warning: WalletNotification): AnalyticsEvent? { return when (warning) { @@ -106,4 +124,53 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.UpgradeHotWalletPromo -> null } } + + @Suppress("CyclomaticComplexMethod") + private fun getEvent2(notificationUM: WalletNotificationUM): AnalyticsEvent? { + return when (notificationUM) { + WalletNotificationUM.DevCard -> DevelopmentCard() + WalletNotificationUM.FailedCardValidation -> ProductSampleCard() + is WalletNotificationUM.MissingBackup -> BackupYourWallet() + is WalletNotificationUM.NumberOfSignedHashesIncorrect -> CardSignedTransactions() + WalletNotificationUM.TestnetCard -> TestnetCard() + WalletNotificationUM.DemoCard -> DemoCard() + is WalletNotificationUM.MissingAddresses -> MissingAddresses() + is WalletNotificationUM.RateApp -> HowDoYouLikeTangem() + is WalletNotificationUM.BackupError -> BackupError() + is WalletNotificationUM.NoteMigration -> NotePromo() + is WalletNotificationUM.OnePlusOnePromo -> NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + program = Program.OnePlusOne, + ) + is WalletNotificationUM.YieldPromo -> NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + program = Program.YieldPromo, + ) + is WalletNotificationUM.FinishWalletActivation -> { + val activationState = if (notificationUM.isBackupExists) { + NoticeFinishActivation.ActivationState.Unfinished + } else { + NoticeFinishActivation.ActivationState.NotStarted + } + val balanceState = when (notificationUM.type) { + WalletNotificationType.Warning -> AnalyticsParam.EmptyFull.Full + else -> AnalyticsParam.EmptyFull.Empty + } + NoticeFinishActivation( + activationState = activationState, + balanceState = balanceState, + ) + } + is WalletNotificationUM.SeedPhraseNotification -> NoticeSeedPhraseSupport() + is WalletNotificationUM.SeedPhraseSecondNotification -> NoticeSeedPhraseSupportSecond() + is WalletNotificationUM.PushNotifications -> PushBanner() + is WalletNotificationUM.UnlockWallets, + is WalletNotificationUM.NoAccount, + is WalletNotificationUM.LowSignatures, + WalletNotificationUM.SomeNetworksUnreachable, + is WalletNotificationUM.UsedOutdatedData, + is WalletNotificationUM.CloreMigration, + -> null + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt index f52290e6d2..fdcdc567b2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt @@ -4,12 +4,8 @@ import com.tangem.common.routing.AppRoute.WalletBackup import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 -import com.tangem.core.ui.components.bottomsheets.message.icon -import com.tangem.core.ui.components.bottomsheets.message.infoBlock -import com.tangem.core.ui.components.bottomsheets.message.onClick -import com.tangem.core.ui.components.bottomsheets.message.primaryButton -import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.components.bottomsheets.message.* +import com.tangem.core.ui.ds.message.TangemMessageEffect import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.models.wallet.UserWallet @@ -19,7 +15,9 @@ import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -71,6 +69,44 @@ internal class WalletWarningsSingleEventSender @Inject constructor( } } + suspend fun send( + userWalletId: UserWalletId, + displayedWalletUM: WalletUM?, + newNotifications: List, + ) { + if (screenLifecycleProvider.isBackgroundState.value) return + if (newNotifications.isEmpty()) return + if (displayedWalletUM == null || displayedWalletUM.pullToRefreshConfig.isRefreshing) return + + val totalNotifications = displayedWalletUM.notifications + displayedWalletUM.notificationsCarousel + val events = newNotifications.filter { it !in totalNotifications } + + // We must show activation bs only for the first seen wallet when open the app (if need, see conditions below), + // so we keep this wallet id and use for future checks, ignore other wallets during the app session. + if (isActivationBottomSheetShown.isEmpty()) { + isActivationBottomSheetShown[userWalletId] = false + } + + events.forEach { event -> + when (event) { + is WalletNotificationUM.SeedPhraseNotification -> { + seedPhraseNotificationUseCase.notified(userWalletId = userWalletId) + } + is WalletNotificationUM.FinishWalletActivation -> { + // We check that map contains the first seen wallet (will return null instead false/true otherwise) + // and for this wallet we haven't shown the activation bs yet (check that returns false, not true) + if (isActivationBottomSheetShown[userWalletId] == false) { + if (event.messageEffect == TangemMessageEffect.Warning && event.isBackupExists.not()) { + showFinishActivationBottomSheet(userWalletId) + } + isActivationBottomSheetShown[userWalletId] = true + } + } + else -> Unit + } + } + } + private fun showFinishActivationBottomSheet(userWalletId: UserWalletId) { val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return if (userWallet !is UserWallet.Hot) return diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt new file mode 100644 index 0000000000..8e0d92d545 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt @@ -0,0 +1,131 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.common.TangemSiteUrlBuilder +import com.tangem.common.ui.notifications.NotificationId +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.promo.ShouldShowPromoWalletUseCase +import com.tangem.domain.promo.models.PromoId +import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM +import com.tangem.utils.extensions.addIf +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import javax.inject.Inject + +/** + * Factory for creating a list of notifications that can be shown on the wallet screen. + * These notifications are not critical and can be stacked with each other. + */ +@ModelScoped +internal class GetWalletNotificationsCarouselFactory @Inject constructor( + private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, + private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val notificationsRepository: NotificationsRepository, +) { + fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { + return combine( + flow = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo) + .distinctUntilChanged(), + flow2 = notificationsRepository.getShouldShowNotification( + NotificationId.EnablePushesReminderNotification.key, + ).distinctUntilChanged(), + flow3 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.OnePlusOne) + .distinctUntilChanged(), + flow4 = isReadyToShowRateAppUseCase().distinctUntilChanged(), + flow5 = getWalletsUseCase().conflate(), + ) { showYieldPromo, showPushesNotification, showOnePlusOnePromo, showRateAppPromo, wallets -> + + buildList { + addNoteMigrationNotification(userWallet, wallets, clickIntents) + addRateAppNotification(showRateAppPromo, clickIntents) + addOnePlusOnePromoNotification(clickIntents, showOnePlusOnePromo) + addYieldPromoNotification(clickIntents, showYieldPromo) + + addPushNotification( + shouldShow = showPushesNotification, + isPushesAllowed = notificationsRepository.isUserAllowToSubscribeOnPushNotifications(), + clickIntents = clickIntents, + ) + }.sortedBy { it.type.ordinal }.toImmutableList() + } + } + + private fun MutableList.addRateAppNotification( + isReadyToShowRating: Boolean, + clickIntents: WalletClickIntents, + ) { + addIf(isReadyToShowRating) { + WalletNotificationUM.RateApp( + onLikeClick = clickIntents::onLikeAppClick, + onDislikeClick = clickIntents::onDislikeAppClick, + onCloseClick = clickIntents::onCloseRateAppWarningClick, + ) + } + } + + private fun MutableList.addYieldPromoNotification( + clickIntents: WalletClickIntents, + shouldShowPromo: Boolean, + ) { + addIf(shouldShowPromo) { + WalletNotificationUM.YieldPromo( + onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.YieldPromo) }, + onTermsAndConditionsClick = { clickIntents.onYieldPromoTermsAndConditionsClick() }, + ) + } + } + + private fun MutableList.addOnePlusOnePromoNotification( + clickIntents: WalletClickIntents, + shouldShowPromo: Boolean, + ) { + addIf(shouldShowPromo) { + WalletNotificationUM.OnePlusOnePromo( + onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.OnePlusOne) }, + onClick = { clickIntents.onPromoClick(promoId = PromoId.OnePlusOne) }, + ) + } + } + + private fun MutableList.addNoteMigrationNotification( + userWallet: UserWallet, + userWallets: List, + clickIntents: WalletClickIntents, + ) { + val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver + + val isUserHasWalletOrWallet2 = userWallets.filterIsInstance().any { wallet -> + val typesResolver = wallet.scanResponse.cardTypesResolver + typesResolver.isTangemWallet() || typesResolver.isWallet2() + } + + addIf(cardTypesResolver != null && cardTypesResolver.isTangemNote() && !isUserHasWalletOrWallet2) { + WalletNotificationUM.NoteMigration( + onClick = { clickIntents.onNoteMigrationButtonClick(TangemSiteUrlBuilder.NOTE_MIGRATION_URL) }, + ) + } + } + + private fun MutableList.addPushNotification( + shouldShow: Boolean, + isPushesAllowed: Boolean, + clickIntents: WalletClickIntents, + ) { + addIf(shouldShow && !isPushesAllowed) { + WalletNotificationUM.PushNotifications( + onCloseClick = clickIntents::onDenyPermissions, + onEnabledClick = clickIntents::onAllowPermissions, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt new file mode 100644 index 0000000000..c900381523 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt @@ -0,0 +1,326 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.common.ui.userwallet.ext.walletInterationIcon +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.card.CardTypesResolver +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TotalFiatBalance +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.wallets.models.SeedPhraseNotificationsStatus +import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase +import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase +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.model.WalletNotificationUM +import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.extensions.addIf +import com.tangem.utils.extensions.isPositive +import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +/** + * Factory for creating a list of notifications that can be shown on the wallet screen. + * These notifications are critical and should be shown separately from each other. + */ +@Suppress("LongParameterList") +@ModelScoped +internal class GetWalletWarningsFactory @Inject constructor( + private val isDemoCardUseCase: IsDemoCardUseCase, + private val isNeedToBackupUseCase: IsNeedToBackupUseCase, + private val backupValidator: BackupValidator, + private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, + private val accountDependencies: AccountDependencies, + private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, + private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, +) { + fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { + val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver + + val params = SingleAccountStatusListProducer.Params(userWallet.walletId) + val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) + + return combine( + flow = accountStatusListFlow, + flow2 = isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(), + flow3 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId).distinctUntilChanged(), + flow4 = getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), + ) { accountList, isNeedToBackup, seedPhraseIssueStatus, shouldAccessCodeSkipped -> + val totalFiatBalance = accountList.totalFiatBalance + val flattenCurrencies = accountList.flattenCurrencies() + + buildList { + addUsedOutdatedDataNotification(totalFiatBalance) + + addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) + + addFinishWalletActivationNotification( + userWallet = userWallet, + totalFiatBalance = totalFiatBalance, + clickIntents = clickIntents, + shouldAccessCodeSkipped = shouldAccessCodeSkipped, + ) + + addInformationalNotifications( + userWallet = userWallet, + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + clickIntents = clickIntents, + ) + + addWarningNotifications( + userWallet = userWallet, + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + isNeedToBackup = isNeedToBackup, + clickIntents = clickIntents, + ) + }.sortedBy { it.type.ordinal }.toImmutableList() + } + } + + private fun MutableList.addUsedOutdatedDataNotification(totalFiatBalance: TotalFiatBalance) { + addIf( + element = WalletNotificationUM.UsedOutdatedData, + condition = (totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE, + ) + } + + private fun MutableList.addCriticalNotifications( + userWallet: UserWallet, + seedPhraseIssueStatus: SeedPhraseNotificationsStatus, + clickIntents: WalletClickIntents, + ) { + if (userWallet !is UserWallet.Cold) { + return + } + + addSeedNotificationIfNeeded(userWallet, seedPhraseIssueStatus, clickIntents) + + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + addIf( + element = WalletNotificationUM.BackupError { clickIntents.onSupportClick() }, + condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError, + ) + + addIf( + element = WalletNotificationUM.DevCard, + condition = !cardTypesResolver.isReleaseFirmwareType(), + ) + + addIf( + element = WalletNotificationUM.FailedCardValidation, + condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(), + ) + + cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures -> + addIf( + element = WalletNotificationUM.LowSignatures(count = remainingSignatures), + condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT, + ) + } + } + + private fun MutableList.addInformationalNotifications( + userWallet: UserWallet, + cardTypesResolver: CardTypesResolver?, + flattenCurrencies: List, + clickIntents: WalletClickIntents, + ) { + addIf( + element = WalletNotificationUM.DemoCard, + condition = cardTypesResolver != null && isDemoCardUseCase(cardId = cardTypesResolver.getCardId()), + ) + + addMissingAddressesNotification(userWallet, flattenCurrencies, clickIntents) + } + + private fun MutableList.addMissingAddressesNotification( + userWallet: UserWallet, + flattenCurrencies: List, + clickIntents: WalletClickIntents, + ) { + val currencies = flattenCurrencies.getMissingAddressCurrencies().ifEmpty { return } + + addIf( + element = WalletNotificationUM.MissingAddresses( + tangemIcon = walletInterationIcon(userWallet), + missingAddressesCount = currencies.count(), + onGenerateClick = { + clickIntents.onGenerateMissedAddressesClick(missedAddressCurrencies = currencies) + }, + ), + condition = currencies.isNotEmpty(), + ) + } + + private fun List.getMissingAddressCurrencies(): List { + return this + .filter { it.value is CryptoCurrencyStatus.MissedDerivation } + .map(CryptoCurrencyStatus::currency) + } + + private suspend fun MutableList.addWarningNotifications( + userWallet: UserWallet, + cardTypesResolver: CardTypesResolver?, + flattenCurrencies: List, + isNeedToBackup: Boolean, + clickIntents: WalletClickIntents, + ) { + addIf( + element = WalletNotificationUM.MissingBackup( + onClick = clickIntents::onAddBackupCardClick, + ), + condition = isNeedToBackup, + ) + + addIf( + element = WalletNotificationUM.TestnetCard, + condition = cardTypesResolver?.isTestCard() == true, + ) + + addIf( + element = WalletNotificationUM.SomeNetworksUnreachable, + condition = flattenCurrencies.hasUnreachableNetworks(), + ) + + addCloreMigrationNotification(flattenCurrencies, clickIntents) + + addNoAccountWarning(cryptoCurrencyStatus = flattenCurrencies.firstOrNull()) + + addIf( + element = WalletNotificationUM.NumberOfSignedHashesIncorrect( + onCloseClick = clickIntents::onCloseAlreadySignedHashesWarningClick, + ), + condition = hasSignedHashes(userWallet, flattenCurrencies.firstOrNull()), + ) + } + + private fun MutableList.addNoAccountWarning(cryptoCurrencyStatus: CryptoCurrencyStatus?) { + val noAccountStatus = cryptoCurrencyStatus?.value as? CryptoCurrencyStatus.NoAccount + if (noAccountStatus != null) { + add( + element = WalletNotificationUM.NoAccount( + network = cryptoCurrencyStatus.currency.name, + amount = noAccountStatus.amountToCreateAccount.toString(), + symbol = cryptoCurrencyStatus.currency.symbol, + ), + ) + } + } + + private fun MutableList.addCloreMigrationNotification( + flattenCurrencies: List, + clickIntents: WalletClickIntents, + ) { + val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return + + add( + WalletNotificationUM.CloreMigration( + onStartMigrationClick = { clickIntents.onCloreMigrationClick(cloreCurrency) }, + ), + ) + } + + private fun List.findCloreCurrency(): CryptoCurrencyStatus? { + return find { currencyStatus -> + BlockchainUtils.isClore(currencyStatus.currency.network.rawId) + } + } + + private fun List.hasUnreachableNetworks(): Boolean { + return any { it.value is CryptoCurrencyStatus.Unreachable } + } + + private fun MutableList.addFinishWalletActivationNotification( + userWallet: UserWallet, + totalFiatBalance: TotalFiatBalance, + clickIntents: WalletClickIntents, + shouldAccessCodeSkipped: Boolean, + ) { + if (userWallet !is UserWallet.Hot) return + + val isBackupExists = userWallet.backedUp + val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword && + !shouldAccessCodeSkipped + val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired + + val messageEffect = when (totalFiatBalance) { + TotalFiatBalance.Failed, + TotalFiatBalance.Loading, + -> TangemMessageEffect.None + is TotalFiatBalance.Loaded -> if (totalFiatBalance.amount.orZero().isPositive()) { + TangemMessageEffect.Warning + } else { + TangemMessageEffect.None + } + } + + addIf( + element = WalletNotificationUM.FinishWalletActivation( + messageEffect = messageEffect, + onClick = { clickIntents.onFinishWalletActivationClick(isBackupExists) }, + isBackupExists = isBackupExists, + ), + condition = shouldShowFinishActivation, + ) + } + + private fun MutableList.addSeedNotificationIfNeeded( + userWallet: UserWallet.Cold, + seedPhraseIssueStatus: SeedPhraseNotificationsStatus, + clickIntents: WalletClickIntents, + ) { + val isNotificationAvailable = with(userWallet) { + val isDemo = isDemoCardUseCase(cardId = userWallet.cardId) + val isWalletWithSeedPhrase = scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported + + !isDemo && isWalletWithSeedPhrase + } + + when (seedPhraseIssueStatus) { + SeedPhraseNotificationsStatus.SHOW_FIRST -> addIf( + element = WalletNotificationUM.SeedPhraseNotification( + onDeclineClick = clickIntents::onSeedPhraseNotificationDecline, + onConfirmClick = clickIntents::onSeedPhraseNotificationConfirm, + ), + condition = isNotificationAvailable, + ) + SeedPhraseNotificationsStatus.SHOW_SECOND -> addIf( + element = WalletNotificationUM.SeedPhraseSecondNotification( + onDeclineClick = clickIntents::onSeedPhraseSecondNotificationReject, + onConfirmClick = clickIntents::onSeedPhraseSecondNotificationAccept, + ), + condition = isNotificationAvailable, + ) + SeedPhraseNotificationsStatus.NOT_NEEDED -> Unit + } + } + + private suspend fun hasSignedHashes( + selectedWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus?, + ): Boolean { + if (selectedWallet !is UserWallet.Cold || !selectedWallet.isMultiCurrency) return false + val network = cryptoCurrencyStatus?.currency?.network ?: return false + + return hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = network) + .conflate() + .distinctUntilChanged() + .firstOrNull() == true + } + + private companion object { + const val MAX_REMAINING_SIGNATURES_COUNT = 10 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt index f55919de64..cdbe103b75 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt @@ -4,7 +4,17 @@ import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWalletId -/** Wallet card state */ +/** + * Represents the state of the wallet balance in the UI. + * + * The sealed interface has three implementations: + * - [Content]: Represents the state when the wallet balance is successfully loaded. + * - [Error]: Represents the state when there was an error loading the wallet balance. + * - [Loading]: Represents the state when the wallet balance is currently being loaded. + * + * @property id The unique identifier of the wallet. + * @property name The name of the wallet. + */ @Immutable internal sealed interface WalletBalanceUM { @@ -27,7 +37,6 @@ internal sealed interface WalletBalanceUM { val balance: TextReference, val isBalanceFlickering: Boolean, val isZeroBalance: Boolean?, - ) : WalletBalanceUM /** diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt index eba6f7dbbe..9d24a0c3da 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt @@ -34,14 +34,33 @@ internal enum class WalletNotificationType { */ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val type: WalletNotificationType) { - data object DevCard : WalletNotificationUM( + // region Status + data object SomeNetworksUnreachable : WalletNotificationUM( messageUM = TangemMessageUM( - id = "DevCardNotification", - title = resourceReference(id = R.string.warning_developer_card_title), - subtitle = resourceReference(id = R.string.warning_developer_card_message), + id = "SomeNetworksUnreachableNotification", + title = resourceReference(id = R.string.warning_some_networks_unreachable_title), + subtitle = resourceReference(id = R.string.warning_some_networks_unreachable_message), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), messageEffect = TangemMessageEffect.None, ), - type = WalletNotificationType.Warning, + type = WalletNotificationType.Status, + ) + + data object UsedOutdatedData : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "UsedOutdatedDataNotification", + title = stringReference("Missing some token balances"), // todo redesign main lokalise + subtitle = stringReference("Will be updated as soon as possible"), // todo redesign main lokalise + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_error_sync_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Status, ) data object FailedCardValidation : WalletNotificationUM( @@ -49,17 +68,69 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t id = "FailedCardValidationNotification", title = resourceReference(id = R.string.warning_failed_to_verify_card_title), subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), messageEffect = TangemMessageEffect.Warning, ), type = WalletNotificationType.Status, ) + data object DevCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "DevCardNotification", + title = resourceReference(id = R.string.warning_developer_card_title), + subtitle = resourceReference(id = R.string.warning_developer_card_message), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Status, + ) + + data object TestnetCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "TestnetCardNotification", + title = resourceReference(id = R.string.warning_testnet_card_title), + subtitle = resourceReference(id = R.string.warning_testnet_card_message), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Status, + ) + + data object DemoCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "DemoCardNotification", + title = resourceReference(id = R.string.warning_demo_mode_title), + subtitle = resourceReference(id = R.string.warning_demo_mode_message), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Status, + ) + // endregion + + // region Critical data class BackupError(val onClick: () -> Unit) : WalletNotificationUM( messageUM = TangemMessageUM( id = "BackupErrorNotification", title = resourceReference(id = R.string.warning_backup_errors_title), subtitle = resourceReference(id = R.string.warning_backup_errors_message), messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), buttonsUM = persistentListOf( TangemMessageButtonUM( text = resourceReference(id = R.string.common_contact_support), @@ -68,7 +139,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t ), ), ), - type = WalletNotificationType.Warning, + type = WalletNotificationType.Critical, ) data class SeedPhraseNotification( @@ -92,6 +163,10 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t onClick = onConfirmClick, ), ), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), ), type = WalletNotificationType.Critical, ) @@ -105,6 +180,10 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t title = resourceReference(id = R.string.warning_seedphrase_action_required_title), subtitle = resourceReference(id = R.string.warning_seedphrase_contacted_support), messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), buttonsUM = persistentListOf( TangemMessageButtonUM( text = resourceReference(id = R.string.seed_warning_no), @@ -117,6 +196,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t onClick = onConfirmClick, ), ), + ), type = WalletNotificationType.Critical, ) @@ -127,6 +207,10 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t title = resourceReference(id = R.string.warning_no_backup_title), subtitle = resourceReference(id = R.string.warning_no_backup_message), messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), buttonsUM = persistentListOf( TangemMessageButtonUM( text = resourceReference(id = R.string.button_start_backup_process), @@ -138,37 +222,6 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t type = WalletNotificationType.Critical, ) - data object SomeNetworksUnreachable : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "SomeNetworksUnreachableNotification", - title = resourceReference(id = R.string.warning_some_networks_unreachable_title), - subtitle = resourceReference(id = R.string.warning_some_networks_unreachable_message), - messageEffect = TangemMessageEffect.None, - ), - type = WalletNotificationType.Status, - ) - - data class NumberOfSignedHashesIncorrect(val onCloseClick: () -> Unit) : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "NumberOfSignedHashesIncorrectNotification", - title = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_title), - subtitle = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_message), - messageEffect = TangemMessageEffect.Warning, - onCloseClick = onCloseClick, - ), - type = WalletNotificationType.Warning, - ) - - data object TestnetCard : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "TestnetCardNotification", - title = resourceReference(id = R.string.warning_testnet_card_title), - subtitle = resourceReference(id = R.string.warning_testnet_card_message), - messageEffect = TangemMessageEffect.None, - ), - type = WalletNotificationType.Warning, - ) - data class LowSignatures(val count: Int) : WalletNotificationUM( messageUM = TangemMessageUM( id = "LowSignaturesNotification", @@ -177,11 +230,69 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t id = R.string.warning_low_signatures_message, formatArgs = wrappedList(count.toString()), ), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), messageEffect = TangemMessageEffect.None, ), type = WalletNotificationType.Critical, ) + data class FinishWalletActivation( + val messageEffect: TangemMessageEffect, + val isBackupExists: Boolean, + val onClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "FinishWalletActivationNotification", + title = resourceReference(R.string.hw_activation_need_title), + subtitle = if (isBackupExists) { + resourceReference(R.string.hw_activation_need_warning_description) + } else { + resourceReference(R.string.hw_activation_need_description) + }, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.img_knight_shield_32, + tintReference = { + when (messageEffect) { + TangemMessageEffect.Warning -> TangemTheme.colors2.graphic.neutral.primary + else -> TangemTheme.colors2.graphic.status.attention + } + }, + ), + messageEffect = messageEffect, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.hw_activation_need_finish), + type = TangemButtonType.PrimaryInverse, + onClick = onClick, + ), + ), + ), + type = when (messageEffect) { + TangemMessageEffect.Warning -> WalletNotificationType.Critical + else -> WalletNotificationType.Warning + }, + ) + + data class NumberOfSignedHashesIncorrect(val onCloseClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NumberOfSignedHashesIncorrectNotification", + title = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_title), + subtitle = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_message), + messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.img_knight_shield_32, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + onCloseClick = onCloseClick, + ), + type = WalletNotificationType.Critical, + ) + // endregion + + // region Warning data class MissingAddresses( @DrawableRes val tangemIcon: Int?, val missingAddressesCount: Int, @@ -196,12 +307,11 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t formatArgs = wrappedList(missingAddressesCount), ), isCentered = true, - iconUM = tangemIcon?.let { TangemIconUM.Icon(it) }, - messageEffect = TangemMessageEffect.Magic, + messageEffect = TangemMessageEffect.Card, buttonsUM = persistentListOf( TangemMessageButtonUM( text = resourceReference(id = R.string.common_generate_addresses), - type = TangemButtonType.PrimaryInverse, + type = TangemButtonType.Primary, iconRes = tangemIcon, onClick = onGenerateClick, ), @@ -223,33 +333,6 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t type = WalletNotificationType.Warning, ) - data object DemoCard : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "DemoCardNotification", - title = resourceReference(id = R.string.warning_demo_mode_title), - subtitle = resourceReference(id = R.string.warning_demo_mode_message), - messageEffect = TangemMessageEffect.None, - ), - type = WalletNotificationType.Warning, - ) - - data class NoteMigration(val onClick: () -> Unit) : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "NoteMigrationNotification", - title = resourceReference(R.string.wallet_promo_banner_title), - subtitle = resourceReference(R.string.wallet_promo_banner_description), - messageEffect = TangemMessageEffect.None, - buttonsUM = persistentListOf( - TangemMessageButtonUM( - text = resourceReference(R.string.wallet_promo_banner_button_title), - onClick = onClick, - type = TangemButtonType.PrimaryInverse, - ), - ), - ), - type = WalletNotificationType.Promo, - ) - data class UnlockWallets(val onClick: () -> Unit) : WalletNotificationUM( messageUM = TangemMessageUM( id = "UnlockWalletsNotification", @@ -261,12 +344,76 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t ), ), onClick = onClick, - messageEffect = TangemMessageEffect.Magic, + messageEffect = TangemMessageEffect.Card, isCentered = true, ), type = WalletNotificationType.Warning, ) + // endregion + // region Promo + data class NoteMigration(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NoteMigrationNotification", + title = resourceReference(R.string.wallet_promo_banner_title), + subtitle = resourceReference(R.string.wallet_promo_banner_description), + messageEffect = TangemMessageEffect.Magic, + isCentered = true, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.wallet_promo_banner_button_title), + onClick = onClick, + type = TangemButtonType.Primary, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + + data class OnePlusOnePromo( + val onCloseClick: () -> Unit, + val onClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "OnePlusOnePromoNotification", + title = resourceReference(R.string.notification_one_plus_one_title), + subtitle = resourceReference(R.string.notification_one_plus_one_text), + messageEffect = TangemMessageEffect.Magic, + onCloseClick = onCloseClick, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.notification_one_plus_one_button), + type = TangemButtonType.Primary, + onClick = onClick, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + + data class YieldPromo( + val onCloseClick: () -> Unit, + val onTermsAndConditionsClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "YieldPromoNotification", + title = resourceReference(R.string.notification_yield_promo_title), + subtitle = resourceReference(R.string.notification_yield_promo_text), + onCloseClick = onCloseClick, + messageEffect = TangemMessageEffect.Magic, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.notification_yield_promo_button), + type = TangemButtonType.Primary, + onClick = onTermsAndConditionsClick, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + // endregion + + // region Survey data class RateApp( val onLikeClick: () -> Unit, val onDislikeClick: () -> Unit, @@ -294,49 +441,9 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t ), type = WalletNotificationType.Survey, ) + // endregion - data object UsedOutdatedData : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "UsedOutdatedDataNotification", - title = stringReference("Missing some token balances"), // todo redesign main lokalise - subtitle = stringReference("Will be updated as soon as possible"), - messageEffect = TangemMessageEffect.None, - ), - type = WalletNotificationType.Status, - ) - - data class FinishWalletActivation( - val messageEffect: TangemMessageEffect, - val isBackupExists: Boolean, - val onClick: () -> Unit, - ) : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "FinishWalletActivationNotification", - title = resourceReference(R.string.hw_activation_need_title), - subtitle = if (isBackupExists) { - resourceReference(R.string.hw_activation_need_warning_description) - } else { - resourceReference(R.string.hw_activation_need_description) - }, - iconUM = TangemIconUM.Icon( - iconRes = R.drawable.img_knight_shield_32, - tintReference = { TangemTheme.colors2.graphic.status.attention }, - ), - messageEffect = messageEffect, - buttonsUM = persistentListOf( - TangemMessageButtonUM( - text = resourceReference(R.string.hw_activation_need_finish), - type = TangemButtonType.PrimaryInverse, - onClick = onClick, - ), - ), - ), - type = when (messageEffect) { - TangemMessageEffect.Card -> WalletNotificationType.Critical - else -> WalletNotificationType.Warning - }, - ) - + // region Informational data class PushNotifications( val onCloseClick: () -> Unit, val onEnabledClick: () -> Unit, @@ -346,7 +453,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t title = resourceReference(R.string.user_push_notification_banner_title), subtitle = resourceReference(R.string.user_push_notification_banner_subtitle), onCloseClick = onCloseClick, - messageEffect = TangemMessageEffect.Card, + messageEffect = TangemMessageEffect.Magic, buttonsUM = persistentListOf( TangemMessageButtonUM( text = resourceReference(R.string.common_later), @@ -385,45 +492,5 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t ), type = WalletNotificationType.Informational, ) - - data class OnePlusOnePromo( - val onCloseClick: () -> Unit, - val onClick: () -> Unit, - ) : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "OnePlusOnePromoNotification", - title = resourceReference(R.string.notification_one_plus_one_title), - subtitle = resourceReference(R.string.notification_one_plus_one_text), - messageEffect = TangemMessageEffect.Magic, - buttonsUM = persistentListOf( - TangemMessageButtonUM( - text = resourceReference(R.string.notification_one_plus_one_button), - type = TangemButtonType.PrimaryInverse, - onClick = onCloseClick, - ), - ), - ), - type = WalletNotificationType.Promo, - ) - - data class YieldPromo( - val onCloseClick: () -> Unit, - val onTermsAndConditionsClick: () -> Unit, - ) : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "YieldPromoNotification", - title = resourceReference(R.string.notification_yield_promo_title), - subtitle = resourceReference(R.string.notification_yield_promo_text), - onCloseClick = onCloseClick, - messageEffect = TangemMessageEffect.Magic, - buttonsUM = persistentListOf( - TangemMessageButtonUM( - text = resourceReference(R.string.notification_yield_promo_button), - type = TangemButtonType.Primary, - onClick = onTermsAndConditionsClick, - ), - ), - ), - type = WalletNotificationType.Promo, - ) + // endregion } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt index 896c34b9aa..d7b3a5af2d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt @@ -15,6 +15,7 @@ internal sealed interface WalletUM { val buttons: PersistentList val notifications: ImmutableList + val notificationsCarousel: ImmutableList val tokensListUM: WalletTokensListUM @@ -29,11 +30,11 @@ internal sealed interface WalletUM { override val walletsBalanceUM: WalletBalanceUM, override val buttons: PersistentList, override val notifications: ImmutableList, + override val notificationsCarousel: ImmutableList, override val tokensListUM: WalletTokensListUM, override val nftState: WalletNFTItemUM, override val type: WalletType, override val tangemPayState: TangemPayState, - val stackableNotifications: ImmutableList, ) : WalletUM data class Locked( @@ -42,6 +43,7 @@ internal sealed interface WalletUM { override val type: WalletType, override val notifications: ImmutableList = persistentListOf(), ) : WalletUM { + override val notificationsCarousel: ImmutableList = persistentListOf() override val pullToRefreshConfig = PullToRefreshConfig(false, {}) override val tokensListUM: WalletTokensListUM = WalletTokensListUM.Empty // todo redesign main locked state override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt index 3a20a52812..fde5efd497 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt @@ -2,14 +2,18 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import timber.log.Timber internal class SetWarningsTransformer( userWalletId: UserWalletId, private val warnings: ImmutableList, + private val notifications: ImmutableList = persistentListOf(), + private val notificationsCarousel: ImmutableList = persistentListOf(), ) : WalletStateTransformer(userWalletId) { override fun transform(prevState: WalletState): WalletState { @@ -26,6 +30,15 @@ internal class SetWarningsTransformer( } override fun transform(walletUM: WalletUM): WalletUM { - return walletUM // todo redesign main + return when (walletUM) { + is WalletUM.Content -> walletUM.copy( + notifications = notifications, + notificationsCarousel = notificationsCarousel, + ) + is WalletUM.Locked -> { + Timber.w("Impossible to update notifications for locked wallet") + walletUM + } + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt index a8c3a91d43..577da2f275 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt @@ -9,12 +9,9 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.conflate -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* internal class MultiWalletWarningsSubscriber( private val userWallet: UserWallet, @@ -37,7 +34,13 @@ internal class MultiWalletWarningsSubscriber( it.wallets.any { walletState -> walletState.walletCardState.id == userWallet.walletId } } - stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings)) + stateHolder.update( + SetWarningsTransformer( + userWalletId = userWallet.walletId, + warnings = warnings, + notifications = persistentListOf(), + ), + ) walletWarningsAnalyticsSender.send(displayedState, warnings) walletWarningsSingleEventSender.send( userWalletId = userWallet.walletId, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt new file mode 100644 index 0000000000..f7e10a9ecf --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt @@ -0,0 +1,71 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender +import com.tangem.feature.wallet.presentation.wallet.domain.GetWalletNotificationsCarouselFactory +import com.tangem.feature.wallet.presentation.wallet.domain.GetWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* + +@Suppress("LongParameterList") +internal class MultiWalletWarningsSubscriberV2( + private val userWallet: UserWallet, + private val stateHolder: WalletStateController, + private val clickIntents: WalletClickIntents, + private val getWalletWarningsFactory: GetWalletWarningsFactory, + private val getWalletNotificationsCarouselFactory: GetWalletNotificationsCarouselFactory, + private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, + private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, +) : WalletSubscriber() { + + override fun create(coroutineScope: CoroutineScope): Flow> { + return combine( + flow = getWalletWarningsFactory.create(userWallet, clickIntents).conflate().distinctUntilChanged(), + flow2 = getWalletNotificationsCarouselFactory.create(userWallet, clickIntents).conflate() + .distinctUntilChanged(), + ) { notifications, notificationsCarousel -> + val displayedWalletUM = stateHolder.getWalletUM(userWallet.walletId) + + // Wait until the wallet appears in the list + stateHolder.uiState.first { + it.wallets2.any { walletUM -> walletUM.walletsBalanceUM.id == userWallet.walletId } + } + + // If there are notifications, we need to filter out the RateApp notification from stackable notifications, + // because it should not be shown together with other notifications. + val alteredNotificationsCarousel = if (notifications.isNotEmpty()) { + notificationsCarousel.filterNot { it is WalletNotificationUM.RateApp } + } else { + notificationsCarousel + }.toPersistentList() + + stateHolder.update( + SetWarningsTransformer( + userWalletId = userWallet.walletId, + warnings = persistentListOf(), + notifications = notifications, + notificationsCarousel = alteredNotificationsCarousel, + ), + ) + + val totalNotifications = (notifications + alteredNotificationsCarousel).toPersistentList() + + walletWarningsAnalyticsSender.send(displayedWalletUM, totalNotifications) + walletWarningsSingleEventSender.send( + userWalletId = userWallet.walletId, + displayedWalletUM = displayedWalletUM, + newNotifications = totalNotifications, + ) + + totalNotifications + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt index d815a9c21c..349dce0765 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt @@ -8,6 +8,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate @@ -32,7 +33,7 @@ internal class SingleWalletNotificationsSubscriber( .onEach { warnings -> val displayedState = stateHolder.getWalletState(userWallet.walletId) - stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings)) + stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings, persistentListOf())) walletWarningsAnalyticsSender.send(displayedState, warnings) } }