Include Lumo in installed app packages, add NPS FF

MAILANDR-2635: submit/skip NPS feedback via separate edpoints
This commit is contained in:
Rok Oblak
2025-07-28 14:55:56 +02:00
committed by MargeBot
parent e3286b44f0
commit b241d38abc
8 changed files with 109 additions and 11 deletions
+1
View File
@@ -35,6 +35,7 @@
<package android:name="ch.protonvpn.android" />
<package android:name="proton.android.pass" />
<package android:name="me.proton.wallet.android" />
<package android:name="me.proton.android.lumo" />
</queries>
<application
@@ -29,23 +29,28 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import me.proton.core.featureflag.domain.entity.FeatureFlag
import org.junit.Test
import javax.inject.Provider
import kotlin.test.assertEquals
internal class ObserveNPSEligibilityTest {
private val observePrimaryUser = mockk<ObservePrimaryUser>()
private val observeMailFeature = mockk<ObserveMailFeature>()
private val npsEnabled = mockk<Provider<Boolean>>()
private val sut = ObserveNPSEligibility(
observePrimaryUser,
observeMailFeature
)
private val sut: ObserveNPSEligibility
get() = ObserveNPSEligibility(
observePrimaryUser,
observeMailFeature,
npsEnabled.get()
)
private val user = UserSample.Primary
@Test
fun `should emit false when primary user is null`() = runTest {
every { observePrimaryUser() } returns flowOf(null)
expectNPSEnabled(true)
sut().test {
assertEquals(false, awaitItem())
awaitComplete()
@@ -53,9 +58,20 @@ internal class ObserveNPSEligibilityTest {
}
@Test
fun `should emit false when mail feature flag is disabled`() = runTest {
fun `should emit false when FF disabled`() = runTest {
every { observePrimaryUser() } returns flowOf(user)
isFFEnabled(false)
expectNPSEnabled(false)
sut().test {
assertEquals(false, awaitItem())
awaitComplete()
}
}
@Test
fun `should emit false when per-user feature flag is disabled`() = runTest {
every { observePrimaryUser() } returns flowOf(user)
expectPerUserFlagEnabled(false)
expectNPSEnabled(true)
sut().test {
assertEquals(false, awaitItem())
@@ -66,7 +82,8 @@ internal class ObserveNPSEligibilityTest {
@Test
fun `should emit true when feature enabled`() = runTest {
every { observePrimaryUser() } returns flowOf(user)
isFFEnabled(true)
expectPerUserFlagEnabled(true)
expectNPSEnabled(true)
sut().test {
assertEquals(true, awaitItem())
@@ -74,8 +91,12 @@ internal class ObserveNPSEligibilityTest {
}
}
private fun isFFEnabled(enabled: Boolean) {
private fun expectPerUserFlagEnabled(enabled: Boolean) {
every { observeMailFeature(user.userId, MailFeatureId.NPSFeedback) } returns
flowOf(FeatureFlag.default("ff1", defaultValue = enabled))
}
private fun expectNPSEnabled(value: Boolean) {
every { npsEnabled.get() } returns value
}
}
@@ -29,6 +29,7 @@ import ch.protonmail.android.mailupselling.domain.annotations.DriveSpotlightEnab
import ch.protonmail.android.mailupselling.domain.annotations.ForceOneClickUpsellingDetailsOverride
import ch.protonmail.android.mailupselling.domain.annotations.HeaderUpsellSocialProofLayoutEnabled
import ch.protonmail.android.mailupselling.domain.annotations.HeaderUpsellVariantLayoutEnabled
import ch.protonmail.android.mailupselling.domain.annotations.NPSEnabled
import ch.protonmail.android.mailupselling.domain.annotations.OneClickUpsellingAlwaysShown
import ch.protonmail.android.mailupselling.domain.annotations.OneClickUpsellingTelemetryEnabled
import ch.protonmail.android.mailupselling.domain.annotations.SidebarUpsellingEnabled
@@ -48,6 +49,7 @@ import ch.protonmail.android.mailupselling.domain.usecase.featureflags.AlwaysSho
import ch.protonmail.android.mailupselling.domain.usecase.featureflags.IsDriveSpotlightEnabled
import ch.protonmail.android.mailupselling.domain.usecase.featureflags.IsHeaderUpsellSocialProofLayoutEnabled
import ch.protonmail.android.mailupselling.domain.usecase.featureflags.IsHeaderUpsellVariantLayoutEnabled
import ch.protonmail.android.mailupselling.domain.usecase.featureflags.IsNPSEnabled
import ch.protonmail.android.mailupselling.domain.usecase.featureflags.IsOneClickUpsellingTelemetryEnabled
import ch.protonmail.android.mailupselling.domain.usecase.featureflags.IsSidebarUpsellingEnabled
import ch.protonmail.android.mailupselling.domain.usecase.featureflags.IsSignupPaidPlanSupportEnabled
@@ -101,6 +103,10 @@ object UpsellingModule {
@DriveSpotlightEnabled
fun provideDriveSpotlightEnabled(isEnabled: IsDriveSpotlightEnabled) = isEnabled(null)
@Provides
@NPSEnabled
fun provideNPSEnabled(isEnabled: IsNPSEnabled) = isEnabled(null)
@Provides
@HeaderUpsellSocialProofLayoutEnabled
fun provideUpsellSocialProofLayoutEnabled(isEnabled: IsHeaderUpsellSocialProofLayoutEnabled) = isEnabled(null)
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2022 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.mailupselling.domain.annotations
import javax.inject.Qualifier
/**
* Indicates whether the NPS is enabled. If disabled, individual user' feature flags will not be checked further.
*/
@Qualifier
annotation class NPSEnabled
@@ -57,5 +57,6 @@ private val ProtonPackages = listOf(
"me.proton.android.drive",
"me.proton.android.calendar",
"proton.android.pass",
"me.proton.wallet.android"
"me.proton.wallet.android",
"me.proton.android.lumo"
)
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2022 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.mailupselling.domain.usecase.featureflags
import me.proton.core.domain.entity.UserId
import me.proton.core.featureflag.domain.ExperimentalProtonFeatureFlag
import me.proton.core.featureflag.domain.FeatureFlagManager
import me.proton.core.featureflag.domain.entity.FeatureId
import javax.inject.Inject
class IsNPSEnabled @Inject constructor(
private val featureFlagManager: FeatureFlagManager
) {
@OptIn(ExperimentalProtonFeatureFlag::class)
operator fun invoke(userId: UserId?) = featureFlagManager.getValue(userId, FeatureId(FeatureFlagId))
private companion object {
const val FeatureFlagId = "MailAndroidNpsFeedback"
}
}
@@ -121,7 +121,8 @@ internal class GetInstalledProtonAppsTest {
"me.proton.android.drive" to "v2",
"me.proton.android.calendar" to "v3",
"proton.android.pass" to "v4",
"me.proton.wallet.android" to "v5"
"me.proton.wallet.android" to "v5",
"me.proton.android.lumo" to "v6"
)
mockInstalled(*all.toTypedArray())
@@ -21,6 +21,7 @@ package ch.protonmail.android.mailupselling.presentation.usecase
import ch.protonmail.android.mailcommon.domain.MailFeatureId
import ch.protonmail.android.mailcommon.domain.usecase.ObserveMailFeature
import ch.protonmail.android.mailcommon.domain.usecase.ObservePrimaryUser
import ch.protonmail.android.mailupselling.domain.annotations.DriveSpotlightEnabled
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
@@ -30,12 +31,14 @@ import javax.inject.Inject
class ObserveNPSEligibility @Inject constructor(
private val observePrimaryUser: ObservePrimaryUser,
private val observeMailFeature: ObserveMailFeature
private val observeMailFeature: ObserveMailFeature,
@DriveSpotlightEnabled private val driveSpotlightEnabled: Boolean
) {
operator fun invoke(): Flow<Boolean> = observePrimaryUser()
.distinctUntilChanged()
.flatMapLatest { user ->
if (user == null) return@flatMapLatest flowOf(false)
if (!driveSpotlightEnabled) return@flatMapLatest flowOf(false)
observeMailFeature(user.userId, MailFeatureId.NPSFeedback).map { npsFeatureFlag ->
npsFeatureFlag.value
}