Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-29 15:09:21 +04:00
parent ba32fed132
commit 9ef9542750
30 changed files with 452 additions and 157 deletions

View file

@ -3,10 +3,16 @@ package com.tangem.tap.data
import com.google.firebase.messaging.FirebaseMessaging
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import kotlinx.coroutines.tasks.await
import timber.log.Timber
import javax.inject.Inject
internal class FirebasePushNotificationsTokenProvider @Inject constructor() : PushNotificationsTokenProvider {
override suspend fun getToken(): String {
return FirebaseMessaging.getInstance().token.await()
return try {
FirebaseMessaging.getInstance().token.await()
} catch (ex: Exception) {
Timber.e(ex)
""
}
}
}

View file

@ -22,6 +22,8 @@ import com.tangem.features.nft.component.NFTComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacksStub
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.staking.api.StakingComponent
@ -370,7 +372,9 @@ internal class ChildFactory @Inject constructor(
is AppRoute.PushNotification -> {
createComponentChild(
context = context,
params = Unit,
params = PushNotificationsParams(
modelCallbacks = PushNotificationsModelCallbacksStub(),
),
componentFactory = pushNotificationsComponentFactory,
)
}

View file

@ -134,6 +134,18 @@ object PreferencesKeys {
val NOTIFICATIONS_ENABLED_STATES_KEY by lazy { stringPreferencesKey(name = "notificationsEnabledStates") }
val NOTIFICATIONS_AUTOMATICALLY_ENABLED_STATES_KEY by lazy {
stringPreferencesKey(
name = "notificationsAutomaticallyEnabledStates",
)
}
val NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY by lazy {
booleanPreferencesKey(
name = "userAllowSendAddresses",
)
}
val TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY by lazy {
intPreferencesKey(name = "tronNetworkFeeNotificationShowCount")
}

View file

@ -66,7 +66,7 @@ fun Showcase(
}
@Composable
private fun ShowcaseButtons(
fun ShowcaseButtons(
primaryButtonText: TextReference,
secondaryButtonText: TextReference,
onPrimaryClick: () -> Unit,

View file

@ -82,4 +82,36 @@ internal class DefaultNotificationsRepository @Inject constructor(
NotificationsEligibleNetworkConverter.convert(it)
}
}
override suspend fun shouldShowSubscribeOnNotificationsAfterUpdate(): Boolean {
return appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY,
) == null
}
override suspend fun isUserAllowToSubscribeOnPushNotifications(): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY,
default = false,
)
}
override suspend fun setUserAllowToSubscribeOnPushNotifications(value: Boolean) {
appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY, value)
}
override suspend fun getWalletAutomaticallyEnabledList(): List<String> = appPreferencesStore
.getObjectMapSync<Boolean>(PreferencesKeys.NOTIFICATIONS_AUTOMATICALLY_ENABLED_STATES_KEY).map {
it.key
}
override suspend fun setNotificationsWasEnabledAutomatically(userWalletId: String) {
appPreferencesStore.editData {
it.setObjectMap(
key = PreferencesKeys.NOTIFICATIONS_AUTOMATICALLY_ENABLED_STATES_KEY,
value = it.getObjectMap<Boolean>(PreferencesKeys.NOTIFICATIONS_AUTOMATICALLY_ENABLED_STATES_KEY)
.plus(userWalletId to true),
)
}
}
}

View file

@ -21,4 +21,14 @@ interface NotificationsRepository {
@Throws
suspend fun getEligibleNetworks(): List<NotificationsEligibleNetwork>
suspend fun shouldShowSubscribeOnNotificationsAfterUpdate(): Boolean
suspend fun isUserAllowToSubscribeOnPushNotifications(): Boolean
suspend fun setUserAllowToSubscribeOnPushNotifications(value: Boolean)
suspend fun getWalletAutomaticallyEnabledList(): List<String>
suspend fun setNotificationsWasEnabledAutomatically(userWalletId: String)
}

