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

@ -8,4 +8,5 @@ package com.tangem.common.ui.notifications
*/
enum class NotificationId(val key: String) {
SendViaSwapTokenSelectorNotification("SendViaSwapTokenSelectorNotificationKey"),
EnablePushesReminderNotification("EnablePushesReminderNotification"),
}

View file

@ -122,16 +122,18 @@ fun ShowcaseButtons(
private fun Showcase_Preview() {
TangemThemePreview {
Showcase(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerIconRes = R.drawable.ic_notification_56,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
R.drawable.ic_rocket_launch_24,
resourceReference(R.string.user_push_notification_agreement_argument_one),
iconRes = R.drawable.ic_rocket_launch_24,
title = resourceReference(R.string.user_push_notification_agreement_argument_one_title),
subTitle = resourceReference(R.string.user_push_notification_agreement_argument_one_subtitle),
),
ShowcaseItemModel(
R.drawable.ic_storefront_24,
resourceReference(R.string.user_push_notification_agreement_argument_two),
iconRes = R.drawable.ic_storefront_24,
title = resourceReference(R.string.user_push_notification_agreement_argument_two_title),
subTitle = resourceReference(R.string.user_push_notification_agreement_argument_two_subtitle),
),
),
primaryButton = ShowcaseButtonModel(resourceReference(R.string.common_allow), {}),

View file

@ -63,7 +63,8 @@ fun ShowcaseContent(
repeat(showcaseItems.size) { index ->
ShowcaseItem(
iconRes = showcaseItems[index].iconRes,
text = showcaseItems[index].text,
title = showcaseItems[index].title,
subtitle = showcaseItems[index].subTitle,
)
}
}

View file

@ -1,33 +1,47 @@
package com.tangem.core.ui.components.showcase
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun ShowcaseItem(@DrawableRes iconRes: Int, text: TextReference) {
Row {
internal fun ShowcaseItem(@DrawableRes iconRes: Int, title: TextReference, subtitle: TextReference) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
Text(
text = text.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = TangemTheme.dimens.spacing20),
)
verticalArrangement = Arrangement.spacedBy(3.dp),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.fillMaxWidth(),
)
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
}
}
}

View file

