diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
index 5ef88d9866..6400bc4ab8 100644
--- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
+++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
@@ -32,6 +32,7 @@ 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.pushnotificationsettings.component.PushNotificationSettingsComponent
import com.tangem.features.send.api.NFTSendComponent
import com.tangem.features.send.api.SendComponent
import com.tangem.features.send.api.SendEntryPointComponent
@@ -87,6 +88,7 @@ internal class ChildFactory @Inject constructor(
private val resetCardComponentFactory: ResetCardComponent.Factory,
private val referralComponentFactory: ReferralComponent.Factory,
private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory,
+ private val pushNotificationSettingsComponentFactory: PushNotificationSettingsComponent.Factory,
private val walletComponentFactory: WalletEntryComponent.Factory,
private val sendComponentFactoryV2: SendComponent.Factory,
private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory,
@@ -172,6 +174,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletSettingsComponentFactory,
)
}
+ is AppRoute.PushNotificationSettings -> {
+ createComponentChild(
+ context = context,
+ params = PushNotificationSettingsComponent.Params(route.userWalletId),
+ componentFactory = pushNotificationSettingsComponentFactory,
+ )
+ }
is AppRoute.WalletBackup -> {
createComponentChild(
context = context,
diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
index 73d0e6bebb..5ae1808b7e 100644
--- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
+++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
@@ -257,6 +257,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_settings/${userWalletId.stringValue}")
+ @Serializable
+ data class PushNotificationSettings(
+ val userWalletId: UserWalletId,
+ ) : AppRoute(path = "/push_notification_settings/${userWalletId.stringValue}")
+
@Serializable
data class WalletBackup(
val userWalletId: UserWalletId,
diff --git a/core/ui/src/main/res/drawable/ic_push_notification_settings_24.xml b/core/ui/src/main/res/drawable/ic_push_notification_settings_24.xml
new file mode 100644
index 0000000000..8cbc8e1bcf
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_push_notification_settings_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/features/push-notification-settings/impl/build.gradle.kts b/features/push-notification-settings/impl/build.gradle.kts
index bed7c654b6..b69c5cd2a8 100644
--- a/features/push-notification-settings/impl/build.gradle.kts
+++ b/features/push-notification-settings/impl/build.gradle.kts
@@ -31,6 +31,7 @@ dependencies {
implementation(projects.domain.pushNotificationPreferences)
/* AndroidX */
+ implementation(deps.androidx.activity.compose)
implementation(deps.lifecycle.compose)
/* Compose */
@@ -50,7 +51,7 @@ dependencies {
implementation(deps.kotlin.immutable.collections)
/* Tests */
- testImplementation(deps.test.junit)
+ testImplementation(deps.test.junit5)
testImplementation(deps.test.mockk)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/component/DefaultPushNotificationSettingsComponent.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/component/DefaultPushNotificationSettingsComponent.kt
new file mode 100644
index 0000000000..491b9dcb17
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/component/DefaultPushNotificationSettingsComponent.kt
@@ -0,0 +1,96 @@
+package com.tangem.features.pushnotificationsettings.impl.component
+
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.arkivanov.decompose.ComponentContext
+import com.arkivanov.decompose.extensions.compose.subscribeAsState
+import com.arkivanov.decompose.router.slot.childSlot
+import com.arkivanov.decompose.router.slot.dismiss
+import com.arkivanov.essenty.lifecycle.doOnResume
+import com.tangem.core.decompose.context.AppComponentContext
+import com.tangem.core.decompose.context.childByContext
+import com.tangem.core.decompose.model.getOrCreateModel
+import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
+import com.tangem.feature.walletsettings.component.NetworksAvailableForNotificationsComponent
+import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
+import com.tangem.features.pushnotificationsettings.component.PushNotificationSettingsComponent
+import com.tangem.features.pushnotificationsettings.impl.entity.NetworksAvailableForNotificationBSConfig
+import com.tangem.features.pushnotificationsettings.impl.model.PushNotificationSettingsModel
+import com.tangem.features.pushnotificationsettings.impl.ui.PushNotificationSettingsScreen
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+internal class DefaultPushNotificationSettingsComponent @AssistedInject constructor(
+ @Assisted context: AppComponentContext,
+ @Assisted private val params: PushNotificationSettingsComponent.Params,
+ private val networksAvailableForNotificationsComponentFactory: NetworksAvailableForNotificationsComponent.Factory,
+) : PushNotificationSettingsComponent, AppComponentContext by context {
+
+ private val model: PushNotificationSettingsModel = getOrCreateModel(params)
+
+ private val bottomSheetSlot = childSlot(
+ source = model.bottomSheetNavigation,
+ serializer = null,
+ handleBackButton = false,
+ key = "moreInfoBottomSheet",
+ childFactory = ::bottomSheetChild,
+ )
+
+ init {
+ lifecycle.doOnResume { model.onResume() }
+ }
+
+ @Composable
+ override fun Content(modifier: Modifier) {
+ val state by model.uiState.collectAsStateWithLifecycle()
+ val bottomSheet by bottomSheetSlot.subscribeAsState()
+
+ val permissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission(),
+ onResult = model::onPermissionResult,
+ )
+
+ LaunchedEffect(Unit) {
+ model.requestPushPermission.collect {
+ val permission = getPushPermissionOrNull()
+ if (permission != null) {
+ permissionLauncher.launch(permission)
+ } else {
+ model.onPermissionResult(isGranted = false)
+ }
+ }
+ }
+
+ PushNotificationSettingsScreen(
+ modifier = modifier,
+ state = state,
+ onBackClick = router::pop,
+ )
+
+ bottomSheet.child?.instance?.BottomSheet()
+ }
+
+ private fun bottomSheetChild(
+ @Suppress("UNUSED_PARAMETER") config: NetworksAvailableForNotificationBSConfig,
+ componentContext: ComponentContext,
+ ): ComposableBottomSheetComponent = networksAvailableForNotificationsComponentFactory.create(
+ context = childByContext(componentContext),
+ params = NetworksAvailableForNotificationsComponent.Params(
+ onDismiss = model.bottomSheetNavigation::dismiss,
+ ),
+ )
+
+ @AssistedFactory
+ interface Factory : PushNotificationSettingsComponent.Factory {
+ override fun create(
+ context: AppComponentContext,
+ params: PushNotificationSettingsComponent.Params,
+ ): DefaultPushNotificationSettingsComponent
+ }
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/di/PushNotificationSettingsComponentModule.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/di/PushNotificationSettingsComponentModule.kt
new file mode 100644
index 0000000000..b6ecab63ea
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/di/PushNotificationSettingsComponentModule.kt
@@ -0,0 +1,20 @@
+package com.tangem.features.pushnotificationsettings.impl.di
+
+import com.tangem.features.pushnotificationsettings.component.PushNotificationSettingsComponent
+import com.tangem.features.pushnotificationsettings.impl.component.DefaultPushNotificationSettingsComponent
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal interface PushNotificationSettingsComponentModule {
+
+ @Binds
+ @Singleton
+ fun bindPushNotificationSettingsComponentFactory(
+ factory: DefaultPushNotificationSettingsComponent.Factory,
+ ): PushNotificationSettingsComponent.Factory
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/PushNotificationSettingsUM.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/PushNotificationSettingsUM.kt
index f31df5f7cf..b24b6ed544 100644
--- a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/PushNotificationSettingsUM.kt
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/PushNotificationSettingsUM.kt
@@ -1,7 +1,6 @@
package com.tangem.features.pushnotificationsettings.impl.entity
import androidx.compose.runtime.Immutable
-import com.tangem.core.ui.event.StateEvent
import kotlinx.collections.immutable.PersistentList
@Immutable
@@ -12,7 +11,6 @@ internal sealed interface PushNotificationSettingsUM {
data class Content(
val banner: AllowPushNotificationsBannerUM?,
val toggles: PersistentList,
- val requestPermissionEvent: StateEvent,
val onMoreInfoClick: () -> Unit,
) : PushNotificationSettingsUM
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt
index f27b886fbb..cbc841a27c 100644
--- a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt
@@ -9,9 +9,6 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.notifications.SystemNotificationsStateProvider
import com.tangem.core.navigation.settings.SettingsManager
-import com.tangem.core.ui.event.StateEvent
-import com.tangem.core.ui.event.consumedEvent
-import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
@@ -38,6 +35,8 @@ import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.logging.TangemLogger
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -45,6 +44,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -69,7 +69,6 @@ internal class PushNotificationSettingsModel @Inject constructor(
private val loadState = MutableStateFlow(LoadState.Loading)
private val osNotificationsEnabled = MutableStateFlow(systemNotificationsStateProvider.areNotificationsEnabled())
- private val pendingRequest = MutableStateFlow>(consumedEvent())
private var pendingPermissionToggle: ToggleSpec? = null
private val preferencesJobHolder = JobHolder()
@@ -77,15 +76,19 @@ internal class PushNotificationSettingsModel @Inject constructor(
private val cachedPrefs: WalletPushNotificationPreferences?
get() = (loadState.value as? LoadState.Content)?.prefs
+ private val requestPushPermissionChannel = Channel(Channel.BUFFERED)
+
+ /** One-shot requests to launch the system push permission prompt, consumed by the component. */
+ val requestPushPermission: Flow = requestPushPermissionChannel.receiveAsFlow()
+
val uiState: StateFlow = combine(
loadState,
osNotificationsEnabled,
- pendingRequest,
- ) { load, osEnabled, request ->
+ ) { load, osEnabled ->
when (load) {
is LoadState.Failed -> PushNotificationSettingsUM.Error(onRetryClick = ::onRetry)
is LoadState.Loading -> PushNotificationSettingsUM.Loading
- is LoadState.Content -> buildContent(prefs = load.prefs, osEnabled = osEnabled, request = request)
+ is LoadState.Content -> buildContent(prefs = load.prefs, osEnabled = osEnabled)
}
}.stateIn(
scope = modelScope,
@@ -117,7 +120,6 @@ internal class PushNotificationSettingsModel @Inject constructor(
}
fun onPermissionResult(isGranted: Boolean) {
- pendingRequest.value = consumedEvent()
val tapped = pendingPermissionToggle
pendingPermissionToggle = null
modelScope.launch {
@@ -145,12 +147,10 @@ internal class PushNotificationSettingsModel @Inject constructor(
private fun buildContent(
prefs: WalletPushNotificationPreferences,
osEnabled: Boolean,
- request: StateEvent,
): PushNotificationSettingsUM.Content {
return PushNotificationSettingsUM.Content(
banner = buildBanner(prefs = prefs, osEnabled = osEnabled),
toggles = buildToggles(prefs),
- requestPermissionEvent = request,
onMoreInfoClick = ::onMoreInfoClick,
)
}
@@ -190,7 +190,7 @@ internal class PushNotificationSettingsModel @Inject constructor(
private fun requestPermission(tapped: ToggleSpec? = null) {
pendingPermissionToggle = tapped
- pendingRequest.value = triggeredEvent(data = Unit, onConsume = ::onPermissionEventConsumed)
+ requestPushPermissionChannel.trySend(Unit)
}
private fun onBannerCtaClick() {
@@ -219,10 +219,6 @@ internal class PushNotificationSettingsModel @Inject constructor(
applyOptimisticToggle(spec, newValue)
}
- private fun onPermissionEventConsumed() {
- pendingRequest.value = consumedEvent()
- }
-
private fun applyOptimisticToggle(spec: ToggleSpec, newValue: Boolean) {
val current = cachedPrefs ?: return
loadState.value = LoadState.Content(current.withCategory(spec.category, newValue))
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/AllowPushNotificationsBanner.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/AllowPushNotificationsBanner.kt
new file mode 100644
index 0000000000..da20ec497a
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/AllowPushNotificationsBanner.kt
@@ -0,0 +1,28 @@
+package com.tangem.features.pushnotificationsettings.impl.ui
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.tangem.core.ui.R as CoreUiR
+import com.tangem.core.ui.components.notifications.Notification
+import com.tangem.core.ui.components.notifications.NotificationConfig
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.features.pushnotificationsettings.impl.R
+import com.tangem.features.pushnotificationsettings.impl.entity.AllowPushNotificationsBannerUM
+
+@Composable
+internal fun AllowPushNotificationsBanner(state: AllowPushNotificationsBannerUM, modifier: Modifier = Modifier) {
+ Notification(
+ modifier = modifier.fillMaxWidth(),
+ config = NotificationConfig(
+ title = resourceReference(R.string.push_notification_settings_banner_title),
+ subtitle = resourceReference(R.string.push_notification_settings_banner_description),
+ iconResId = CoreUiR.drawable.ic_alert_circle_24,
+ iconTint = NotificationConfig.IconTint.Warning,
+ buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
+ text = resourceReference(R.string.common_open_settings_button_title),
+ onClick = state.onOpenSettingsClick,
+ ),
+ ),
+ )
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/NotificationSettingRow.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/NotificationSettingRow.kt
new file mode 100644
index 0000000000..751b904642
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/NotificationSettingRow.kt
@@ -0,0 +1,92 @@
+package com.tangem.features.pushnotificationsettings.impl.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.tangem.core.ui.components.TangemSwitch
+import com.tangem.core.ui.components.block.BlockCard
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.combinedReference
+import com.tangem.core.ui.extensions.resolveAnnotatedReference
+import com.tangem.core.ui.extensions.stringReference
+import com.tangem.core.ui.extensions.stringResourceSafe
+import com.tangem.core.ui.extensions.styledResourceReference
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.pushnotificationsettings.impl.R
+import com.tangem.features.pushnotificationsettings.impl.entity.ToggleUM
+
+@Composable
+internal fun NotificationSettingRow(
+ toggle: ToggleUM,
+ showInlineMoreInfoLink: Boolean,
+ onMoreInfoClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
+ ) {
+ BlockCard(modifier = Modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = TangemTheme.dimens.spacing16,
+ vertical = TangemTheme.dimens.spacing12,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
+ ) {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = stringResourceSafe(id = toggle.titleRes),
+ style = TangemTheme.typography.subtitle1,
+ color = TangemTheme.colors.text.primary1,
+ )
+ TangemSwitch(
+ checked = toggle.isOn,
+ onCheckedChange = toggle.onCheckedChange,
+ )
+ }
+ }
+
+ SubtitleText(
+ base = toggle.subtitle,
+ showMoreInfo = showInlineMoreInfoLink,
+ onMoreInfoClick = onMoreInfoClick,
+ )
+ }
+}
+
+@Composable
+private fun SubtitleText(base: TextReference, showMoreInfo: Boolean, onMoreInfoClick: () -> Unit) {
+ val reference: TextReference = if (showMoreInfo) {
+ combinedReference(
+ base,
+ stringReference(" "),
+ styledResourceReference(
+ id = R.string.push_notifications_more_info,
+ spanStyleReference = {
+ TangemTheme.typography.caption2
+ .copy(color = TangemTheme.colors.text.accent)
+ .toSpanStyle()
+ },
+ onClick = onMoreInfoClick,
+ ),
+ )
+ } else {
+ base
+ }
+ Text(
+ modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
+ text = reference.resolveAnnotatedReference(),
+ style = TangemTheme.typography.caption2,
+ color = TangemTheme.colors.text.tertiary,
+ )
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsContent.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsContent.kt
new file mode 100644
index 0000000000..391208437f
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsContent.kt
@@ -0,0 +1,68 @@
+package com.tangem.features.pushnotificationsettings.impl.ui
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.pushnotificationsettings.impl.entity.AllowPushNotificationsBannerUM
+import com.tangem.features.pushnotificationsettings.impl.entity.PushNotificationSettingsUM
+import com.tangem.features.pushnotificationsettings.impl.entity.ToggleId
+
+@Composable
+internal fun PushNotificationSettingsContent(
+ state: PushNotificationSettingsUM.Content,
+ modifier: Modifier = Modifier,
+) {
+ LazyColumn(
+ modifier = modifier
+ .fillMaxSize()
+ .navigationBarsPadding(),
+ contentPadding = PaddingValues(
+ horizontal = TangemTheme.dimens.spacing16,
+ vertical = TangemTheme.dimens.spacing12,
+ ),
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
+ ) {
+ item(key = "banner") {
+ AnimatedBannerSlot(banner = state.banner)
+ }
+
+ items(items = state.toggles, key = { it.id.name }) { toggle ->
+ NotificationSettingRow(
+ modifier = Modifier.animateItem(),
+ toggle = toggle,
+ showInlineMoreInfoLink = toggle.id == ToggleId.TransactionAlerts,
+ onMoreInfoClick = state.onMoreInfoClick,
+ )
+ }
+ }
+}
+
+@Composable
+private fun AnimatedBannerSlot(banner: AllowPushNotificationsBannerUM?) {
+ var lastVisible by remember { mutableStateOf(banner) }
+ if (banner != null) {
+ lastVisible = banner
+ }
+ AnimatedVisibility(
+ visible = banner != null,
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically(),
+ ) {
+ lastVisible?.let { AllowPushNotificationsBanner(state = it) }
+ }
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsError.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsError.kt
new file mode 100644
index 0000000000..46c152f6f0
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsError.kt
@@ -0,0 +1,19 @@
+package com.tangem.features.pushnotificationsettings.impl.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.tangem.core.ui.components.UnableToLoadData
+import com.tangem.features.pushnotificationsettings.impl.entity.PushNotificationSettingsUM
+
+@Composable
+internal fun PushNotificationSettingsError(state: PushNotificationSettingsUM.Error, modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ UnableToLoadData(onRetryClick = state.onRetryClick)
+ }
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsLoading.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsLoading.kt
new file mode 100644
index 0000000000..bc2bcf7d3d
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsLoading.kt
@@ -0,0 +1,80 @@
+package com.tangem.features.pushnotificationsettings.impl.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.components.RectangleShimmer
+import com.tangem.core.ui.components.block.BlockCard
+import com.tangem.core.ui.res.TangemTheme
+
+@Composable
+internal fun PushNotificationSettingsLoading(modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = TangemTheme.dimens.spacing16,
+ vertical = TangemTheme.dimens.spacing12,
+ ),
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
+ ) {
+ repeat(SHIMMER_ROW_COUNT) {
+ ShimmerRow()
+ }
+ }
+}
+
+@Composable
+private fun ShimmerRow(modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
+ ) {
+ BlockCard(modifier = Modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = TangemTheme.dimens.spacing16,
+ vertical = TangemTheme.dimens.spacing12,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ RectangleShimmer(
+ modifier = Modifier
+ .width(SHIMMER_TITLE_WIDTH)
+ .height(SHIMMER_TITLE_HEIGHT),
+ )
+ RectangleShimmer(
+ modifier = Modifier
+ .width(SHIMMER_SWITCH_WIDTH)
+ .height(SHIMMER_SWITCH_HEIGHT),
+ radius = TangemTheme.dimens.radius12,
+ )
+ }
+ }
+ RectangleShimmer(
+ modifier = Modifier
+ .padding(horizontal = TangemTheme.dimens.spacing16)
+ .width(SHIMMER_SUBTITLE_WIDTH)
+ .height(SHIMMER_SUBTITLE_HEIGHT),
+ )
+ }
+}
+
+private const val SHIMMER_ROW_COUNT = 3
+private val SHIMMER_TITLE_WIDTH = 160.dp
+private val SHIMMER_TITLE_HEIGHT = 18.dp
+private val SHIMMER_SWITCH_WIDTH = 40.dp
+private val SHIMMER_SWITCH_HEIGHT = 22.dp
+private val SHIMMER_SUBTITLE_WIDTH = 220.dp
+private val SHIMMER_SUBTITLE_HEIGHT = 14.dp
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsScreen.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsScreen.kt
new file mode 100644
index 0000000000..586d61f56a
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsScreen.kt
@@ -0,0 +1,46 @@
+package com.tangem.features.pushnotificationsettings.impl.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.tangem.core.ui.components.appbar.TangemTopAppBar
+import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.pushnotificationsettings.impl.R
+import com.tangem.features.pushnotificationsettings.impl.entity.PushNotificationSettingsUM
+
+@Composable
+internal fun PushNotificationSettingsScreen(
+ state: PushNotificationSettingsUM,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Scaffold(
+ modifier = modifier,
+ containerColor = TangemTheme.colors.background.secondary,
+ topBar = {
+ TangemTopAppBar(
+ modifier = Modifier.statusBarsPadding(),
+ title = resourceReference(R.string.push_notification_settings_title),
+ startButton = TopAppBarButtonUM.Back(onBackClicked = onBackClick),
+ )
+ },
+ ) { padding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ ) {
+ when (state) {
+ is PushNotificationSettingsUM.Loading -> PushNotificationSettingsLoading()
+ is PushNotificationSettingsUM.Content -> PushNotificationSettingsContent(state = state)
+ is PushNotificationSettingsUM.Error -> PushNotificationSettingsError(state = state)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt b/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt
index 152d433421..500cfcf9b6 100644
--- a/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt
+++ b/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt
@@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
-import org.junit.Test
+import org.junit.jupiter.api.Test
@Suppress("LongParameterList")
class PushNotificationSettingsModelTest {
@@ -204,13 +204,15 @@ class PushNotificationSettingsModelTest {
val model = model(osEnabled = false, preferencesFlow = flow)
advanceUntilIdle()
- val offers = (model.uiState.value as PushNotificationSettingsUM.Content)
- .toggles.first { it.id == ToggleId.OffersUpdates }
- offers.onCheckedChange(true)
- advanceUntilIdle()
+ model.requestPushPermission.test {
+ val offers = (model.uiState.value as PushNotificationSettingsUM.Content)
+ .toggles.first { it.id == ToggleId.OffersUpdates }
+ offers.onCheckedChange(true)
+ advanceUntilIdle()
- val content = model.uiState.value as PushNotificationSettingsUM.Content
- assertThat(content.requestPermissionEvent.javaClass.simpleName).isEqualTo("Triggered")
+ awaitItem()
+ expectNoEvents()
+ }
coVerify(exactly = 0) { updatePreference(any(), any(), any()) }
}
@@ -224,12 +226,14 @@ class PushNotificationSettingsModelTest {
val banner = requireNotNull(
(model.uiState.value as PushNotificationSettingsUM.Content).banner,
)
- banner.onOpenSettingsClick()
- advanceUntilIdle()
+ model.requestPushPermission.test {
+ banner.onOpenSettingsClick()
+ advanceUntilIdle()
+ // The banner CTA opens system settings directly and never asks for the permission.
+ expectNoEvents()
+ }
verify(exactly = 1) { settingsManager.openAppNotificationSettings() }
- val refreshed = model.uiState.value as PushNotificationSettingsUM.Content
- assertThat(refreshed.requestPermissionEvent.javaClass.simpleName).isEqualTo("Consumed")
}
@Test
diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts
index 073dc92b0f..58b07a4853 100644
--- a/features/wallet-settings/impl/build.gradle.kts
+++ b/features/wallet-settings/impl/build.gradle.kts
@@ -19,6 +19,7 @@ dependencies {
implementation(projects.features.nft.api)
implementation(projects.features.onboardingV2.api)
implementation(projects.features.pushNotifications.api)
+ implementation(projects.features.pushNotificationSettings.api)
implementation(projects.features.hotWallet.api)
implementation(projects.features.wallet.api)
diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt
index 4fa7a53f60..23e9499009 100644
--- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt
+++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt
@@ -63,6 +63,8 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
isNotificationsEnabled = true,
onCheckedNotificationsChanged = {},
onNotificationsDescriptionClick = {},
+ isPushNotificationSettingsEnabled = false,
+ onNotificationSettingsClick = {},
isNotificationsPermissionGranted = false,
onAccessCodeClick = {},
onBackupClick = {},
diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt
index 8f236ca63a..6741173d06 100644
--- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt
+++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt
@@ -51,6 +51,7 @@ import com.tangem.feature.walletsettings.utils.ItemsBuilder
import com.tangem.feature.walletsettings.utils.WalletCardItemDelegate
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
+import com.tangem.features.pushnotificationsettings.PushNotificationSettingsFeatureToggles
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.PersistentList
@@ -92,6 +93,7 @@ internal class WalletSettingsModel @Inject constructor(
private val accountListSortingSaver: AccountListSortingSaver,
private val startAssetsDiscoveryUseCase: StartAssetsDiscoveryUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
+ private val pushNotificationSettingsFeatureToggles: PushNotificationSettingsFeatureToggles,
) : Model() {
val params: WalletSettingsComponent.Params = paramsContainer.require()
@@ -203,6 +205,8 @@ internal class WalletSettingsModel @Inject constructor(
is UserWallet.Cold -> userWallet.isMultiCurrency
is UserWallet.Hot -> true
}
+ val isPushNotificationSettingsEnabled =
+ pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled
return itemsBuilder.buildItems(
userWallet = userWallet,
cardItem = cardItem,
@@ -245,6 +249,8 @@ internal class WalletSettingsModel @Inject constructor(
isNotificationsPermissionGranted = isNotificationsPermissionGranted,
onCheckedNotificationsChanged = ::onCheckedNotificationsChange,
onNotificationsDescriptionClick = ::onNotificationsDescriptionClick,
+ isPushNotificationSettingsEnabled = isPushNotificationSettingsEnabled,
+ onNotificationSettingsClick = ::onNotificationSettingsClick,
onAccessCodeClick = { onAccessCodeClick(userWallet) },
onBackupClick = ::onBackupClick,
onCardSettingsClick = ::onCardSettingsClick,
@@ -252,6 +258,10 @@ internal class WalletSettingsModel @Inject constructor(
)
}
+ private fun onNotificationSettingsClick() {
+ router.push(AppRoute.PushNotificationSettings(userWalletId = params.userWalletId))
+ }
+
private fun forgetWallet() = modelScope.launch {
val userWallet = getUserWalletUseCase(params.userWalletId)
.getOrNull()
diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt
index 8bde456990..21dfa146a5 100644
--- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt
+++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt
@@ -33,6 +33,8 @@ internal class ItemsBuilder @Inject constructor() {
isNotificationsPermissionGranted: Boolean,
onCheckedNotificationsChanged: (Boolean) -> Unit,
onNotificationsDescriptionClick: () -> Unit,
+ isPushNotificationSettingsEnabled: Boolean,
+ onNotificationSettingsClick: () -> Unit,
forgetWallet: () -> Unit,
onLinkMoreCardsClick: () -> Unit,
onReferralClick: () -> Unit,
@@ -50,20 +52,26 @@ internal class ItemsBuilder @Inject constructor() {
isLinkMoreCardsAvailable = isLinkMoreCardsAvailable,
isReferralAvailable = isReferralAvailable,
isManageTokensAvailable = isManageTokensAvailable,
+ isPushNotificationSettingsEnabled = isPushNotificationSettingsEnabled,
onLinkMoreCardsClick = onLinkMoreCardsClick,
onReferralClick = onReferralClick,
onManageTokensClick = onManageTokensClick,
onBackupClick = onBackupClick,
onCardSettingsClick = onCardSettingsClick,
+ onNotificationSettingsClick = onNotificationSettingsClick,
),
)
.addAll(
- buildNotificationItems(
- isNotificationsPermissionGranted = isNotificationsPermissionGranted,
- isNotificationsEnabled = isNotificationsEnabled,
- onCheckedNotificationsChanged = onCheckedNotificationsChanged,
- onNotificationsDescriptionClick = onNotificationsDescriptionClick,
- ),
+ if (isPushNotificationSettingsEnabled) {
+ emptyList()
+ } else {
+ buildNotificationItems(
+ isNotificationsPermissionGranted = isNotificationsPermissionGranted,
+ isNotificationsEnabled = isNotificationsEnabled,
+ onCheckedNotificationsChanged = onCheckedNotificationsChanged,
+ onNotificationsDescriptionClick = onNotificationsDescriptionClick,
+ )
+ },
)
.addAll(
buildNFTItems(
@@ -137,11 +145,13 @@ internal class ItemsBuilder @Inject constructor() {
isLinkMoreCardsAvailable: Boolean,
isReferralAvailable: Boolean,
isManageTokensAvailable: Boolean,
+ isPushNotificationSettingsEnabled: Boolean,
onLinkMoreCardsClick: () -> Unit,
onReferralClick: () -> Unit,
onManageTokensClick: () -> Unit,
onBackupClick: () -> Unit,
onCardSettingsClick: () -> Unit,
+ onNotificationSettingsClick: () -> Unit,
) = WalletSettingsItemUM.WithItems(
id = "card",
description = null,
@@ -207,6 +217,16 @@ internal class ItemsBuilder @Inject constructor() {
add(referralBlock)
}
+
+ if (isPushNotificationSettingsEnabled) {
+ val notificationSettingsBlock = BlockUM(
+ text = resourceReference(R.string.push_notification_settings_title),
+ iconRes = R.drawable.ic_push_notification_settings_24,
+ onClick = onNotificationSettingsClick,
+ )
+
+ add(notificationSettingsBlock)
+ }
}.toImmutableList(),
)