View file

@ -0,0 +1,27 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import javax.inject.Inject
/**
* Use case for retrieving wallets where automatically enabling push notifications was not applied.
* * This use case filters out wallets that have already had push notifications automatically enabled
* from the complete list of user wallets, returning only those wallets that still need to have
* push notifications automatically enabled.
* * @property userWalletsListManager Manager for user wallets list operations
* @property dispatchers Coroutine dispatcher provider for background operations
*/
class GetWalletsForAutomaticallyPushEnablingUseCase @Inject constructor(
private val userWalletsListManager: UserWalletsListManager,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(walletsListWherePushWasEnabled: List<UserWalletId>): List<UserWalletId> =
withContext(dispatchers.default) {
val allLocalWallets = userWalletsListManager.userWalletsSync.map { it.walletId }
allLocalWallets - walletsListWherePushWasEnabled.toSet()
}
}

View file

@ -37,6 +37,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.card)
implementation(projects.domain.settings)
implementation(projects.domain.notifications)
/** Feature modules */
implementation(projects.features.disclaimer.api)

View file

@ -3,7 +3,7 @@ package com.tangem.features.disclaimer.impl.entity
internal data class DisclaimerUM(
val url: String,
val isTosAccepted: Boolean,
val onAccept: (Boolean) -> Unit,
val onAccept: () -> Unit,
val popBack: () -> Unit,
)

View file