@ -5,5 +5,6 @@ import com.tangem.core.ui.extensions.TextReference
data class ShowcaseItemModel(
@DrawableRes val iconRes: Int,
val text: TextReference,
val title: TextReference,
val subTitle: TextReference,
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="56dp"
android:height="56dp"
android:viewportWidth="56"
android:viewportHeight="56">
<path
android:pathData="M10.289,26.72C10.784,17.3 18.567,9.917 28,9.917C37.433,9.917 45.216,17.3 45.711,26.72C45.733,27.123 45.748,27.508 45.764,27.879L45.764,27.88C45.815,29.143 45.86,30.243 46.089,31.298C46.363,32.557 46.869,33.548 47.898,34.32C49.694,35.666 50.75,37.779 50.75,40.023C50.75,43.246 48.213,46.083 44.8,46.083H11.2C7.787,46.083 5.25,43.246 5.25,40.023C5.25,37.779 6.306,35.666 8.102,34.32C9.131,33.548 9.637,32.557 9.911,31.298C10.14,30.243 10.185,29.143 10.236,27.88L10.236,27.88C10.252,27.508 10.267,27.123 10.289,26.72Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M24.567,3.207C25.605,2.527 26.873,2.333 28,2.333C29.127,2.333 30.395,2.527 31.433,3.207C32.586,3.961 33.25,5.182 33.25,6.708C33.25,8.099 32.706,9.552 31.879,10.665C31.065,11.76 29.73,12.833 28,12.833C26.27,12.833 24.935,11.76 24.121,10.665C23.294,9.552 22.75,8.099 22.75,6.708C22.75,5.182 23.414,3.961 24.567,3.207Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M21,42C22.288,42 23.333,43.045 23.333,44.333C23.333,46.911 25.423,49 28,49C30.577,49 32.667,46.911 32.667,44.333C32.667,43.045 33.711,42 35,42C36.289,42 37.333,43.045 37.333,44.333C37.333,49.488 33.154,53.667 28,53.667C22.845,53.667 18.667,49.488 18.667,44.333C18.667,43.045 19.711,42 21,42Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="25dp"
android:viewportWidth="24"
android:viewportHeight="25">
<path
android:pathData="M14.75,5.75C14.75,3.541 16.541,1.75 18.75,1.75C20.959,1.75 22.75,3.541 22.75,5.75C22.75,7.959 20.959,9.75 18.75,9.75C16.541,9.75 14.75,7.959 14.75,5.75Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M12.663,2.75C13.279,2.75 13.587,2.75 13.707,2.914C13.827,3.078 13.72,3.412 13.508,4.08C13.34,4.607 13.25,5.168 13.25,5.75C13.25,8.788 15.712,11.25 18.75,11.25C19.339,11.25 19.907,11.157 20.44,10.985C21.091,10.775 21.418,10.671 21.582,10.79C21.746,10.91 21.747,11.209 21.749,11.809C21.75,12.172 21.75,12.55 21.75,12.942V13.058C21.75,15.248 21.75,16.968 21.569,18.312C21.384,19.688 20.997,20.781 20.139,21.639C19.281,22.497 18.188,22.884 16.812,23.069C15.468,23.25 13.748,23.25 11.558,23.25H11.442C9.252,23.25 7.532,23.25 6.188,23.069C4.812,22.884 3.72,22.497 2.861,21.639C2.003,20.781 1.616,19.688 1.431,18.312C1.25,16.968 1.25,15.248 1.25,13.058V12.942C1.25,10.752 1.25,9.032 1.431,7.688C1.616,6.312 2.003,5.22 2.861,4.361C3.72,3.503 4.812,3.116 6.188,2.931C7.532,2.75 9.252,2.75 11.442,2.75H12.663ZM7,15.75C6.586,15.75 6.25,16.086 6.25,16.5C6.25,16.914 6.586,17.25 7,17.25H15C15.414,17.25 15.75,16.914 15.75,16.5C15.75,16.086 15.414,15.75 15,15.75H7ZM7,10.75C6.586,10.75 6.25,11.086 6.25,11.5C6.25,11.914 6.586,12.25 7,12.25H11C11.414,12.25 11.75,11.914 11.75,11.5C11.75,11.086 11.414,10.75 11,10.75H7Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="25dp"
android:viewportWidth="24"
android:viewportHeight="25">
<path
android:pathData="M12,2.75C12.414,2.75 12.75,3.086 12.75,3.5C12.75,5.506 13.761,7.575 15.343,9.157C16.925,10.739 18.994,11.75 21,11.75C21.414,11.75 21.75,12.086 21.75,12.5C21.75,12.914 21.414,13.25 21,13.25C18.994,13.25 16.925,14.261 15.343,15.843C13.761,17.425 12.75,19.494 12.75,21.5C12.75,21.914 12.414,22.25 12,22.25C11.586,22.25 11.25,21.914 11.25,21.5C11.25,19.494 10.239,17.425 8.657,15.843C7.075,14.261 5.006,13.25 3,13.25C2.586,13.25 2.25,12.914 2.25,12.5C2.25,12.086 2.586,11.75 3,11.75C5.006,11.75 7.075,10.739 8.657,9.157C10.239,7.575 11.25,5.506 11.25,3.5C11.25,3.086 11.586,2.75 12,2.75Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M19.25,1.75C19.463,1.75 19.648,1.898 19.695,2.106L19.93,3.146C20.09,3.856 20.644,4.41 21.354,4.57L22.394,4.805C22.602,4.852 22.75,5.037 22.75,5.25C22.75,5.463 22.602,5.648 22.394,5.695L21.354,5.93C20.644,6.09 20.09,6.644 19.93,7.354L19.695,8.394C19.648,8.602 19.463,8.75 19.25,8.75C19.037,8.75 18.852,8.602 18.805,8.394L18.57,7.354C18.41,6.644 17.856,6.09 17.146,5.93L16.106,5.695C15.898,5.648 15.75,5.463 15.75,5.25C15.75,5.037 15.898,4.852 16.106,4.805L17.146,4.57C17.856,4.41 18.41,3.856 18.57,3.146L18.805,2.106C18.852,1.898 19.037,1.75 19.25,1.75Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M4.75,16.25C4.963,16.25 5.148,16.398 5.195,16.606L5.43,17.646C5.59,18.356 6.144,18.91 6.854,19.07L7.894,19.305C8.102,19.352 8.25,19.537 8.25,19.75C8.25,19.963 8.102,20.148 7.894,20.195L6.854,20.43C6.144,20.59 5.59,21.144 5.43,21.854L5.195,22.894C5.148,23.102 4.963,23.25 4.75,23.25C4.537,23.25 4.352,23.102 4.305,22.894L4.07,21.854C3.91,21.144 3.356,20.59 2.646,20.43L1.606,20.195C1.398,20.148 1.25,19.963 1.25,19.75C1.25,19.537 1.398,19.352 1.606,19.305L2.646,19.07C3.356,18.91 3.91,18.356 4.07,17.646L4.305,16.606C4.352,16.398 4.537,16.25 4.75,16.25Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -2,11 +2,13 @@ package com.tangem.data.notifications
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.notifications.repository.NotificationsRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class DefaultNotificationsRepository @Inject constructor(
@ -17,6 +19,10 @@ class DefaultNotificationsRepository @Inject constructor(
return appPreferencesStore.getSyncOrDefault(PreferencesKeys.getShouldShowNotificationKey(key), true)
}
override fun getShouldShowNotification(key: String): Flow<Boolean> {
return appPreferencesStore.get(PreferencesKeys.getShouldShowNotificationKey(key), true)
}
override suspend fun setShouldShowNotifications(key: String, value: Boolean) {
appPreferencesStore.store(PreferencesKeys.getShouldShowNotificationKey(key), value)
}

View file

@ -1,5 +1,7 @@
package com.tangem.domain.notifications.repository
import kotlinx.coroutines.flow.Flow
/**
* Repository interface for managing local notification logic and state.
*
@ -20,6 +22,13 @@ interface NotificationsRepository {
*/
suspend fun shouldShowNotification(key: String): Boolean
/**
* Subscribe on whether a notification with the given [key] should be shown to the user.
* @param key The unique identifier for the notification.
* @return true if the notification should be shown, false otherwise.
*/
fun getShouldShowNotification(key: String): Flow<Boolean>
/**
* Sets whether a notification with the given [key] should be shown to the user.
* @param key The unique identifier for the notification.

View file

@ -53,20 +53,18 @@ internal fun PushNotificationsContent(
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ShowcaseContent(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerIconRes = R.drawable.ic_notification_56,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
R.drawable.ic_rocket_launch_24,
resourceReference(R.string.user_push_notification_agreement_argument_one),
iconRes = R.drawable.ic_notification_square_24,
title = resourceReference(R.string.user_push_notification_agreement_argument_one_title),
subTitle = resourceReference(R.string.user_push_notification_agreement_argument_one_subtitle),
),
ShowcaseItemModel(
R.drawable.ic_storefront_24,
resourceReference(R.string.user_push_notification_agreement_argument_two),
),
ShowcaseItemModel(
R.drawable.ic_notifications_24,
resourceReference(R.string.user_push_notification_agreement_argument_three),
iconRes = R.drawable.ic_stars_24,
title = resourceReference(R.string.user_push_notification_agreement_argument_two_title),
subTitle = resourceReference(R.string.user_push_notification_agreement_argument_two_subtitle),
),
),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing40),

View file

@ -26,20 +26,18 @@ internal fun PushNotificationsScreen(
)
Showcase(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerIconRes = R.drawable.ic_notification_56,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
R.drawable.ic_rocket_launch_24,
resourceReference(R.string.user_push_notification_agreement_argument_one),
iconRes = R.drawable.ic_notification_square_24,
title = resourceReference(R.string.user_push_notification_agreement_argument_one_title),
subTitle = resourceReference(R.string.user_push_notification_agreement_argument_one_subtitle),
),
ShowcaseItemModel(
R.drawable.ic_storefront_24,
resourceReference(R.string.user_push_notification_agreement_argument_two),
),
ShowcaseItemModel(
R.drawable.ic_notifications_24,
resourceReference(R.string.user_push_notification_agreement_argument_three),
iconRes = R.drawable.ic_stars_24,
title = resourceReference(R.string.user_push_notification_agreement_argument_two_title),
subTitle = resourceReference(R.string.user_push_notification_agreement_argument_two_subtitle),
),
),
primaryButton = ShowcaseButtonModel(

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()
}
}