Merge branch 'feat/3071_prompt-whitelisted-user-to-rate-app' into 'develop'

Require users to rate app when meeting conditions

See merge request android/mail/proton-mail-android!1251
This commit is contained in:
Marino Meneghel 2023-04-13 09:34:24 +00:00
commit d9aab635d8
23 changed files with 751 additions and 6 deletions

View File

@ -13,6 +13,7 @@
<option name="configurationPath" value="" />
<option name="exclusions">
<set>
<option value="kotlin/libraries.kt" />
<option value="versionsConfig.kt" />
</set>
</option>

View File

@ -357,6 +357,7 @@ dependencies {
`constraint-layout`,
`material`,
`paging-runtime`,
`google-play-review`,
// Lifecycle
`lifecycle-extensions`,

View File

@ -144,6 +144,11 @@
android:name="ch.protonmail.android.onboarding.base.presentation.StartOnboardingObserverInitializer"
android:value="androidx.startup"
tools:node="remove" />
<meta-data
android:name="ch.protonmail.android.featureflags.FeatureFlagsInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<provider
android:name="androidx.core.content.FileProvider"

View File

@ -87,6 +87,7 @@ import ch.protonmail.android.exceptions.ErrorStateGeneratorsKt;
import ch.protonmail.android.feature.account.AccountManagerKt;
import ch.protonmail.android.feature.account.AccountStateHandlerInitializer;
import ch.protonmail.android.feature.account.CoreAccountManagerMigration;
import ch.protonmail.android.featureflags.FeatureFlagsInitializer;
import ch.protonmail.android.notifications.data.remote.fcm.MultiUserFcmTokenManager;
import ch.protonmail.android.notifications.presentation.utils.NotificationServer;
import ch.protonmail.android.onboarding.base.presentation.AddStartOnboardingObserverIfNeeded;
@ -109,8 +110,8 @@ import me.proton.core.auth.presentation.MissingScopeInitializer;
import me.proton.core.crypto.validator.presentation.init.CryptoValidatorInitializer;
import me.proton.core.domain.entity.UserId;
import me.proton.core.humanverification.presentation.HumanVerificationInitializer;
import me.proton.core.plan.presentation.UnredeemedPurchaseInitializer;
import me.proton.core.network.presentation.init.UnAuthSessionFetcherInitializer;
import me.proton.core.plan.presentation.UnredeemedPurchaseInitializer;
import me.proton.core.util.kotlin.CoreLogger;
import studio.forface.viewstatestore.ViewStateStoreConfig;
import timber.log.Timber;
@ -234,6 +235,7 @@ public class ProtonMailApplication extends Application implements androidx.work.
appInitializer.initializeComponent(MissingScopeInitializer.class);
appInitializer.initializeComponent(UnredeemedPurchaseInitializer.class);
appInitializer.initializeComponent(UnAuthSessionFetcherInitializer.class);
appInitializer.initializeComponent(FeatureFlagsInitializer.class);
checkForUpdateAndClearCache();
}

View File