@ -7,6 +7,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.settings.NeverRequestPermissionUseCase
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
@ -26,6 +27,7 @@ internal class DisclaimerModel @Inject constructor(
private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase,
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
private val appFinisher: AppFinisher,
private val notificationsRepository: NotificationsRepository,
paramsContainer: ParamsContainer,
) : Model() {
@ -40,12 +42,12 @@ internal class DisclaimerModel @Inject constructor(
),
)
private fun onAccept(shouldAskPushPermission: Boolean) = modelScope.launch {
private fun onAccept() = modelScope.launch {
if (params.isTosAccepted) {
router.pop()
} else {
cardRepository.acceptTangemTOS()
val shouldAskPushPermission = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate()
if (shouldAskPushPermission) {
router.push(AppRoute.PushNotification)
} else {

View file

@ -15,9 +15,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.web.WebView
import com.google.accompanist.web.WebViewNavigator
import com.google.accompanist.web.rememberWebViewNavigator
@ -39,7 +36,6 @@ import com.tangem.features.disclaimer.impl.R
import com.tangem.features.disclaimer.impl.entity.DisclaimerUM
import com.tangem.features.disclaimer.impl.entity.DummyDisclaimer
import com.tangem.features.disclaimer.impl.local.localTermsOfServices
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.withDebounce
import java.nio.charset.StandardCharsets
@ -167,15 +163,11 @@ private fun WebViewNavigator.loadLocalToS() {
)
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) {
val isPermissionGranted = getPushPermissionOrNull()?.let { permission ->
rememberPermissionState(permission = permission).status.isGranted
} ?: true
private fun BoxScope.DisclaimerButton(onAccept: () -> Unit) {
PrimaryButton(
text = stringResourceSafe(id = R.string.common_accept),
onClick = { onAccept(!isPermissionGranted) },
onClick = onAccept,
colors = ButtonColors(
containerColor = TangemColorPalette.Light4,
contentColor = TangemColorPalette.Dark6,

View file

@ -0,0 +1,9 @@
package com.tangem.features.pushnotifications.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
interface PushNotificationsBottomSheetComponent : ComposableBottomSheetComponent {
interface Factory : ComponentFactory<PushNotificationsParams, PushNotificationsBottomSheetComponent>
}

View file

@ -5,5 +5,5 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
interface PushNotificationsComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Unit, PushNotificationsComponent>
interface Factory : ComponentFactory<PushNotificationsParams, PushNotificationsComponent>
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.pushnotifications.api
interface PushNotificationsModelCallbacks {
fun onAllowSystemPermission()
fun onDenySystemPermission()
fun onDismiss()
}
class PushNotificationsModelCallbacksStub(
val onAllowSystemPermission: () -> Unit = {},
val onDenySystemPermission: () -> Unit = {},
val onDismiss: () -> Unit = {},
) : PushNotificationsModelCallbacks {
override fun onAllowSystemPermission() = onAllowSystemPermission.invoke()
override fun onDenySystemPermission() = onDenySystemPermission.invoke()
override fun onDismiss() = onDismiss.invoke()
}

View file

@ -0,0 +1,6 @@
package com.tangem.features.pushnotifications.api
data class PushNotificationsParams(
val isBottomSheet: Boolean = false,
val modelCallbacks: PushNotificationsModelCallbacks,
)

View file

@ -16,6 +16,8 @@ dependencies {
implementation(deps.androidx.activity.compose)
/** Compose */
implementation(deps.compose.material3)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.foundation)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.accompanist.permission)
@ -39,6 +41,7 @@ dependencies {
/** Domain module */
implementation(projects.domain.settings)
implementation(projects.domain.notifications.toggles)
implementation(projects.domain.notifications)
/** Feature modules */
implementation(projects.features.pushNotifications.api)

View file

@ -0,0 +1,62 @@
package com.tangem.features.pushnotifications.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.pushnotifications.impl.model.PushNotificationsModel
import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsBottomSheet
import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultPushNotificationsBottomSheetComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: PushNotificationsParams,
) : PushNotificationsBottomSheetComponent, AppComponentContext by appComponentContext {
private val model: PushNotificationsModel = getOrCreateModel(params)
@AssistedFactory
interface Factory : PushNotificationsBottomSheetComponent.Factory {
override fun create(
context: AppComponentContext,
params: PushNotificationsParams,
): DefaultPushNotificationsBottomSheetComponent
}
override fun dismiss() {
params.modelCallbacks.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.state.collectAsState()
val bottomSheetConfig = remember(key1 = this) {
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
)
}
PushNotificationsBottomSheet(
config = bottomSheetConfig,
) {
PushNotificationsContent(
onAllowClick = model::onAllowClick,
onLaterClick = model::onLaterClick,
onAllowPermission = model::onAllowPermission,
onDenyPermission = model::onDenyPermission,
showNotificationsInfo = state.showInfoAboutNotifications,
)
}
}
}

View file

@ -11,6 +11,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.utils.findActivity
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.pushnotifications.impl.model.PushNotificationsModel
import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsScreen
import dagger.assisted.Assisted
@ -19,7 +20,7 @@ import dagger.assisted.AssistedInject
internal class DefaultPushNotificationsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
@Assisted private val params: PushNotificationsParams,
) : PushNotificationsComponent, AppComponentContext by appComponentContext {
private val model: PushNotificationsModel = getOrCreateModel(params)
@ -31,8 +32,8 @@ internal class DefaultPushNotificationsComponent @AssistedInject constructor(
BackHandler(onBack = { activity.finish() })
NavigationBar3ButtonsScrim()
PushNotificationsScreen(
onRequest = model::onRequest,
onNeverRequest = model::onNeverRequest,
onAllowClick = model::onAllowClick,
onLaterClick = model::onLaterClick,
onAllowPermission = model::onAllowPermission,
onDenyPermission = model::onDenyPermission,
showNotificationsInfo = state.showInfoAboutNotifications,
@ -41,6 +42,9 @@ internal class DefaultPushNotificationsComponent @AssistedInject constructor(
@AssistedFactory
interface Factory : PushNotificationsComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultPushNotificationsComponent
override fun create(
context: AppComponentContext,
params: PushNotificationsParams,
): DefaultPushNotificationsComponent
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.features.pushnotifications.impl.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.impl.DefaultPushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.impl.DefaultPushNotificationsComponent
import com.tangem.features.pushnotifications.impl.model.PushNotificationsModel
import dagger.Binds
@ -15,6 +17,11 @@ import dagger.multibindings.IntoMap
@InstallIn(SingletonComponent::class)
internal interface PushNotificationsModule {
@Binds
fun bindBottomSheetComponentFactory(
impl: DefaultPushNotificationsBottomSheetComponent.Factory,
): PushNotificationsBottomSheetComponent.Factory
@Binds
fun bindComponentFactory(impl: DefaultPushNotificationsComponent.Factory): PushNotificationsComponent.Factory

View file

@ -1,9 +1,9 @@
package com.tangem.features.pushnotifications.impl.model
internal interface PushNotificationsClickIntents {
fun onRequest()
fun onAllowClick()
fun onNeverRequest()
fun onLaterClick()
fun onAllowPermission()

View file

@ -7,9 +7,12 @@ 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.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.domain.settings.NeverRequestPermissionUseCase
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.impl.presentation.state.PushNotificationsUM
@ -19,17 +22,22 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class PushNotificationsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase,
private val appRouter: AppRouter,
private val analyticHandler: AnalyticsEventHandler,
private val notificationsFeatureToggles: NotificationsFeatureToggles,
private val notificationsRepository: NotificationsRepository,
) : Model(), PushNotificationsClickIntents {
val params: PushNotificationsParams = paramsContainer.require()
private val _state = MutableStateFlow(
PushNotificationsUM(
showInfoAboutNotifications = notificationsFeatureToggles.isNotificationsEnabled,
@ -38,20 +46,33 @@ internal class PushNotificationsModel @Inject constructor(
val state = _state.asStateFlow()
override fun onRequest() {
override fun onAllowClick() {
if (notificationsFeatureToggles.isNotificationsEnabled) {
modelScope.launch {
notificationsRepository.setUserAllowToSubscribeOnPushNotifications(true)
}
}
analyticHandler.send(
PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Stories),
)
}
override fun onNeverRequest() {
override fun onLaterClick() {
if (notificationsFeatureToggles.isNotificationsEnabled) {
modelScope.launch {
notificationsRepository.setUserAllowToSubscribeOnPushNotifications(false)
}
}
analyticHandler.send(
PushNotificationAnalyticEvents.ButtonLater(AnalyticsParam.ScreensSources.Stories),
)
modelScope.launch {
neverRequestPermissionUseCase(PUSH_PERMISSION)
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
appRouter.push(AppRoute.Home)
params.modelCallbacks.onDenySystemPermission()
if (!params.isBottomSheet) {
appRouter.push(AppRoute.Home)
}
}
}
@ -62,7 +83,10 @@ internal class PushNotificationsModel @Inject constructor(
modelScope.launch {
neverRequestPermissionUseCase(PUSH_PERMISSION)
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
appRouter.push(AppRoute.Home)
params.modelCallbacks.onAllowSystemPermission()
if (!params.isBottomSheet) {
appRouter.push(AppRoute.Home)
}
}
}
@ -73,7 +97,10 @@ internal class PushNotificationsModel @Inject constructor(
modelScope.launch {
neverRequestPermissionUseCase(PUSH_PERMISSION)
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
appRouter.push(AppRoute.Home)
params.modelCallbacks.onDenySystemPermission()
if (!params.isBottomSheet) {
appRouter.push(AppRoute.Home)
}
}
}
}

View file

@ -0,0 +1,118 @@
package com.tangem.features.pushnotifications.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH28
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.showcase.ShowcaseButtons
import com.tangem.core.ui.components.showcase.ShowcaseContent
import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.requestPushPermission
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PushNotificationsBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) {
TangemModalBottomSheet<TangemBottomSheetConfigContent>(
config = config,
title = {
TangemModalBottomSheetTitle(
endIconRes = R.drawable.ic_close_24,
onEndClick = config.onDismissRequest,
)
},
) {
content()
}
}
@Composable
internal fun PushNotificationsContent(
onAllowClick: () -> Unit,
onLaterClick: () -> Unit,
onAllowPermission: () -> Unit,
onDenyPermission: () -> Unit,
showNotificationsInfo: Boolean,
) {
val requestPushPermission = requestPushPermission(
onAllow = onAllowPermission,
onDeny = onDenyPermission,
pushPermission = getPushPermissionOrNull(),
)
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ShowcaseContent(
headerIconRes = R.drawable.ic_notifications_unread_24,
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),
),
ShowcaseItemModel(
R.drawable.ic_storefront_24,
resourceReference(R.string.user_push_notification_agreement_argument_two),
),
).let { baseItems ->
if (showNotificationsInfo) {
baseItems.add(
ShowcaseItemModel(
R.drawable.ic_notifications_24,
resourceReference(R.string.user_push_notification_agreement_argument_three),
),
)
} else {
baseItems
}
},
modifier = Modifier.padding(top = TangemTheme.dimens.spacing40),
)
SpacerH28()
ShowcaseButtons(
primaryButtonText = resourceReference(R.string.common_allow),
onPrimaryClick = {
onAllowClick()
requestPushPermission()
},
secondaryButtonText = resourceReference(R.string.common_later),
onSecondaryClick = {
onLaterClick()
},
)
}
}
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_PushNotificationsBottomSheet() {
TangemThemePreview {
PushNotificationsBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
) {
PushNotificationsContent(
onAllowClick = {},
onLaterClick = {},
onAllowPermission = {},
onDenyPermission = {},
showNotificationsInfo = true,
)
}
}
}

View file

@ -14,8 +14,8 @@ import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PushNotificationsScreen(
onRequest: () -> Unit,
onNeverRequest: () -> Unit,
onAllowClick: () -> Unit,
onLaterClick: () -> Unit,
onAllowPermission: () -> Unit,
onDenyPermission: () -> Unit,
showNotificationsInfo: Boolean,
@ -53,13 +53,15 @@ internal fun PushNotificationsScreen(
primaryButton = ShowcaseButtonModel(
buttonText = resourceReference(R.string.common_allow),
onClick = {
onRequest()
onAllowClick()
requestPushPermission()
},
),
secondaryButton = ShowcaseButtonModel(
buttonText = resourceReference(R.string.common_later),
onClick = onNeverRequest,
onClick = {
onLaterClick()
},
),
modifier = Modifier.systemBarsPadding(),
)

View file

@ -24,6 +24,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.nft.DisableWalletNFTUseCase
import com.tangem.domain.nft.EnableWalletNFTUseCase
import com.tangem.domain.nft.GetWalletNFTEnabledUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.domain.settings.repositories.PermissionRepository
import com.tangem.domain.wallets.models.UserWallet
@ -72,6 +73,7 @@ internal class WalletSettingsModel @Inject constructor(
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
private val settingsManager: SettingsManager,
private val permissionsRepository: PermissionRepository,
private val notificationsRepository: NotificationsRepository,
) : Model() {
val params: WalletSettingsComponent.Params = paramsContainer.require()
@ -258,6 +260,7 @@ internal class WalletSettingsModel @Inject constructor(
if (isGranted) {
modelScope.launch {
setNotificationsEnabledUseCase(params.userWalletId, true).onRight {
notificationsRepository.setNotificationsWasEnabledAutomatically(params.userWalletId.stringValue)
analyticsEventHandler.send(PushNotificationAnalyticEvents.NotificationsEnabled(true))
}
}

View file

@ -96,6 +96,8 @@ dependencies {
implementation(projects.domain.visa)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.notifications)
implementation(projects.domain.notifications.toggles)
/** Feature Apis */
implementation(projects.features.details.api)

View file

@ -21,6 +21,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
import com.tangem.feature.walletsettings.component.RenameWalletComponent
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.markets.entry.MarketsEntryComponent
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -32,6 +34,7 @@ internal class WalletComponent @AssistedInject constructor(
private val renameWalletComponentFactory: RenameWalletComponent.Factory,
private val marketsEntryComponentFactory: MarketsEntryComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: WalletModel = getOrCreateModel()
@ -66,6 +69,13 @@ internal class WalletComponent @AssistedInject constructor(
),
)
}
WalletDialogConfig.AskForPushNotifications -> pushNotificationsBottomSheetComponent.create(
context = childByContext(componentContext),
params = PushNotificationsParams(
isBottomSheet = true,
modelCallbacks = model.askForPushNotificationsModelCallbacks,
),
)
}
},
)

View file

@ -17,6 +17,8 @@ import com.tangem.core.deeplink.global.ReferralDeepLink
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.domain.settings.*
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
@ -25,6 +27,8 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.usecase.GetWalletsForAutomaticallyPushEnablingUseCase
import com.tangem.domain.wallets.usecase.SetNotificationsEnabledUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
@ -36,7 +40,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction
@ -45,6 +48,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.utils.Provider
@ -86,11 +90,16 @@ internal class WalletModel @Inject constructor(
private val appRouter: AppRouter,
private val routingFeatureToggle: RoutingFeatureToggle,
private val observeAndClearNFTCacheIfNeedUseCase: ObserveAndClearNFTCacheIfNeedUseCase,
private val notificationsRepository: NotificationsRepository,
private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase,
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
private val notificationsFeatureToggles: NotificationsFeatureToggles,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
) : Model() {
val askBiometryModelCallbacks = AskBiometryModelCallbacks()
val askForPushNotificationsModelCallbacks = AskForPushNotificationsCallbacks()
val uiState: StateFlow<WalletScreenState> = stateHolder.uiState
private val walletsUpdateJobHolder = JobHolder()
@ -113,6 +122,7 @@ internal class WalletModel @Inject constructor(
subscribeOnSelectedWalletFlow()
subscribeToScreenBackgroundState()
subscribeOnPushNotificationsPermission()
enableNotificationsIfNeeded()
clickIntents.initialize(innerWalletRouter, modelScope)
}
@ -196,20 +206,20 @@ internal class WalletModel @Inject constructor(
private fun subscribeOnPushNotificationsPermission() {
modelScope.launch {
val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION)
val isPushPermissionAvailable = getPushPermissionOrNull() != null
if (!shouldRequestPush || !isPushPermissionAvailable) return@launch
val shouldAskPermission = shouldAskPermissionUseCase(PUSH_PERMISSION)
val afterUpdate = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate()
val shouldShowBottomSheet = if (notificationsFeatureToggles.isNotificationsEnabled) {
(shouldAskPermission || afterUpdate) && !shouldShowSaveWalletScreenUseCase()
} else {
val isPushPermissionAvailable = getPushPermissionOrNull() != null
shouldAskPermission && isPushPermissionAvailable
}
if (!shouldShowBottomSheet) return@launch
delay(timeMillis = 1_800)
stateHolder.showBottomSheet(
content = PushNotificationsBottomSheetConfig(
onRequest = clickIntents::onRequestPushPermission,
onNeverRequest = { clickIntents.onNeverAskPushPermission(false) },
onAllow = clickIntents::onAllowPushPermission,
onDeny = clickIntents::onDenyPushPermission,
),
onDismiss = { clickIntents.onNeverAskPushPermission(true) },
innerWalletRouter.dialogNavigation.activate(
configuration = WalletDialogConfig.AskForPushNotifications,
)
}
}
@ -433,6 +443,8 @@ internal class WalletModel @Inject constructor(
it.copy(selectedWalletIndex = action.selectedWalletIndex)
}
}
enableNotificationsIfNeeded()
}
private fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) {
@ -526,6 +538,26 @@ internal class WalletModel @Inject constructor(
}
}
private fun enableNotificationsIfNeeded() {
if (!notificationsFeatureToggles.isNotificationsEnabled) return
modelScope.launch {
val isUserAllowToEnableNotifications = notificationsRepository.isUserAllowToSubscribeOnPushNotifications()
if (isUserAllowToEnableNotifications) {
val alreadyEnabledWallets = notificationsRepository.getWalletAutomaticallyEnabledList().map {
UserWalletId(it)
}
val walletsListWhichShouldBeEnabled = getWalletsListForEnablingUseCase(alreadyEnabledWallets)
walletsListWhichShouldBeEnabled.forEach { userWalletId ->
setNotificationsEnabledUseCase(userWalletId, true).onRight {
notificationsRepository.setNotificationsWasEnabledAutomatically(userWalletId.stringValue)
}.onLeft {
Timber.e(it)
}
}
}
}
}
inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks {
override fun onAllowed() {
analyticsEventsHandler.send(MainScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On))
@ -538,6 +570,23 @@ internal class WalletModel @Inject constructor(
}
}
inner class AskForPushNotificationsCallbacks : PushNotificationsModelCallbacks {
override fun onAllowSystemPermission() {
innerWalletRouter.dialogNavigation.dismiss()
enableNotificationsIfNeeded()
}
override fun onDenySystemPermission() {
innerWalletRouter.dialogNavigation.dismiss()
enableNotificationsIfNeeded()
}
override fun onDismiss() {
innerWalletRouter.dialogNavigation.dismiss()
}
}
private companion object {
const val REFRESH_WALLET_BACKGROUND_TIMER_MILLIS = 10000L
const val EXPRESS_STATUS_UPDATE_DELAY = 10000L

View file

@ -16,4 +16,7 @@ internal sealed interface WalletDialogConfig {
@Serializable
data object AskForBiometry : WalletDialogConfig
@Serializable
data object AskForPushNotifications : WalletDialogConfig
}

View file

@ -74,7 +74,6 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder
import com.tangem.feature.wallet.presentation.wallet.ui.components.PushNotificationsBottomSheet
import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList
import com.tangem.feature.wallet.presentation.wallet.ui.components.common.*
@ -706,7 +705,6 @@ private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig)
is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig)
is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig)
is PushNotificationsBottomSheetConfig -> PushNotificationsBottomSheet(config = bottomSheetConfig)
is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = bottomSheetConfig)
}
}

View file

@ -1,111 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.showcase.ShowcaseContent
import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.requestPushPermission
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PushNotificationsBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<PushNotificationsBottomSheetConfig>(config = config) {
PushNotificationsSheetContent(content = it, onDismiss = config.onDismissRequest)
}
}
@Composable
private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetConfig, onDismiss: () -> Unit) {
val requestPushPermission = requestPushPermission(
pushPermission = getPushPermissionOrNull(),
onAllow = {
content.onAllow()
onDismiss()
},
onDeny = {
content.onDeny()
onDismiss()
},
)
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ShowcaseContent(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
iconRes = R.drawable.ic_rocket_launch_24,
text = resourceReference(R.string.user_push_notification_agreement_argument_one),
),
ShowcaseItemModel(
iconRes = R.drawable.ic_storefront_24,
text = resourceReference(R.string.user_push_notification_agreement_argument_two),
),
),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing40),
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing40,
bottom = TangemTheme.dimens.spacing16,
),
) {
SecondaryButton(
text = stringResourceSafe(R.string.common_later),
onClick = {
content.onNeverRequest()
onDismiss()
},
modifier = Modifier.weight(1f),
)
PrimaryButton(
text = stringResourceSafe(R.string.common_allow),
onClick = {
content.onRequest()
requestPushPermission()
},
modifier = Modifier.weight(1f),
)
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PushNotificationsSheetContent_Preview() {
TangemThemePreview {
PushNotificationsSheetContent(
PushNotificationsBottomSheetConfig(
onRequest = {},
onNeverRequest = {},
onAllow = {},
onDeny = {},
),
onDismiss = {},
)
}
}
// endregion