Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-24 10:18:53 +02:00
parent 1a23bd8576
commit 796267c863
23 changed files with 265 additions and 53 deletions

View file

@ -1,18 +1,19 @@
package com.tangem.feature.wallet.child.wallet.model.intents
import android.os.Build
import arrow.core.getOrElse
import com.tangem.common.TangemBlogUrlBuilder
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.notifications.NotificationId
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.card.SetCardWasScannedUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.core.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.models.currency.CryptoCurrency
@ -20,17 +21,21 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.notifications.SetShouldShowNotificationUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.settings.NeverToSuggestRateAppUseCase
import com.tangem.domain.settings.RemindToRateAppLaterUseCase
import com.tangem.domain.settings.repositories.PermissionRepository
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.models.UnlockWalletsError
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
@ -54,6 +59,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("TooManyFunctions")
internal interface WalletWarningsClickIntents {
fun onAddBackupCardClick()
@ -93,9 +99,13 @@ internal interface WalletWarningsClickIntents {
fun onSeedPhraseSecondNotificationReject()
fun onFinishWalletActivationClick()
fun onAllowPermissions()
fun onDenyPermissions()
}
@Suppress("LargeClass", "LongParameterList")
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@ModelScoped
internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val stateHolder: WalletStateController,
@ -121,6 +131,9 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val appRouter: AppRouter,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val userWalletsListRepository: UserWalletsListRepository,
private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase,
private val notificationsRepository: NotificationsRepository,
private val permissionRepository: PermissionRepository,
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
override fun onAddBackupCardClick() {
@ -422,6 +435,39 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
appRouter.push(AppRoute.WalletActivation(userWallet.walletId))
}
override fun onAllowPermissions() {
if (isNotificationsPermissionGranted()) {
updateSubscribeOnPushPermissions(true)
} else {
walletEventSender.send(
event = WalletEvent.RequestPushPermissions(
onAllow = { updateSubscribeOnPushPermissions(true) },
onDeny = { updateSubscribeOnPushPermissions(false) },
),
)
}
}
override fun onDenyPermissions() = updateSubscribeOnPushPermissions(false)
private fun updateSubscribeOnPushPermissions(shouldAllow: Boolean) {
modelScope.launch {
notificationsRepository.setUserAllowToSubscribeOnPushNotifications(shouldAllow)
setShouldShowNotificationUseCase(
key = NotificationId.EnablePushesReminderNotification.key,
value = false,
)
}
}
private fun isNotificationsPermissionGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionRepository.hasRuntimePermission(android.Manifest.permission.POST_NOTIFICATIONS)
} else {
true
}
}
private suspend fun fetchCryptoCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
coroutineScope {
listOf(

View file

@ -4,7 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent.*
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent.Program
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
@ -49,11 +49,11 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
is WalletNotification.Critical.BackupError -> MainScreen.BackupError
is WalletNotification.NoteMigration -> MainScreen.NotePromo
is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
is WalletNotification.SwapPromo -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.Empty, // Use it on new promo action
)
is WalletNotification.Sepa -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
is WalletNotification.Sepa -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.Sepa,
)
@ -70,6 +70,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
-> null
is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport
is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond
is WalletNotification.PushNotifications -> null
}
}
}

View file

