Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-10 13:30:48 +03:00
parent 6c41eefe0b
commit 044efe36c5
19 changed files with 540 additions and 35 deletions

View file

@ -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,

View file

@ -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,

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M14.852,19.604C14.462,20.806 13.333,21.676 12,21.676C10.667,21.676 9.538,20.806 9.147,19.604H14.852ZM12.424,2.009C16.132,2.197 19.08,5.263 19.08,9.018V15.053L21.08,17.135V18.106H3.045V17.135L5.045,15.053V9.018C5.045,5.142 8.187,2 12.063,2L12.424,2.009ZM12.063,4C9.292,4 7.045,6.247 7.045,9.018V15.858L6.807,16.106H17.319L17.08,15.857V9.018C17.08,6.247 14.833,4 12.063,4Z"
android:fillColor="#656565"
android:fillType="evenOdd"/>
</vector>

View file

@ -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)

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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<ToggleUM>,
val requestPermissionEvent: StateEvent<Unit>,
val onMoreInfoClick: () -> Unit,
) : PushNotificationSettingsUM

View file

@ -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>(LoadState.Loading)
private val osNotificationsEnabled = MutableStateFlow(systemNotificationsStateProvider.areNotificationsEnabled())
private val pendingRequest = MutableStateFlow<StateEvent<Unit>>(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<Unit>(Channel.BUFFERED)
/** One-shot requests to launch the system push permission prompt, consumed by the component. */
val requestPushPermission: Flow<Unit> = requestPushPermissionChannel.receiveAsFlow()
val uiState: StateFlow<PushNotificationSettingsUM> = 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<Unit>,
): 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))

View file

@ -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,
),
),
)
}

View file

@ -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,
)
}

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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)

View file

@ -63,6 +63,8 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
isNotificationsEnabled = true,
onCheckedNotificationsChanged = {},
onNotificationsDescriptionClick = {},
isPushNotificationSettingsEnabled = false,
onNotificationSettingsClick = {},
isNotificationsPermissionGranted = false,
onAccessCodeClick = {},
onBackupClick = {},

View file

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

View file

@ -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(),
)