Add separate feature spotlight FF

ET-5738
This commit is contained in:
Niccolò Forlini
2026-01-20 11:10:32 +00:00
committed by MargeBot
parent e2aef7419d
commit e75df7fe60
5 changed files with 150 additions and 6 deletions
@@ -20,25 +20,38 @@ package ch.protonmail.android.feature.spotlight
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import ch.protonmail.android.mailfeatureflags.domain.annotation.IsFeatureSpotlightEnabled
import ch.protonmail.android.mailfeatureflags.domain.model.FeatureFlag
import ch.protonmail.android.mailspotlight.domain.usecase.ObserveFeatureSpotlightDisplay
import ch.protonmail.android.mailspotlight.presentation.model.FeatureSpotlightState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
class HomeFeatureSpotlightViewModel @Inject constructor(
observeFeatureSpotlightDisplay: ObserveFeatureSpotlightDisplay
observeFeatureSpotlightDisplay: ObserveFeatureSpotlightDisplay,
@IsFeatureSpotlightEnabled private val isEnabled: FeatureFlag<Boolean>
) : ViewModel() {
val state: StateFlow<FeatureSpotlightState> = observeFeatureSpotlightDisplay().map { preferenceEither ->
preferenceEither.fold(
ifLeft = { FeatureSpotlightState.Hide },
ifRight = { if (it.show) FeatureSpotlightState.Show else FeatureSpotlightState.Hide }
)
val state: StateFlow<FeatureSpotlightState> = flow {
if (!isEnabled.get()) {
emit(FeatureSpotlightState.Hide)
} else {
emitAll(
observeFeatureSpotlightDisplay().map { preferenceEither ->
preferenceEither.fold(
ifLeft = { FeatureSpotlightState.Hide },
ifRight = { if (it.show) FeatureSpotlightState.Show else FeatureSpotlightState.Hide }
)
}
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2025 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.feature.spotlight
import app.cash.turbine.test
import arrow.core.left
import arrow.core.right
import ch.protonmail.android.mailcommon.domain.model.PreferencesError
import ch.protonmail.android.mailfeatureflags.domain.model.FeatureFlag
import ch.protonmail.android.mailspotlight.domain.model.FeatureSpotlightDisplay
import ch.protonmail.android.mailspotlight.domain.usecase.ObserveFeatureSpotlightDisplay
import ch.protonmail.android.mailspotlight.presentation.model.FeatureSpotlightState
import ch.protonmail.android.test.utils.rule.MainDispatcherRule
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import kotlin.test.Test
import kotlin.test.assertEquals
internal class HomeFeatureSpotlightViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val mockFeatureFlag = mockk<FeatureFlag<Boolean>>()
private val mockObserveFeatureSpotlightDisplay = mockk<ObserveFeatureSpotlightDisplay>()
@Test
fun `should emit Hide when feature flag is disabled`() = runTest {
// Given
coEvery { mockFeatureFlag.get() } returns false
val viewModel = buildViewModel()
// When/Then
viewModel.state.test {
assertEquals(FeatureSpotlightState.Hide, awaitItem())
}
}
@Test
fun `should emit Show when feature flag is enabled and preference is show`() = runTest {
// Given
coEvery { mockFeatureFlag.get() } returns true
every { mockObserveFeatureSpotlightDisplay() } returns flowOf(FeatureSpotlightDisplay(show = true).right())
val viewModel = buildViewModel()
// When/Then
viewModel.state.test {
assertEquals(FeatureSpotlightState.Show, awaitItem())
}
}
@Test
fun `should emit Hide when feature flag is enabled and preference is hide`() = runTest {
// Given
coEvery { mockFeatureFlag.get() } returns true
every { mockObserveFeatureSpotlightDisplay() } returns flowOf(FeatureSpotlightDisplay(show = false).right())
val viewModel = buildViewModel()
// When/Then
viewModel.state.test {
assertEquals(FeatureSpotlightState.Hide, awaitItem())
}
}
@Test
fun `should emit Hide when feature flag is enabled and preference returns error`() = runTest {
// Given
coEvery { mockFeatureFlag.get() } returns true
every { mockObserveFeatureSpotlightDisplay() } returns flowOf(PreferencesError.left())
val viewModel = buildViewModel()
// When/Then
viewModel.state.test {
assertEquals(FeatureSpotlightState.Hide, awaitItem())
}
}
private fun buildViewModel() = HomeFeatureSpotlightViewModel(
observeFeatureSpotlightDisplay = mockObserveFeatureSpotlightDisplay,
isEnabled = mockFeatureFlag
)
}
@@ -28,6 +28,7 @@ import ch.protonmail.android.mailfeatureflags.domain.annotation.FeatureFlagsCoro
import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBlackFridayWave1Enabled
import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBlackFridayWave2Enabled
import ch.protonmail.android.mailfeatureflags.domain.annotation.IsDebugInspectDbEnabled
import ch.protonmail.android.mailfeatureflags.domain.annotation.IsFeatureSpotlightEnabled
import ch.protonmail.android.mailfeatureflags.domain.annotation.IsInjectCssOverrideEnabled
import ch.protonmail.android.mailfeatureflags.domain.annotation.IsLastMessageAutoExpandEnabled
import ch.protonmail.android.mailfeatureflags.domain.annotation.IsOnboardingUpsellEnabled
@@ -39,6 +40,7 @@ import ch.protonmail.android.mailfeatureflags.domain.model.ComposerAutoCollapseQ
import ch.protonmail.android.mailfeatureflags.domain.model.ConversationDetailAutoExpandLastMessageEnabled
import ch.protonmail.android.mailfeatureflags.domain.model.DebugInspectDbEnabled
import ch.protonmail.android.mailfeatureflags.domain.model.FeatureFlagDefinition
import ch.protonmail.android.mailfeatureflags.domain.model.FeatureSpotlight
import ch.protonmail.android.mailfeatureflags.domain.model.InjectDetailCssOverrideEnabled
import ch.protonmail.android.mailfeatureflags.domain.model.MailBlackFriday2025Enabled
import ch.protonmail.android.mailfeatureflags.domain.model.MailBlackFriday2025Wave2Enabled
@@ -186,4 +188,15 @@ object FeatureFlagsModule {
@IntoSet
@Singleton
fun providePrivacyBundle2601EnabledDef(): FeatureFlagDefinition = PrivacyBundle2601
@Provides
@Singleton
@IsFeatureSpotlightEnabled
fun provideFeatureSpotlightEnabled(factory: BooleanFeatureFlagFactory) = factory.create(FeatureSpotlight.key, false)
@Provides
@IntoSet
@Singleton
fun provideFeatureSpotlightEnabledDef(): FeatureFlagDefinition = FeatureSpotlight
}
@@ -63,3 +63,7 @@ annotation class IsShowRatingBoosterEnabled
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IsPrivacyBundle2601Enabled
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IsFeatureSpotlightEnabled
@@ -105,3 +105,11 @@ data object PrivacyBundle2601 : FeatureFlagDefinition(
description = "Enable padlocks + blocked trackers in details/composer.",
defaultValue = false
)
data object FeatureSpotlight : FeatureFlagDefinition(
key = "MailAndroidV7FeatureSpotlight",
name = "Enable the feature spotlight",
category = FeatureFlagCategory.Details,
description = "Show a what's new screen on startup when available.",
defaultValue = false
)