@ -3,6 +3,7 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import arrow.core.getOrElse
import com.tangem.common.ui.notifications.NotificationId
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
@ -18,6 +19,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.onramp.GetOnrampCountryUseCase
import com.tangem.domain.onramp.OnrampSepaAvailableUseCase
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
@ -33,16 +35,16 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.coroutines.combine6
import com.tangem.utils.extensions.addIf
import com.tangem.utils.extensions.isPositive
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
@ModelScoped
internal class GetMultiWalletWarningsFactory @Inject constructor(
private val tokenListStore: MultiWalletTokenListStore,
@ -55,20 +57,31 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
private val onrampSepaAvailableUseCase: OnrampSepaAvailableUseCase,
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
private val notificationsRepository: NotificationsRepository,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
) {
@Suppress("UNCHECKED_CAST", "MagicNumber")
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver
return combine6(
flow1 = tokenListStore.getOrThrow(userWallet.walletId),
flow2 = isReadyToShowRateAppUseCase(),
flow3 = isNeedToBackupUseCase(userWallet.walletId),
flow4 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId),
flow5 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Referral),
flow6 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa),
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, seedPhraseIssueStatus, shouldShowReferralPromo, shouldShowSepaBanner ->
return combine(
tokenListStore.getOrThrow(userWallet.walletId),
isReadyToShowRateAppUseCase(),
isNeedToBackupUseCase(userWallet.walletId),
seedPhraseNotificationUseCase(userWalletId = userWallet.walletId),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Referral),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa),
notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key),
) { array ->
val maybeTokenList = array[0] as Lce<TokenListError, TokenList>
val isReadyToShowRating = array[1] as Boolean
val isNeedToBackup = array[2] as Boolean
val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus
val shouldShowReferralPromo = array[4] as Boolean
val shouldShowSepaBanner = array[5] as Boolean
val shouldShowEnablePushesReminderNotification = array[6] as Boolean
buildList {
addUsedOutdatedDataNotification(maybeTokenList)
@ -84,6 +97,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
addWarningNotifications(cardTypesResolver, maybeTokenList, isNeedToBackup, clickIntents)
addPushReminderNotification(
clickIntents = clickIntents,
shouldShowPushReminderBanner = shouldShowEnablePushesReminderNotification &&
!notificationsRepository.isUserAllowToSubscribeOnPushNotifications(),
)
addYieldSupplyNotifications(maybeTokenList)
val hasCriticalOrWarning = any { notification ->
@ -305,6 +324,19 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun MutableList<WalletNotification>.addPushReminderNotification(
clickIntents: WalletClickIntents,
shouldShowPushReminderBanner: Boolean,
) {
addIf(
element = WalletNotification.PushNotifications(
onCloseClick = clickIntents::onDenyPermissions,
onEnabledClick = clickIntents::onAllowPermissions,
),
condition = shouldShowPushReminderBanner,
)
}
private fun Lce<TokenListError, TokenList>.hasUnreachableNetworks(): Boolean {
val tokenList = getOrNull(isPartialContentAccepted = false) ?: return false

View file

@ -30,4 +30,9 @@ internal sealed class WalletEvent {
RIGHT,
}
}
data class RequestPushPermissions(
val onAllow: () -> Unit,
val onDeny: () -> Unit,
) : WalletEvent()
}

View file

@ -315,4 +315,23 @@ sealed class WalletNotification(val config: NotificationConfig) {
iconSize = 54.dp,
),
)
data class PushNotifications(
val onCloseClick: () -> Unit,
val onEnabledClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.user_push_notification_banner_title),
subtitle = resourceReference(R.string.user_push_notification_banner_subtitle),
iconResId = R.drawable.img_push_reminder,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig(
primaryText = resourceReference(id = R.string.common_enable),
onPrimaryClick = onEnabledClick,
secondaryText = resourceReference(id = R.string.common_later),
onSecondaryClick = onCloseClick,
),
iconSize = 54.dp,
),
)
}

View file

@ -3,19 +3,20 @@ package com.tangem.feature.wallet.presentation.wallet.ui
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.utils.requestPermission
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ReviewManagerRequester
import com.tangem.feature.wallet.presentation.wallet.ui.utils.animateScrollByIndex
import com.tangem.feature.wallet.presentation.wallet.ui.utils.demonstrateScrolling
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
@Composable
internal fun WalletEventEffect(
@ -28,6 +29,13 @@ internal fun WalletEventEffect(
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val resources = LocalContext.current.resources
var showPermissionRequest by remember { mutableStateOf<Pair<() -> Unit, () -> Unit>?>(null) }
HandlePermissionRequest(
permissionRequestParams = showPermissionRequest,
onPermissionRequestResult = { showPermissionRequest = null },
)
EventEffect(
event = event,
onTrigger = { value ->
@ -56,7 +64,34 @@ internal fun WalletEventEffect(
is WalletEvent.DemonstrateWalletsScrollPreview -> {
walletsListState.demonstrateScrolling(coroutineScope = coroutineScope, direction = value.direction)
}
is WalletEvent.RequestPushPermissions -> {
showPermissionRequest = value.onAllow to value.onDeny
}
}
},
)
}
@Composable
private fun HandlePermissionRequest(
permissionRequestParams: Pair<() -> Unit, () -> Unit>?,
onPermissionRequestResult: () -> Unit,
) {
val actualPermissionLauncher = permissionRequestParams?.let { (onAllowExternal, onDenyExternal) ->
requestPermission(
onAllow = {
onAllowExternal()
onPermissionRequestResult()
},
onDeny = {
onDenyExternal()
onPermissionRequestResult()
},
permission = PUSH_PERMISSION,
)
}
LaunchedEffect(permissionRequestParams) {
actualPermissionLauncher?.invoke()
}
}