@ -62,6 +62,7 @@ import ch.protonmail.android.events.DownloadEmbeddedImagesEvent
import ch.protonmail.android.events.DownloadedAttachmentEvent
import ch.protonmail.android.events.PostPhishingReportEvent
import ch.protonmail.android.events.Status
import ch.protonmail.android.feature.rating.MailboxScreenViewInMemoryRepository
import ch.protonmail.android.jobs.PostSpamJob
import ch.protonmail.android.labels.domain.model.LabelId
import ch.protonmail.android.labels.domain.model.LabelType
@ -122,6 +123,9 @@ internal class MessageDetailsActivity : BaseStoragePermissionActivity() {
@Inject
lateinit var accountSettingsRepository: AccountSettingsRepository
@Inject
lateinit var mailboxScreenViewRepository: MailboxScreenViewInMemoryRepository
private lateinit var messageOrConversationId: String
private lateinit var messageExpandableAdapter: MessageDetailsAdapter
private lateinit var primaryBaseActivity: Context
@ -358,6 +362,11 @@ internal class MessageDetailsActivity : BaseStoragePermissionActivity() {
mApp.bus.unregister(this)
}
override fun onBackPressed() {
mailboxScreenViewRepository.recordScreenView()
super.onBackPressed()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {

View File

@ -31,6 +31,7 @@ import ch.protonmail.android.contacts.groups.edit.chooser.AddressChooserViewMode
import ch.protonmail.android.core.ProtonMailApplication
import ch.protonmail.android.core.UserManager
import ch.protonmail.android.drawer.presentation.mapper.DrawerFoldersAndLabelsSectionUiModelMapper
import ch.protonmail.android.feature.rating.usecase.StartRateAppFlowIfNeeded
import ch.protonmail.android.labels.domain.LabelRepository
import ch.protonmail.android.labels.domain.usecase.ObserveLabels
import ch.protonmail.android.labels.domain.usecase.ObserveLabelsAndFoldersWithChildren
@ -117,7 +118,8 @@ internal class ViewModelModule {
getMailSettings: GetMailSettings,
mailboxItemUiModelMapper: MailboxItemUiModelMapper,
fetchEventsAndReschedule: FetchEventsAndReschedule,
clearNotificationsForUser: ClearNotificationsForUser
clearNotificationsForUser: ClearNotificationsForUser,
startRateAppFlowIfNeeded: StartRateAppFlowIfNeeded
) = MailboxViewModel(
messageDetailsRepositoryFactory = messageDetailsRepositoryFactory,
userManager = userManager,
@ -143,6 +145,7 @@ internal class ViewModelModule {
getMailSettings = getMailSettings,
mailboxItemUiModelMapper = mailboxItemUiModelMapper,
fetchEventsAndReschedule = fetchEventsAndReschedule,
clearNotificationsForUser = clearNotificationsForUser
clearNotificationsForUser = clearNotificationsForUser,
startRateAppFlowIfNeeded = startRateAppFlowIfNeeded
)
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2022 Proton AG
*
* This file is part of 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.rating
import timber.log.Timber
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MailboxScreenViewInMemoryRepository @Inject constructor() {
public val screenViewCount: Int
get() = mailboxScreenViews.get()
private var mailboxScreenViews = AtomicInteger(0)
fun recordScreenView() {
mailboxScreenViews.incrementAndGet()
Timber.d("Recording mailbox screen view: count $mailboxScreenViews")
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2022 Proton AG
*
* This file is part of 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.rating
import android.content.Context
import com.google.android.play.core.review.ReviewManagerFactory
import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber
import javax.inject.Inject
class StartRateAppFlow @Inject constructor(
@ApplicationContext
private val context: Context
) {
operator fun invoke() {
val manager = ReviewManagerFactory.create(context)
manager.requestReviewFlow().addOnCompleteListener {
Timber.d("App review finished. Success = ${it.isSuccessful}")
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2022 Proton AG
*
* This file is part of 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.rating.usecase
import ch.protonmail.android.feature.rating.MailboxScreenViewInMemoryRepository
import ch.protonmail.android.feature.rating.StartRateAppFlow
import ch.protonmail.android.featureflags.MailFeatureFlags
import me.proton.core.domain.entity.UserId
import me.proton.core.featureflag.domain.FeatureFlagManager
import me.proton.core.featureflag.domain.entity.FeatureFlag
import javax.inject.Inject
class StartRateAppFlowIfNeeded @Inject constructor(
private val mailboxScreenViewsRepository: MailboxScreenViewInMemoryRepository,
private val featureFlagManager: FeatureFlagManager,
private val startRateAppFlow: StartRateAppFlow
) {
suspend operator fun invoke(userId: UserId) {
if (!isShowReviewFeatureFlagEnabled(userId)) {
return
}
if (mailboxScreenViewsRepository.screenViewCount < MailboxScreenViewsThreshold) {
return
}
startRateAppFlow()
recordReviewFlowStarted(userId)
}
private suspend fun recordReviewFlowStarted(userId: UserId) {
val featureFlag = getShowReviewFeatureFlag(userId)
val offFeatureFlag = featureFlag.copy(defaultValue = false, value = false)
featureFlagManager.update(offFeatureFlag)
}
private suspend fun isShowReviewFeatureFlagEnabled(userId: UserId) = getShowReviewFeatureFlag(userId).value
private suspend fun getShowReviewFeatureFlag(
userId: UserId
) = featureFlagManager.getOrDefault(
userId,
MailFeatureFlags.ShowReviewAppDialog.featureId,
FeatureFlag.default(MailFeatureFlags.ShowReviewAppDialog.featureId.id, false)
)
companion object {
private const val MailboxScreenViewsThreshold = 2
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2022 Proton AG
*
* This file is part of 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.featureflags
import android.content.Context
import androidx.startup.Initializer
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
class FeatureFlagsInitializer : Initializer<RefreshFeatureFlags> {
override fun create(context: Context): RefreshFeatureFlags {
val refreshFeatureFlags = EntryPointAccessors.fromApplication(
context.applicationContext,
FeatureFlagsEntryPoint::class.java
).refreshFeatureFlags()
refreshFeatureFlags.refresh()
return refreshFeatureFlags
}
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
@EntryPoint
@InstallIn(SingletonComponent::class)
interface FeatureFlagsEntryPoint {
fun refreshFeatureFlags(): RefreshFeatureFlags
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2022 Proton AG
*
* This file is part of 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.featureflags
import me.proton.core.featureflag.domain.entity.FeatureId
enum class MailFeatureFlags(val featureId: FeatureId) {
ShowReviewAppDialog(FeatureId("RatingAndroidMail"))
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2022 Proton AG
*
* This file is part of 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.featureflags
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.domain.entity.UserId
import me.proton.core.featureflag.domain.FeatureFlagManager
import me.proton.core.featureflag.domain.entity.FeatureFlag
import me.proton.core.util.kotlin.CoroutineScopeProvider
import timber.log.Timber
import javax.inject.Inject
class RefreshFeatureFlags @Inject constructor(
private val scopeProvider: CoroutineScopeProvider,
private val featureFlagManager: FeatureFlagManager,
private val accountManager: AccountManager
) {
private val scope get() = scopeProvider.GlobalIOSupervisedScope
fun refresh() {
scope.launch {
val accounts = accountManager.getAccounts().firstOrNull() ?: return@launch
Timber.d("Refreshing feature flags for ${accounts.count()} accounts")
accounts.map { it.userId }.forEach { userId ->
refreshCachedShowRatingsFeatureFlag(userId)
Timber.d("Rating feature flag refreshed for user $userId")
}
}
}
private suspend fun refreshCachedShowRatingsFeatureFlag(userId: UserId) {
featureFlagManager.getOrDefault(
userId = userId,
featureId = MailFeatureFlags.ShowReviewAppDialog.featureId,
default = FeatureFlag.default(MailFeatureFlags.ShowReviewAppDialog.featureId.id, false),
refresh = true
)
}
}

View File

@ -778,6 +778,8 @@ internal class MailboxActivity :
if (mailboxLocation == MessageLocationType.INBOX) {
userManager.currentUserId?.let { mailboxViewModel.clearNotifications(it) }
}
mailboxViewModel.startRateAppFlowIfNeeded()
}
override fun onPause() {

View File

@ -41,6 +41,7 @@ import ch.protonmail.android.domain.loadMoreMap
import ch.protonmail.android.drawer.presentation.mapper.DrawerFoldersAndLabelsSectionUiModelMapper
import ch.protonmail.android.drawer.presentation.model.DrawerFoldersAndLabelsSectionUiModel
import ch.protonmail.android.feature.NotLoggedIn
import ch.protonmail.android.feature.rating.usecase.StartRateAppFlowIfNeeded
import ch.protonmail.android.labels.domain.LabelRepository
import ch.protonmail.android.labels.domain.model.Label
import ch.protonmail.android.labels.domain.model.LabelId
@ -136,7 +137,8 @@ internal class MailboxViewModel @Inject constructor(
private val getMailSettings: GetMailSettings,
private val mailboxItemUiModelMapper: MailboxItemUiModelMapper,
private val fetchEventsAndReschedule: FetchEventsAndReschedule,
private val clearNotificationsForUser: ClearNotificationsForUser
private val clearNotificationsForUser: ClearNotificationsForUser,
private val startRateAppFlowIfNeeded: StartRateAppFlowIfNeeded
) : ConnectivityBaseViewModel(verifyConnection, networkConfigurator) {
private val _manageLimitReachedWarning = MutableLiveData<Event<Boolean>>()
@ -722,6 +724,13 @@ internal class MailboxViewModel @Inject constructor(
getMailSettings(userId).map(::Right)
}
fun startRateAppFlowIfNeeded() {
val userId = userManager.currentUserId ?: return
viewModelScope.launch {
startRateAppFlowIfNeeded.invoke(userId)
}
}
data class GetMailboxItemsParameters(
val userId: UserId,
val labelId: LabelId,

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2022 Proton AG
*
* This file is part of 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.rating
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.test.assertEquals
class MailboxScreenViewInMemoryRepositoryTest {
private val showReviewAppRepository = MailboxScreenViewInMemoryRepository()
@Test
fun `increase mailbox screen views counter when record mailbox screen view is called`() = runTest {
// given
check(showReviewAppRepository.screenViewCount == 0)
// when
showReviewAppRepository.recordScreenView()
// then
assertEquals(1, showReviewAppRepository.screenViewCount)
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2022 Proton AG
*
* This file is part of 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.rating
import android.content.Context
import com.google.android.play.core.review.ReviewManager
import com.google.android.play.core.review.ReviewManagerFactory
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import io.mockk.verify
import org.junit.After
import org.junit.Test
class StartRateAppFlowTest {
private val context: Context = mockk(relaxed = true)
private val startRateAppFlow = StartRateAppFlow(context)
@After
fun tearDown() {
unmockkStatic(ReviewManagerFactory::class)
}
@Test
fun `request review flow from google play review manager`() {
// given
val reviewMangerMock = mockk<ReviewManager> {
every { this@mockk.requestReviewFlow() } returns mockk(relaxed = true)
}
mockkStatic(ReviewManagerFactory::class)
every { ReviewManagerFactory.create(context) } returns reviewMangerMock
// when
startRateAppFlow()
// then
verify { reviewMangerMock.requestReviewFlow() }
}
}

View File

@ -0,0 +1,122 @@
/*
* Copyright (c) 2022 Proton AG
*
* This file is part of 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.rating.usecase
import ch.protonmail.android.feature.rating.MailboxScreenViewInMemoryRepository
import ch.protonmail.android.feature.rating.StartRateAppFlow
import ch.protonmail.android.featureflags.MailFeatureFlags
import ch.protonmail.android.testdata.UserTestData
import io.mockk.Called
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import me.proton.core.domain.entity.UserId
import me.proton.core.featureflag.domain.FeatureFlagManager
import me.proton.core.featureflag.domain.entity.FeatureFlag
import me.proton.core.featureflag.domain.entity.Scope
import org.junit.Test
class StartRateAppFlowIfNeededTest {
private val userId = UserTestData.userId
private val mailboxScreenViewsRepository: MailboxScreenViewInMemoryRepository = mockk()
private val featureFlagManager: FeatureFlagManager = mockk(relaxUnitFun = true)
private val startRateAppFlow: StartRateAppFlow = mockk(relaxUnitFun = true)
private val startRateAppFlowIfNeeded = StartRateAppFlowIfNeeded(
mailboxScreenViewsRepository,
featureFlagManager,
startRateAppFlow
)
@Test
fun `rate app flow is started when feature flag is true and mailbox screen views reached threshold`() = runTest {
// given
mockFeatureFlagValue(userId, true)
mockScreenViews(2)
// when
startRateAppFlowIfNeeded(userId)
// then
verify { startRateAppFlow() }
}
@Test
fun `rate app flow is not started when feature flag is false`() = runTest {
// given
mockFeatureFlagValue(userId, false)
mockScreenViews(2)
// when
startRateAppFlowIfNeeded(userId)
// then
verify { startRateAppFlow wasNot Called }
}
@Test
fun `rate app flow is not started when mailbox screen views are less than threshold`() = runTest {
// given
mockFeatureFlagValue(userId, true)
mockScreenViews(1)
// when
startRateAppFlowIfNeeded(userId)
// then
verify { startRateAppFlow wasNot Called }
}
@Test
fun `notify backend that the rate flow was started by disabling feature flag`() = runTest {
// given
mockFeatureFlagValue(userId, true)
mockScreenViews(2)
// when
startRateAppFlowIfNeeded(userId)
// then
val featureId = MailFeatureFlags.ShowReviewAppDialog.featureId
val featureFlag = FeatureFlag(userId, featureId, Scope.User, defaultValue = false, value = false)
coVerify { featureFlagManager.update(featureFlag) }
}
private suspend fun mockFeatureFlagValue(userId: UserId, isEnabled: Boolean) {
val featureId = MailFeatureFlags.ShowReviewAppDialog.featureId
coEvery {
featureFlagManager.getOrDefault(
userId = userId,
featureId = featureId,
default = FeatureFlag.default(featureId.id, false),
refresh = false
)
} returns FeatureFlag(userId, featureId, Scope.User, false, isEnabled)
}
private fun mockScreenViews(count: Int) {
every { mailboxScreenViewsRepository.screenViewCount } returns count
}
}

View File

@ -0,0 +1,105 @@
/*
* Copyright (c) 2022 Proton AG
*
* This file is part of 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.featureflags
import ch.protonmail.android.testdata.AccountTestData
import ch.protonmail.android.testdata.UserTestData
import io.mockk.Called
import io.mockk.coEvery
import io.mockk.coVerifyAll
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.domain.entity.UserId
import me.proton.core.featureflag.domain.FeatureFlagManager
import me.proton.core.featureflag.domain.entity.FeatureFlag
import me.proton.core.featureflag.domain.entity.Scope
import me.proton.core.util.kotlin.DefaultCoroutineScopeProvider
import me.proton.core.util.kotlin.DefaultDispatcherProvider
import org.junit.Test
class RefreshFeatureFlagsTest {
private val featureFlagManager: FeatureFlagManager = mockk()
private val accountManager: AccountManager = mockk()
private val refreshFeatureFlags = RefreshFeatureFlags(
DefaultCoroutineScopeProvider(DefaultDispatcherProvider()),
featureFlagManager,
accountManager
)
@Test
fun `does nothing when there are no accounts`() = runTest {
// given
coEvery { accountManager.getAccounts() } returns flowOf()
// when
refreshFeatureFlags.refresh()
// then
verify { featureFlagManager wasNot Called }
}
@Test
fun `refresh show rating feature flag for each existing user`() = runTest {
// given
showRatingsFlagForUserMocked(UserTestData.userId, false)
showRatingsFlagForUserMocked(UserTestData.secondaryUserId, true)
coEvery { accountManager.getAccounts() } returns flowOf(AccountTestData.accounts)
// when
refreshFeatureFlags.refresh()
// then
coVerifyAll {
showRatingsFlagFetchedForUser(UserTestData.userId)
showRatingsFlagFetchedForUser(UserTestData.secondaryUserId)
}
}
private fun showRatingsFlagForUserMocked(userId: UserId, isEnabled: Boolean) {
coEvery {
featureFlagManager.getOrDefault(
userId = userId,
featureId = showReviewAppDialogFeatureFlag.featureId,
default = FeatureFlag.default(showReviewAppDialogFeatureFlag.featureId.id, false),
refresh = true
)
} returns FeatureFlag(userId, showReviewAppDialogFeatureFlag.featureId, Scope.User, false, isEnabled)
}
private suspend fun showRatingsFlagFetchedForUser(userId: UserId) {
featureFlagManager.getOrDefault(
userId = userId,
featureId = showReviewAppDialogFeatureFlag.featureId,
default = FeatureFlag.default(showReviewAppDialogFeatureFlag.featureId.id, false),
refresh = true
)
}
companion object {
private val showReviewAppDialogFeatureFlag = MailFeatureFlags.ShowReviewAppDialog
}
}

View File

@ -41,6 +41,7 @@ import ch.protonmail.android.di.JobEntryPoint
import ch.protonmail.android.domain.loadMoreFlowOf
import ch.protonmail.android.domain.withLoadMore
import ch.protonmail.android.feature.NotLoggedIn
import ch.protonmail.android.feature.rating.usecase.StartRateAppFlowIfNeeded
import ch.protonmail.android.labels.domain.LabelRepository
import ch.protonmail.android.labels.domain.model.Label
import ch.protonmail.android.labels.domain.model.LabelId
@ -73,6 +74,7 @@ import ch.protonmail.android.usecase.delete.EmptyFolder
import ch.protonmail.android.usecase.message.ChangeMessagesReadStatus
import ch.protonmail.android.usecase.message.ChangeMessagesStarredStatus
import dagger.hilt.EntryPoints
import io.mockk.Called
import io.mockk.called
import io.mockk.coEvery
import io.mockk.coVerify
@ -82,6 +84,7 @@ import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
@ -178,6 +181,7 @@ class MailboxViewModelTest : ArchTest by ArchTest(),
private val fetchEventsAndReschedule: FetchEventsAndReschedule = mockk {
coEvery { this@mockk.invoke() } just runs
}
private val startRateAppFlowIfNeeded: StartRateAppFlowIfNeeded = mockk()
private lateinit var viewModel: MailboxViewModel
@ -272,7 +276,8 @@ class MailboxViewModelTest : ArchTest by ArchTest(),
getMailSettings = getMailSettings,
mailboxItemUiModelMapper = mailboxItemUiModelMapper,
fetchEventsAndReschedule = fetchEventsAndReschedule,
clearNotificationsForUser = clearNotificationsForUser
clearNotificationsForUser = clearNotificationsForUser,
startRateAppFlowIfNeeded = startRateAppFlowIfNeeded
)
}
@ -606,6 +611,30 @@ class MailboxViewModelTest : ArchTest by ArchTest(),
}
}
@Test
fun `calls to start rate app flow if needed delegates to use case`() = runTest {
// given
coEvery { startRateAppFlowIfNeeded.invoke(testUserId) } returns Unit
// when
viewModel.startRateAppFlowIfNeeded()
// then
coVerify { startRateAppFlowIfNeeded.invoke(testUserId) }
}
@Test
fun `start rate app flow if needed is not called when current user id is invalid`() = runTest {
// given
every { userManager.currentUserId } returns null
// when
viewModel.startRateAppFlowIfNeeded()
// then
verify { startRateAppFlowIfNeeded wasNot Called }
}
private fun List<MailboxItemUiModel>.toMailboxState(): MailboxListState.Data =
MailboxListState.Data(this, isFreshData = false, shouldResetPosition = true)

View File

@ -0,0 +1,74 @@
/*
* 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.testdata
import me.proton.core.account.domain.entity.Account
import me.proton.core.account.domain.entity.AccountDetails
import me.proton.core.account.domain.entity.AccountState.Ready
import me.proton.core.account.domain.entity.AccountType.Internal
import me.proton.core.account.domain.entity.SessionDetails
import me.proton.core.account.domain.entity.SessionState.Authenticated
import me.proton.core.network.domain.session.SessionId
object AccountTestData {
private const val RAW_USERNAME = "username"
private const val RAW_EMAIL = "email@protonmail.ch"
private const val INITIAL_EVENT_ID = "event_id"
val primaryAccount = Account(
userId = UserTestData.userId,
username = RAW_USERNAME,
email = RAW_EMAIL,
state = Ready,
sessionId = SessionId(UserTestData.userId.id),
sessionState = Authenticated,
details = AccountDetails(
null,
SessionDetails(
initialEventId = INITIAL_EVENT_ID,
requiredAccountType = Internal,
secondFactorEnabled = true,
twoPassModeEnabled = true,
password = null
)
)
)
val secondaryAccount = Account(
userId = UserTestData.secondaryUserId,
username = RAW_USERNAME,
email = RAW_EMAIL,
state = Ready,
sessionId = SessionId(UserTestData.secondaryUserId.id),
sessionState = Authenticated,
details = AccountDetails(
null,
SessionDetails(
initialEventId = INITIAL_EVENT_ID,
requiredAccountType = Internal,
secondFactorEnabled = true,
twoPassModeEnabled = true,
password = null
)
)
)
val accounts = listOf(primaryAccount, secondaryAccount)
}

View File

@ -28,7 +28,9 @@ import me.proton.core.domain.entity.UserId
object UserTestData {
private const val RAW_ID = "user_id"
private const val RAW_SECONDARY_USER_ID = "secondary_user_id"
val userId = UserId(RAW_ID)
val secondaryUserId = UserId(RAW_SECONDARY_USER_ID)
fun withAddresses(addressesList: Addresses): User = mockk {
every { addresses } returns addressesList

View File

@ -82,6 +82,7 @@ val DependencyHandler.`android-startup-runtime` get() = androidx("startup",
val DependencyHandler.`lifecycle-extensions` get() = androidxLifecycle("extensions") version `lifecycle-extensions version`
val DependencyHandler.`room-rxJava` get() = androidxRoom("rxjava2")
val DependencyHandler.`safetyNet` get() = playServices("safetynet")
val DependencyHandler.`google-play-review` get() = google("android.play", "review") version `google-play-core-libs`
fun DependencyHandler.googleServices(moduleSuffix: String? = null, version: String = `googleServices version`) =
google("gms", "google-services", moduleSuffix, version)

View File

@ -62,7 +62,7 @@ fun initVersions() {
}
// Proton Core
const val `Proton-core version` = "10.1.0"
const val `Proton-core version` = "10.2.0"
// Test
const val `aerogear version` = "1.0.0" // Released: Mar 23, 2013
@ -90,6 +90,7 @@ const val `flexbox version` = "2.0.1" // Released: Jan
const val `lifecycle-extensions version` = "2.2.0" // Released: Jan 22, 2020
const val `googleServices version` = "4.3.3" // Released: Nov 11, 2019
const val `playServices version` = "17.0.0" // Released: Jun 19, 2019
const val `google-play-core-libs` = "2.0.1"
// Other
const val `apache-commons-lang version` = "3.4" // Released: Apr 03, 2015