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