Updated on 2026-08-14
This commit is contained in:
parent
6c41eefe0b
commit
044efe36c5
19 changed files with 540 additions and 35 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
|
|||
isNotificationsEnabled = true,
|
||||
onCheckedNotificationsChanged = {},
|
||||
onNotificationsDescriptionClick = {},
|
||||
isPushNotificationSettingsEnabled = false,
|
||||
onNotificationSettingsClick = {},
|
||||
isNotificationsPermissionGranted = false,
|
||||
onAccessCodeClick = {},
|
||||
onBackupClick = {},
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue