diff --git a/.idea/saveactions_settings.xml b/.idea/saveactions_settings.xml index 74bfed93c..d446172cc 100644 --- a/.idea/saveactions_settings.xml +++ b/.idea/saveactions_settings.xml @@ -13,6 +13,7 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f04e6f4cf..42a047724 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -357,6 +357,7 @@ dependencies { `constraint-layout`, `material`, `paging-runtime`, + `google-play-review`, // Lifecycle `lifecycle-extensions`, diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4505582d8..4899c5ee4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -144,6 +144,11 @@ android:name="ch.protonmail.android.onboarding.base.presentation.StartOnboardingObserverInitializer" android:value="androidx.startup" tools:node="remove" /> + + { diff --git a/app/src/main/java/ch/protonmail/android/di/ViewModelModule.kt b/app/src/main/java/ch/protonmail/android/di/ViewModelModule.kt index b53a2124d..b93fc6b65 100644 --- a/app/src/main/java/ch/protonmail/android/di/ViewModelModule.kt +++ b/app/src/main/java/ch/protonmail/android/di/ViewModelModule.kt @@ -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 ) } diff --git a/app/src/main/java/ch/protonmail/android/feature/rating/MailboxScreenViewInMemoryRepository.kt b/app/src/main/java/ch/protonmail/android/feature/rating/MailboxScreenViewInMemoryRepository.kt new file mode 100644 index 000000000..848bdce2e --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/feature/rating/MailboxScreenViewInMemoryRepository.kt @@ -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") + } + +} diff --git a/app/src/main/java/ch/protonmail/android/feature/rating/StartRateAppFlow.kt b/app/src/main/java/ch/protonmail/android/feature/rating/StartRateAppFlow.kt new file mode 100644 index 000000000..7f8ac4039 --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/feature/rating/StartRateAppFlow.kt @@ -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}") + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/ch/protonmail/android/feature/rating/usecase/StartRateAppFlowIfNeeded.kt b/app/src/main/java/ch/protonmail/android/feature/rating/usecase/StartRateAppFlowIfNeeded.kt new file mode 100644 index 000000000..784ca497e --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/feature/rating/usecase/StartRateAppFlowIfNeeded.kt @@ -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 + } +} diff --git a/app/src/main/java/ch/protonmail/android/featureflags/FeatureFlagsInitializer.kt b/app/src/main/java/ch/protonmail/android/featureflags/FeatureFlagsInitializer.kt new file mode 100644 index 000000000..43a26bd9a --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/featureflags/FeatureFlagsInitializer.kt @@ -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 { + + override fun create(context: Context): RefreshFeatureFlags { + val refreshFeatureFlags = EntryPointAccessors.fromApplication( + context.applicationContext, + FeatureFlagsEntryPoint::class.java + ).refreshFeatureFlags() + refreshFeatureFlags.refresh() + return refreshFeatureFlags + } + + override fun dependencies(): List>> = emptyList() + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface FeatureFlagsEntryPoint { + + fun refreshFeatureFlags(): RefreshFeatureFlags + } +} diff --git a/app/src/main/java/ch/protonmail/android/featureflags/MailFeatureFlags.kt b/app/src/main/java/ch/protonmail/android/featureflags/MailFeatureFlags.kt new file mode 100644 index 000000000..1076d48e2 --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/featureflags/MailFeatureFlags.kt @@ -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")) +} \ No newline at end of file diff --git a/app/src/main/java/ch/protonmail/android/featureflags/RefreshFeatureFlags.kt b/app/src/main/java/ch/protonmail/android/featureflags/RefreshFeatureFlags.kt new file mode 100644 index 000000000..5de04a291 --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/featureflags/RefreshFeatureFlags.kt @@ -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 + ) + } + +} diff --git a/app/src/main/java/ch/protonmail/android/mailbox/presentation/ui/MailboxActivity.kt b/app/src/main/java/ch/protonmail/android/mailbox/presentation/ui/MailboxActivity.kt index 1c2885d7c..7a80f9bac 100644 --- a/app/src/main/java/ch/protonmail/android/mailbox/presentation/ui/MailboxActivity.kt +++ b/app/src/main/java/ch/protonmail/android/mailbox/presentation/ui/MailboxActivity.kt @@ -778,6 +778,8 @@ internal class MailboxActivity : if (mailboxLocation == MessageLocationType.INBOX) { userManager.currentUserId?.let { mailboxViewModel.clearNotifications(it) } } + + mailboxViewModel.startRateAppFlowIfNeeded() } override fun onPause() { diff --git a/app/src/main/java/ch/protonmail/android/mailbox/presentation/viewmodel/MailboxViewModel.kt b/app/src/main/java/ch/protonmail/android/mailbox/presentation/viewmodel/MailboxViewModel.kt index aa8cdafde..6c6304722 100644 --- a/app/src/main/java/ch/protonmail/android/mailbox/presentation/viewmodel/MailboxViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/mailbox/presentation/viewmodel/MailboxViewModel.kt @@ -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>() @@ -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, diff --git a/app/src/test/java/ch/protonmail/android/feature/rating/MailboxScreenViewInMemoryRepositoryTest.kt b/app/src/test/java/ch/protonmail/android/feature/rating/MailboxScreenViewInMemoryRepositoryTest.kt new file mode 100644 index 000000000..9e260fb43 --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/feature/rating/MailboxScreenViewInMemoryRepositoryTest.kt @@ -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) + } + +} \ No newline at end of file diff --git a/app/src/test/java/ch/protonmail/android/feature/rating/StartRateAppFlowTest.kt b/app/src/test/java/ch/protonmail/android/feature/rating/StartRateAppFlowTest.kt new file mode 100644 index 000000000..3cd616ded --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/feature/rating/StartRateAppFlowTest.kt @@ -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 { + every { this@mockk.requestReviewFlow() } returns mockk(relaxed = true) + } + mockkStatic(ReviewManagerFactory::class) + every { ReviewManagerFactory.create(context) } returns reviewMangerMock + + // when + startRateAppFlow() + + // then + verify { reviewMangerMock.requestReviewFlow() } + } +} \ No newline at end of file diff --git a/app/src/test/java/ch/protonmail/android/feature/rating/usecase/StartRateAppFlowIfNeededTest.kt b/app/src/test/java/ch/protonmail/android/feature/rating/usecase/StartRateAppFlowIfNeededTest.kt new file mode 100644 index 000000000..2c5c106f7 --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/feature/rating/usecase/StartRateAppFlowIfNeededTest.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/test/java/ch/protonmail/android/featureflags/RefreshFeatureFlagsTest.kt b/app/src/test/java/ch/protonmail/android/featureflags/RefreshFeatureFlagsTest.kt new file mode 100644 index 000000000..745901951 --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/featureflags/RefreshFeatureFlagsTest.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/test/java/ch/protonmail/android/mailbox/presentation/viewmodel/MailboxViewModelTest.kt b/app/src/test/java/ch/protonmail/android/mailbox/presentation/viewmodel/MailboxViewModelTest.kt index 6ae614c8e..59c6ed0f8 100644 --- a/app/src/test/java/ch/protonmail/android/mailbox/presentation/viewmodel/MailboxViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/mailbox/presentation/viewmodel/MailboxViewModelTest.kt @@ -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.toMailboxState(): MailboxListState.Data = MailboxListState.Data(this, isFreshData = false, shouldResetPosition = true) diff --git a/app/src/test/java/ch/protonmail/android/testdata/AccountTestData.kt b/app/src/test/java/ch/protonmail/android/testdata/AccountTestData.kt new file mode 100644 index 000000000..2210d62c5 --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/testdata/AccountTestData.kt @@ -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 . + */ + +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) + +} diff --git a/app/src/test/java/ch/protonmail/android/testdata/UserTestData.kt b/app/src/test/java/ch/protonmail/android/testdata/UserTestData.kt index 782d2352f..3ef058f08 100644 --- a/app/src/test/java/ch/protonmail/android/testdata/UserTestData.kt +++ b/app/src/test/java/ch/protonmail/android/testdata/UserTestData.kt @@ -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 diff --git a/buildSrc/src/main/kotlin/libraries.kt b/buildSrc/src/main/kotlin/libraries.kt index 86cc7d88a..23d8f1683 100644 --- a/buildSrc/src/main/kotlin/libraries.kt +++ b/buildSrc/src/main/kotlin/libraries.kt @@ -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) diff --git a/buildSrc/src/main/kotlin/versionsConfig.kt b/buildSrc/src/main/kotlin/versionsConfig.kt index fb19ae8f7..ee5ba58fd 100644 --- a/buildSrc/src/main/kotlin/versionsConfig.kt +++ b/buildSrc/src/main/kotlin/versionsConfig.kt @@ -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