diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem/SettingsListItem.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem/SettingsListItem.kt index a9352c8aa72f..003f31e84fe6 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem/SettingsListItem.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem/SettingsListItem.kt @@ -76,8 +76,8 @@ class SettingsListItem @JvmOverloads constructor( } /** Sets the item click listener */ - fun setClickListener(onClick: () -> Unit) { - binding.root.setOnClickListener { onClick() } + fun setClickListener(onClick: (() -> Unit)?) { + binding.root.setOnClickListener { onClick?.invoke() } } /** Sets whether the status indicator is on or off */ diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionManager.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionManager.kt index 7d45824807f6..643b764de7d5 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionManager.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionManager.kt @@ -19,7 +19,6 @@ package com.duckduckgo.networkprotection.impl.subscription import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus -import com.duckduckgo.settings.api.NewSettingsFeature import com.duckduckgo.subscriptions.api.Product.NetP import com.duckduckgo.subscriptions.api.SubscriptionStatus import com.duckduckgo.subscriptions.api.Subscriptions @@ -38,8 +37,6 @@ interface NetpSubscriptionManager { EXPIRED, SIGNED_OUT, INACTIVE, - WAITING, - INELIGIBLE, } } @@ -55,7 +52,6 @@ fun VpnStatus.isExpired(): Boolean { class RealNetpSubscriptionManager @Inject constructor( private val subscriptions: Subscriptions, private val dispatcherProvider: DispatcherProvider, - private val newSettingsFeature: NewSettingsFeature, ) : NetpSubscriptionManager { override suspend fun getVpnStatus(): VpnStatus { @@ -75,29 +71,15 @@ class RealNetpSubscriptionManager @Inject constructor( private fun hasValidEntitlementFlow(): Flow = subscriptions.getEntitlementStatus().map { it.contains(NetP) } private suspend fun getVpnStatusInternal(hasValidEntitlement: Boolean): VpnStatus { - return if (newSettingsFeature.self().isEnabled()) { - when { - !hasValidEntitlement -> VpnStatus.INELIGIBLE - else -> { - when (subscriptions.getSubscriptionStatus()) { - SubscriptionStatus.INACTIVE, SubscriptionStatus.EXPIRED -> VpnStatus.EXPIRED - SubscriptionStatus.UNKNOWN -> VpnStatus.SIGNED_OUT - SubscriptionStatus.AUTO_RENEWABLE, SubscriptionStatus.NOT_AUTO_RENEWABLE, SubscriptionStatus.GRACE_PERIOD -> VpnStatus.ACTIVE - SubscriptionStatus.WAITING -> VpnStatus.WAITING - } - } - } - } else { - val subscriptionState = subscriptions.getSubscriptionStatus() - when (subscriptionState) { - SubscriptionStatus.INACTIVE, SubscriptionStatus.EXPIRED -> VpnStatus.EXPIRED - SubscriptionStatus.UNKNOWN -> VpnStatus.SIGNED_OUT - else -> { - if (hasValidEntitlement) { - VpnStatus.ACTIVE - } else { - VpnStatus.INACTIVE - } + val subscriptionState = subscriptions.getSubscriptionStatus() + return when (subscriptionState) { + SubscriptionStatus.INACTIVE, SubscriptionStatus.EXPIRED -> VpnStatus.EXPIRED + SubscriptionStatus.UNKNOWN -> VpnStatus.SIGNED_OUT + else -> { + if (hasValidEntitlement) { + VpnStatus.ACTIVE + } else { + VpnStatus.INACTIVE } } } diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsState.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsState.kt deleted file mode 100644 index 9d4df6906dec..000000000000 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsState.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.networkprotection.impl.subscription.settings - -import kotlinx.coroutines.flow.Flow - -interface NetworkProtectionSettingsState { - - /** - * Returns a flow of the visibility states of NetP - * The caller DOES NOT need to specify the dispatcher when calling this method - */ - suspend fun getNetPSettingsStateFlow(): Flow - - /** - * If the Netp Settings Item should be visible to the user and it's current subscription state - */ - sealed interface NetPSettingsState { - - sealed interface Visible : NetPSettingsState { - data object Subscribed : Visible - data object Expired : Visible - data object Activating : Visible - } - data object Hidden : NetPSettingsState - } -} diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImpl.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImpl.kt deleted file mode 100644 index a129e1208617..000000000000 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImpl.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.networkprotection.impl.subscription.settings - -import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.networkprotection.api.NetworkProtectionState -import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager -import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus -import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.ACTIVE -import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.EXPIRED -import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.INACTIVE -import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.INELIGIBLE -import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.SIGNED_OUT -import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.WAITING -import com.duckduckgo.networkprotection.impl.subscription.isActive -import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState -import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Hidden -import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Visible.Activating -import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Visible.Expired -import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Visible.Subscribed -import com.squareup.anvil.annotations.ContributesBinding -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map - -@ContributesBinding(AppScope::class) -class NetworkProtectionSettingsStateImpl @Inject constructor( - private val dispatcherProvider: DispatcherProvider, - private val networkProtectionState: NetworkProtectionState, - private val netpSubscriptionManager: NetpSubscriptionManager, -) : NetworkProtectionSettingsState { - - override suspend fun getNetPSettingsStateFlow(): Flow = - netpSubscriptionManager.vpnStatus().map { status -> - if (!status.isActive()) { - // if entitlement check succeeded and not an active subscription then reset state - handleRevokedVPNState() - } - - mapToSettingsState(status) - }.flowOn(dispatcherProvider.io()) - - private fun mapToSettingsState(vpnStatus: VpnStatus): NetPSettingsState = when (vpnStatus) { - ACTIVE -> Subscribed - INACTIVE, EXPIRED -> Expired - WAITING -> Activating - SIGNED_OUT, INELIGIBLE -> Hidden - } - - private suspend fun handleRevokedVPNState() { - if (networkProtectionState.isEnabled()) { - networkProtectionState.stop() - } - } -} diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPView.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPView.kt index dcfc63c9974c..2ff901da3050 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPView.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPView.kt @@ -35,10 +35,9 @@ import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNet import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Command.OpenNetPScreen import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Factory import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState -import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Activating -import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Expired +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Disabled +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Enabled import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden -import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Subscribed import dagger.android.support.AndroidSupportInjection import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -79,7 +78,7 @@ class ProSettingNetPView @JvmOverloads constructor( coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) viewModel.viewState - .onEach { updateNetPSettings(it.networkProtectionEntryState) } + .onEach { updateNetPSettings(it.netPEntryState) } .launchIn(coroutineScope!!) viewModel.commands() @@ -91,15 +90,14 @@ class ProSettingNetPView @JvmOverloads constructor( with(binding.netpPSetting) { when (networkProtectionEntryState) { Hidden -> isGone = true - Activating, - Expired, - -> { + is Disabled -> { isVisible = true isClickable = false + setClickListener(null) setLeadingIconResource(R.drawable.ic_vpn_grayscale_color_24) setStatus(isOn = false) } - is Subscribed -> { + is Enabled -> { isVisible = true isClickable = true setClickListener { viewModel.onNetPSettingClicked() } diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModel.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModel.kt index 303b10d7af77..a5881ddeb1de 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModel.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModel.kt @@ -29,12 +29,10 @@ import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState import com.duckduckgo.networkprotection.api.NetworkProtectionState import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_SETTINGS_PRESSED -import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState -import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Hidden -import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Visible.Activating -import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Visible.Expired -import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Visible.Subscribed import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Command.OpenNetPScreen +import com.duckduckgo.subscriptions.api.Product.NetP +import com.duckduckgo.subscriptions.api.SubscriptionStatus +import com.duckduckgo.subscriptions.api.Subscriptions import javax.inject.Inject import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel @@ -44,30 +42,32 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import logcat.logcat @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle class ProSettingNetPViewModel( - private val networkProtectionSettingsState: NetworkProtectionSettingsState, private val networkProtectionState: NetworkProtectionState, private val networkProtectionAccessState: NetworkProtectionAccessState, + private val subscriptions: Subscriptions, private val dispatcherProvider: DispatcherProvider, private val pixel: Pixel, ) : ViewModel(), DefaultLifecycleObserver { - data class ViewState(val networkProtectionEntryState: NetPEntryState = NetPEntryState.Hidden) + data class ViewState(val netPEntryState: NetPEntryState = NetPEntryState.Hidden) sealed class Command { data class OpenNetPScreen(val params: ActivityParams) : Command() } sealed class NetPEntryState { + data object Hidden : NetPEntryState() - data class Subscribed(val isActive: Boolean) : NetPEntryState() - data object Expired : NetPEntryState() - data object Activating : NetPEntryState() + data class Enabled(val isActive: Boolean) : NetPEntryState() + data object Disabled : NetPEntryState() } private val command = Channel(1, BufferOverflow.DROP_OLDEST) @@ -78,17 +78,56 @@ class ProSettingNetPViewModel( override fun onStart(owner: LifecycleOwner) { super.onStart(owner) - viewModelScope.launch { - combine( - networkProtectionSettingsState.getNetPSettingsStateFlow(), - networkProtectionState.getConnectionStateFlow(), - ) { accessState, connectionState -> - _viewState.emit( - viewState.value.copy( - networkProtectionEntryState = getNetworkProtectionEntryState(accessState, connectionState), - ), - ) - }.flowOn(dispatcherProvider.main()).launchIn(viewModelScope) + combine( + subscriptions.getEntitlementStatus().map { entitledProducts -> entitledProducts.contains(NetP) }, + networkProtectionState.getConnectionStateFlow(), + ) { netpEntitlementStatus, connectionState -> + + val subscriptionStatus = subscriptions.getSubscriptionStatus() + + val netPEntryState = getNetpEntryState(netpEntitlementStatus, connectionState, subscriptionStatus) + + _viewState.update { it.copy(netPEntryState = netPEntryState) } + } + .flowOn(dispatcherProvider.main()) + .launchIn(viewModelScope) + } + + private suspend fun getNetpEntryState( + netpEntitlementStatus: Boolean, + connectionState: ConnectionState, + subscriptionStatus: SubscriptionStatus, + ): NetPEntryState { + return when (subscriptionStatus) { + SubscriptionStatus.UNKNOWN -> { + handleRevokedVPNState() + NetPEntryState.Hidden + } + + SubscriptionStatus.INACTIVE, + SubscriptionStatus.EXPIRED, + SubscriptionStatus.WAITING, + -> { + if (hasNetpProduct()) { + NetPEntryState.Disabled + } else { + handleRevokedVPNState() + NetPEntryState.Hidden + } + } + + SubscriptionStatus.AUTO_RENEWABLE, + SubscriptionStatus.NOT_AUTO_RENEWABLE, + SubscriptionStatus.GRACE_PERIOD, + -> { + if (netpEntitlementStatus) { + NetPEntryState.Enabled(isActive = connectionState.isConnected()) + } else { + // ensure VPN is stopped in case entitlement is revoked + handleRevokedVPNState() + NetPEntryState.Hidden + } + } } } @@ -102,22 +141,22 @@ class ProSettingNetPViewModel( } } - private fun getNetworkProtectionEntryState( - settingsState: NetPSettingsState, - networkProtectionConnectionState: ConnectionState, - ): NetPEntryState = - when (settingsState) { - Hidden -> NetPEntryState.Hidden - Subscribed -> NetPEntryState.Subscribed(isActive = networkProtectionConnectionState.isConnected()) - Activating -> NetPEntryState.Activating - Expired -> NetPEntryState.Expired + private suspend fun hasNetpProduct(): Boolean { + val products = subscriptions.getAvailableProducts() + return products.contains(NetP) + } + + private suspend fun handleRevokedVPNState() { + if (networkProtectionState.isEnabled()) { + networkProtectionState.stop() } + } @Suppress("UNCHECKED_CAST") class Factory @Inject constructor( - private val networkProtectionSettingsState: NetworkProtectionSettingsState, private val networkProtectionState: NetworkProtectionState, private val networkProtectionAccessState: NetworkProtectionAccessState, + private val subscriptions: Subscriptions, private val dispatcherProvider: DispatcherProvider, private val pixel: Pixel, ) : ViewModelProvider.NewInstanceFactory() { @@ -125,9 +164,9 @@ class ProSettingNetPViewModel( return with(modelClass) { when { isAssignableFrom(ProSettingNetPViewModel::class.java) -> ProSettingNetPViewModel( - networkProtectionSettingsState, networkProtectionState, networkProtectionAccessState, + subscriptions, dispatcherProvider, pixel, ) diff --git a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImplTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImplTest.kt index c95f916d502d..f4844bf04b79 100644 --- a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImplTest.kt +++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImplTest.kt @@ -1,101 +1,11 @@ package com.duckduckgo.networkprotection.impl.subscription.settings -import app.cash.turbine.test -import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.networkprotection.api.NetworkProtectionState import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus -import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -class NetworkProtectionSettingsStateImplTest { - - @get:Rule - val coroutineTestRule = CoroutineTestRule() - - private lateinit var networkProtectionSettingsState: NetworkProtectionSettingsStateImpl - private lateinit var fakeNetworkProtectionState: FakeNetworkProtectionState - private lateinit var fakeNetpSubscriptionManager: FakeNetpSubscriptionManager - - @Before - fun setUp() { - fakeNetworkProtectionState = FakeNetworkProtectionState() - fakeNetpSubscriptionManager = FakeNetpSubscriptionManager() - - networkProtectionSettingsState = NetworkProtectionSettingsStateImpl( - dispatcherProvider = coroutineTestRule.testDispatcherProvider, - networkProtectionState = fakeNetworkProtectionState, - netpSubscriptionManager = fakeNetpSubscriptionManager, - ) - } - - @Test - fun `when VpnStatus is active then returns subscribed`() = runTest { - fakeNetpSubscriptionManager.setVpnStatus(VpnStatus.ACTIVE) - - networkProtectionSettingsState.getNetPSettingsStateFlow().test { - assertEquals(NetPSettingsState.Visible.Subscribed, awaitItem()) - awaitComplete() - } - } - - @Test - fun `when VpnStatus is inactive then returns expired`() = runTest { - fakeNetpSubscriptionManager.setVpnStatus(VpnStatus.INACTIVE) - - networkProtectionSettingsState.getNetPSettingsStateFlow().test { - assertEquals(NetPSettingsState.Visible.Expired, awaitItem()) - awaitComplete() - } - } - - @Test - fun `when VpnStatus is expired then returns expired`() = runTest { - fakeNetpSubscriptionManager.setVpnStatus(VpnStatus.EXPIRED) - - networkProtectionSettingsState.getNetPSettingsStateFlow().test { - assertEquals(NetPSettingsState.Visible.Expired, awaitItem()) - awaitComplete() - } - } - - @Test - fun `when VpnStatus is activating then returns waiting`() = runTest { - fakeNetpSubscriptionManager.setVpnStatus(VpnStatus.WAITING) - - networkProtectionSettingsState.getNetPSettingsStateFlow().test { - assertEquals(NetPSettingsState.Visible.Activating, awaitItem()) - awaitComplete() - } - } - - @Test - fun `when VpnStatus is signed out then returns hidden`() = runTest { - fakeNetpSubscriptionManager.setVpnStatus(VpnStatus.SIGNED_OUT) - - networkProtectionSettingsState.getNetPSettingsStateFlow().test { - assertEquals(NetPSettingsState.Hidden, awaitItem()) - awaitComplete() - } - } - - @Test - fun `when VpnStatus is ineligible then returns hidden`() = runTest { - fakeNetpSubscriptionManager.setVpnStatus(VpnStatus.INELIGIBLE) - - networkProtectionSettingsState.getNetPSettingsStateFlow().test { - assertEquals(NetPSettingsState.Hidden, awaitItem()) - awaitComplete() - } - } -} private class FakeNetworkProtectionState : NetworkProtectionState { override suspend fun isOnboarded(): Boolean = false diff --git a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModelTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModelTest.kt index 68d72548e687..92e8b91c50bb 100644 --- a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModelTest.kt +++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModelTest.kt @@ -23,15 +23,21 @@ import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState import com.duckduckgo.networkprotection.api.NetworkProtectionState import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.CONNECTED -import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.CONNECTING import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.DISCONNECTED import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_SETTINGS_PRESSED -import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Command -import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Activating -import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Expired +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Disabled +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Enabled import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden -import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Subscribed +import com.duckduckgo.subscriptions.api.Product +import com.duckduckgo.subscriptions.api.SubscriptionStatus.AUTO_RENEWABLE +import com.duckduckgo.subscriptions.api.SubscriptionStatus.EXPIRED +import com.duckduckgo.subscriptions.api.SubscriptionStatus.GRACE_PERIOD +import com.duckduckgo.subscriptions.api.SubscriptionStatus.INACTIVE +import com.duckduckgo.subscriptions.api.SubscriptionStatus.NOT_AUTO_RENEWABLE +import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN +import com.duckduckgo.subscriptions.api.SubscriptionStatus.WAITING +import com.duckduckgo.subscriptions.api.Subscriptions import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Assert.* @@ -50,22 +56,22 @@ class ProSettingNetPViewModelTest { private val pixel: Pixel = mock() private val networkProtectionState: NetworkProtectionState = mock() private val networkProtectionAccessState: NetworkProtectionAccessState = mock() - private val networkProtectionSettingsState: NetworkProtectionSettingsState = mock() + private val subscriptions: Subscriptions = mock() private lateinit var proSettingNetPViewModel: ProSettingNetPViewModel @Before fun before() { proSettingNetPViewModel = ProSettingNetPViewModel( - networkProtectionSettingsState, networkProtectionState, networkProtectionAccessState, + subscriptions, coroutineTestRule.testDispatcherProvider, pixel, ) } @Test - fun whenNetPSettingClickedThenNetPScreenOpened() = runTest { + fun `when netp Setting clicked then netp screen is opened`() = runTest { val testScreen = object : ActivityParams {} whenever(networkProtectionAccessState.getScreenForCurrentState()).thenReturn(testScreen) @@ -80,92 +86,367 @@ class ProSettingNetPViewModelTest { } @Test - fun whenNetPVisibilityStateIsHiddenThenNetPEntryStateIsHidden() = runTest { + fun `when subscription state is unknown then NetpEntryState is hidden`() = runTest { whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) - whenever(networkProtectionSettingsState.getNetPSettingsStateFlow()).thenReturn(flowOf(NetPSettingsState.Hidden)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(UNKNOWN) proSettingNetPViewModel.onStart(mock()) proSettingNetPViewModel.viewState.test { assertEquals( Hidden, - expectMostRecentItem().networkProtectionEntryState, + expectMostRecentItem().netPEntryState, ) } } @Test - fun whenNetPVisibilityStateIsActivatingThenNetPEntryStateIsActivating() = runTest { + fun `when subscription state is unknown and vpn is enabled then vpn is stopped`() = runTest { whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) - whenever(networkProtectionSettingsState.getNetPSettingsStateFlow()).thenReturn(flowOf(NetPSettingsState.Visible.Activating)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(UNKNOWN) + + proSettingNetPViewModel.onStart(mock()) + + verify(networkProtectionState).stop() + } + + @Test + fun `when subscription state is inactive and no netp product available then NetpEntryState is hidden`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(INACTIVE) + whenever(subscriptions.getAvailableProducts()).thenReturn(emptySet()) + + proSettingNetPViewModel.onStart(mock()) + + proSettingNetPViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().netPEntryState, + ) + } + } + + @Test + fun `when subscription state is inactive and netp product available then NetpEntryState is disabled`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(INACTIVE) + whenever(subscriptions.getAvailableProducts()).thenReturn(setOf(Product.NetP)) + + proSettingNetPViewModel.onStart(mock()) + + proSettingNetPViewModel.viewState.test { + assertEquals( + Disabled, + expectMostRecentItem().netPEntryState, + ) + } + } + + @Test + fun `when subscription state is inactive and vpn is enabled then vpn is stopped`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(INACTIVE) + whenever(subscriptions.getAvailableProducts()).thenReturn(emptySet()) + + proSettingNetPViewModel.onStart(mock()) + + verify(networkProtectionState).stop() + } + + @Test + fun `when subscription state is expired and no netp product available then NetpEntryState is hidden`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(EXPIRED) + whenever(subscriptions.getAvailableProducts()).thenReturn(emptySet()) + + proSettingNetPViewModel.onStart(mock()) + + proSettingNetPViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().netPEntryState, + ) + } + } + + @Test + fun `when subscription state is expired and netp product available then NetpEntryState is disabled`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(EXPIRED) + whenever(subscriptions.getAvailableProducts()).thenReturn(setOf(Product.NetP)) + + proSettingNetPViewModel.onStart(mock()) + + proSettingNetPViewModel.viewState.test { + assertEquals( + Disabled, + expectMostRecentItem().netPEntryState, + ) + } + } + + @Test + fun `when subscription state is expired and vpn is enabled then vpn is stopped`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(EXPIRED) + whenever(subscriptions.getAvailableProducts()).thenReturn(emptySet()) + + proSettingNetPViewModel.onStart(mock()) + + verify(networkProtectionState).stop() + } + + @Test + fun `when subscription state is waiting and no netp product available then NetpEntryState is hidden`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(WAITING) + whenever(subscriptions.getAvailableProducts()).thenReturn(emptySet()) + + proSettingNetPViewModel.onStart(mock()) + + proSettingNetPViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().netPEntryState, + ) + } + } + + @Test + fun `when subscription state is waiting and netp product available then NetpEntryState is disabled`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(WAITING) + whenever(subscriptions.getAvailableProducts()).thenReturn(setOf(Product.NetP)) + + proSettingNetPViewModel.onStart(mock()) + + proSettingNetPViewModel.viewState.test { + assertEquals( + Disabled, + expectMostRecentItem().netPEntryState, + ) + } + } + + @Test + fun `when subscription state is waiting and vpn is enabled then vpn is stopped`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(WAITING) + whenever(subscriptions.getAvailableProducts()).thenReturn(emptySet()) + + proSettingNetPViewModel.onStart(mock()) + + verify(networkProtectionState).stop() + } + + @Test + fun `when subscription state is auto renewable and entitled then NetpEntryState is enabled`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.NetP))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(AUTO_RENEWABLE) proSettingNetPViewModel.onStart(mock()) proSettingNetPViewModel.viewState.test { assertEquals( - Activating, - expectMostRecentItem().networkProtectionEntryState, + Enabled(isActive = false), + expectMostRecentItem().netPEntryState, ) } } @Test - fun whenNetPVisibilityStateConnectedAndAccessStateIsSubscribedThenNetPEntryStateIsSubscribedAndActive() = runTest { + fun `when subscription state is auto renewable and entitled and connection is active then NetpEntryState is enabled and active`() = runTest { whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(CONNECTED)) - whenever(networkProtectionSettingsState.getNetPSettingsStateFlow()).thenReturn(flowOf(NetPSettingsState.Visible.Subscribed)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.NetP))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(AUTO_RENEWABLE) + + proSettingNetPViewModel.onStart(mock()) + + proSettingNetPViewModel.viewState.test { + assertEquals( + Enabled(isActive = true), + expectMostRecentItem().netPEntryState, + ) + } + } + + @Test + fun `when subscription state is auto renewable and not entitled then NetpEntryState is hidden`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(AUTO_RENEWABLE) proSettingNetPViewModel.onStart(mock()) proSettingNetPViewModel.viewState.test { assertEquals( - Subscribed(isActive = true), - expectMostRecentItem().networkProtectionEntryState, + Hidden, + expectMostRecentItem().netPEntryState, ) } } @Test - fun whenNetPVisibilityStateDisconnectedAndAccessStateIsSubscribedThenNetPEntryStateIsSubscribedAndInactive() = runTest { + fun `when subscription state is auto renewable and not entitled then vpn is stopped`() = runTest { whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) - whenever(networkProtectionSettingsState.getNetPSettingsStateFlow()).thenReturn(flowOf(NetPSettingsState.Visible.Subscribed)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(AUTO_RENEWABLE) + + proSettingNetPViewModel.onStart(mock()) + + verify(networkProtectionState).stop() + } + + @Test + fun `when subscription state is not auto renewable and entitled then NetpEntryState is enabled`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.NetP))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(NOT_AUTO_RENEWABLE) proSettingNetPViewModel.onStart(mock()) proSettingNetPViewModel.viewState.test { assertEquals( - Subscribed(isActive = false), - expectMostRecentItem().networkProtectionEntryState, + Enabled(isActive = false), + expectMostRecentItem().netPEntryState, ) } } @Test - fun whenNetPVisibilityStateIsConnectingThenNetPEntryStateIsSubscribedAndNotActive() = runTest { - whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(CONNECTING)) - whenever(networkProtectionSettingsState.getNetPSettingsStateFlow()).thenReturn(flowOf(NetPSettingsState.Visible.Subscribed)) + fun `when subscription state is not auto renewable and entitled and connection is active then NetpEntryState is enabled and active`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(CONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.NetP))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(NOT_AUTO_RENEWABLE) proSettingNetPViewModel.onStart(mock()) proSettingNetPViewModel.viewState.test { assertEquals( - Subscribed(isActive = false), - expectMostRecentItem().networkProtectionEntryState, + Enabled(isActive = true), + expectMostRecentItem().netPEntryState, ) } } @Test - fun whenNetPVisibilityStateIsExpiredThenNetPEntryStateIsExpired() = runTest { + fun `when subscription state is not auto renewable and not entitled then NetpEntryState is hidden`() = runTest { whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) - whenever(networkProtectionSettingsState.getNetPSettingsStateFlow()).thenReturn(flowOf(NetPSettingsState.Visible.Expired)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(NOT_AUTO_RENEWABLE) proSettingNetPViewModel.onStart(mock()) proSettingNetPViewModel.viewState.test { assertEquals( - Expired, - expectMostRecentItem().networkProtectionEntryState, + Hidden, + expectMostRecentItem().netPEntryState, ) } } + + @Test + fun `when subscription state is not auto renewable and not entitled then vpn is stopped`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(NOT_AUTO_RENEWABLE) + + proSettingNetPViewModel.onStart(mock()) + + verify(networkProtectionState).stop() + } + + @Test + fun `when subscription state is grace period and entitled then NetpEntryState is enabled`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.NetP))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(GRACE_PERIOD) + + proSettingNetPViewModel.onStart(mock()) + + proSettingNetPViewModel.viewState.test { + assertEquals( + Enabled(isActive = false), + expectMostRecentItem().netPEntryState, + ) + } + } + + @Test + fun `when subscription state is grace period and entitled and connection is active then NetpEntryState is enabled and active`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(CONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.NetP))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(GRACE_PERIOD) + + proSettingNetPViewModel.onStart(mock()) + + proSettingNetPViewModel.viewState.test { + assertEquals( + Enabled(isActive = true), + expectMostRecentItem().netPEntryState, + ) + } + } + + @Test + fun `when subscription state is grace period and not entitled then NetpEntryState is hidden`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(GRACE_PERIOD) + + proSettingNetPViewModel.onStart(mock()) + + proSettingNetPViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().netPEntryState, + ) + } + } + + @Test + fun `when subscription state is grace period and not entitled then vpn is stopped`() = runTest { + whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(GRACE_PERIOD) + + proSettingNetPViewModel.onStart(mock()) + + verify(networkProtectionState).stop() + } } diff --git a/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/Subscriptions.kt b/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/Subscriptions.kt index b6675f38ca5f..e8fee1710a3c 100644 --- a/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/Subscriptions.kt +++ b/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/Subscriptions.kt @@ -54,6 +54,14 @@ interface Subscriptions { */ suspend fun getSubscriptionStatus(): SubscriptionStatus + /** + * This is a suspend function because we access disk IO + * You DO NOT need to set any dispatcher to call this suspend function + * + * @return a [Set] of available products for the subscription or an empty set if subscription is not available + */ + suspend fun getAvailableProducts(): Set + /** * @return `true` if the given URL can be handled internally or `false` otherwise */ diff --git a/subscriptions/subscriptions-dummy-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsDummy.kt b/subscriptions/subscriptions-dummy-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsDummy.kt index 267d2ca5835c..df49c95c219e 100644 --- a/subscriptions/subscriptions-dummy-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsDummy.kt +++ b/subscriptions/subscriptions-dummy-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsDummy.kt @@ -40,6 +40,8 @@ class SubscriptionsDummy @Inject constructor() : Subscriptions { override suspend fun getSubscriptionStatus(): SubscriptionStatus = UNKNOWN + override suspend fun getAvailableProducts(): Set = emptySet() + override fun shouldLaunchPrivacyProForUrl(url: String): Boolean = false override fun launchPrivacyPro( diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt index fbd17b79bd1f..93ffac1c18a5 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt @@ -85,6 +85,12 @@ class RealSubscriptions @Inject constructor( return subscriptionsManager.subscriptionStatus() } + override suspend fun getAvailableProducts(): Set { + return subscriptionsManager.getFeatures() + .mapNotNull { feature -> Product.entries.firstOrNull { it.value == feature } } + .toSet() + } + override fun launchPrivacyPro(context: Context, uri: Uri?) { val origin = uri?.getQueryParameter("origin") val settings = globalActivityStarter.startIntent(context, SettingsScreenNoParams) ?: return diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt index 5128b240e557..09879611415c 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt @@ -166,6 +166,11 @@ interface SubscriptionsManager { */ suspend fun subscriptionStatus(): SubscriptionStatus + /** + * Returns a [Set] of available features for the subscription or an empty set if subscription is not available + */ + suspend fun getFeatures(): Set + /** * Checks if user is signed in or not (using either auth API v1 or v2) */ @@ -447,6 +452,16 @@ class RealSubscriptionsManager @Inject constructor( } } + override suspend fun getFeatures(): Set { + val subscription = authRepository.getSubscription() + + return if (subscription != null) { + getFeaturesInternal(subscription.productId) + } else { + emptySet() + } + } + @Deprecated("This method will be removed after migrating to auth v2") override suspend fun exchangeAuthToken(authToken: String): String { val accessToken = authService.accessToken("Bearer $authToken").accessToken @@ -678,15 +693,7 @@ class RealSubscriptionsManager @Inject constructor( ) } - val features = if (privacyProFeature.get().featuresApi().isEnabled()) { - authRepository.getFeatures(offer.basePlanId) - } else { - when (offer.basePlanId) { - MONTHLY_PLAN_US, YEARLY_PLAN_US -> setOf(LEGACY_FE_NETP, LEGACY_FE_PIR, LEGACY_FE_ITR) - MONTHLY_PLAN_ROW, YEARLY_PLAN_ROW -> setOf(NETP, ROW_ITR) - else -> throw IllegalStateException() - } - } + val features = getFeaturesInternal(offer.basePlanId) if (features.isEmpty()) return@let emptyList() @@ -699,6 +706,18 @@ class RealSubscriptionsManager @Inject constructor( } } + private suspend fun getFeaturesInternal(planId: String): Set { + return if (privacyProFeature.get().featuresApi().isEnabled()) { + authRepository.getFeatures(planId) + } else { + when (planId) { + MONTHLY_PLAN_US, YEARLY_PLAN_US -> setOf(LEGACY_FE_NETP, LEGACY_FE_PIR, LEGACY_FE_ITR) + MONTHLY_PLAN_ROW, YEARLY_PLAN_ROW -> setOf(NETP, ROW_ITR) + else -> throw IllegalStateException() + } + } + } + override suspend fun purchase( activity: Activity, planId: String, diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingView.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingView.kt index 3029d9c278a0..8dd9209930eb 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingView.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingView.kt @@ -100,17 +100,18 @@ class ItrSettingView @JvmOverloads constructor( private fun renderView(viewState: ViewState) { with(binding.itrSettings) { when (viewState.itrState) { - is ItrState.Subscribed -> { + is ItrState.Enabled -> { isVisible = true setStatus(isOn = true) setLeadingIconResource(R.drawable.ic_identity_theft_restoration_color_24) isClickable = true setClickListener { viewModel.onItr() } } - ItrState.Expired, ItrState.Activating -> { + ItrState.Disabled -> { isVisible = true isClickable = false setStatus(isOn = false) + setClickListener(null) setLeadingIconResource(R.drawable.ic_identity_theft_restoration_grayscale_color_24) } ItrState.Hidden -> isGone = true diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModel.kt index ec91328297ca..43fa33bbf7a2 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModel.kt @@ -25,13 +25,8 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.subscriptions.api.Product.ITR import com.duckduckgo.subscriptions.api.Product.ROW_ITR -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager.ProductStatus.ACTIVE -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager.ProductStatus.EXPIRED -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager.ProductStatus.INACTIVE -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager.ProductStatus.INELIGIBLE -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager.ProductStatus.SIGNED_OUT -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager.ProductStatus.WAITING +import com.duckduckgo.subscriptions.api.SubscriptionStatus +import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.Command.OpenItr import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.ViewState.ItrState @@ -42,6 +37,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update @@ -50,7 +46,7 @@ import kotlinx.coroutines.launch @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle @ContributesViewModel(ViewScope::class) class ItrSettingViewModel @Inject constructor( - private val productSubscriptionManager: ProductSubscriptionManager, + private val subscriptions: Subscriptions, private val pixelSender: SubscriptionPixelSender, ) : ViewModel(), DefaultLifecycleObserver { @@ -65,9 +61,8 @@ class ItrSettingViewModel @Inject constructor( sealed class ItrState { data object Hidden : ItrState() - data object Subscribed : ItrState() - data object Expired : ItrState() - data object Activating : ItrState() + data object Enabled : ItrState() + data object Disabled : ItrState() } } @@ -82,16 +77,53 @@ class ItrSettingViewModel @Inject constructor( override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) - productSubscriptionManager.entitlementStatus(ITR, ROW_ITR).onEach { status -> - val itrState = when (status) { - ACTIVE -> ItrState.Subscribed - INACTIVE, EXPIRED -> ItrState.Expired - WAITING -> ItrState.Activating - SIGNED_OUT, INELIGIBLE -> ItrState.Hidden + subscriptions.getEntitlementStatus() + .map { entitledProducts -> + entitledProducts.any { product -> product == ITR || product == ROW_ITR } } + .onEach { hasValidEntitlement -> + val subscriptionStatus = subscriptions.getSubscriptionStatus() - _viewState.update { it.copy(itrState = itrState) } - }.launchIn(viewModelScope) + val itrState = getItrState(hasValidEntitlement, subscriptionStatus) + + _viewState.update { it.copy(itrState = itrState) } + } + .launchIn(viewModelScope) + } + + private suspend fun getItrState( + hasValidEntitlement: Boolean, + subscriptionStatus: SubscriptionStatus, + ): ItrState { + return when (subscriptionStatus) { + SubscriptionStatus.UNKNOWN -> ItrState.Hidden + + SubscriptionStatus.INACTIVE, + SubscriptionStatus.EXPIRED, + SubscriptionStatus.WAITING, + -> { + if (isItrAvailable()) { + ItrState.Disabled + } else { + ItrState.Hidden + } + } + + SubscriptionStatus.AUTO_RENEWABLE, + SubscriptionStatus.NOT_AUTO_RENEWABLE, + SubscriptionStatus.GRACE_PERIOD, + -> { + if (hasValidEntitlement) { + ItrState.Enabled + } else { + ItrState.Hidden + } + } + } + } + + private suspend fun isItrAvailable(): Boolean { + return subscriptions.getAvailableProducts().any { feature -> feature == ITR || feature == ROW_ITR } } private fun sendCommand(newCommand: Command) { diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingView.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingView.kt index 620e7172f5e7..4d8b37089dfd 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingView.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingView.kt @@ -37,10 +37,9 @@ import com.duckduckgo.subscriptions.impl.pir.PirActivity.Companion.PirScreenWith import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.Command import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.Command.OpenPir import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState -import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState.PirState.Activating -import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState.PirState.Expired +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState.PirState.Disabled +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState.PirState.Enabled import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState.PirState.Hidden -import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState.PirState.Subscribed import dagger.android.support.AndroidSupportInjection import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -102,17 +101,18 @@ class PirSettingView @JvmOverloads constructor( private fun renderView(viewState: ViewState) { with(binding.pirSettings) { when (viewState.pirState) { - is Subscribed -> { + is Enabled -> { isVisible = true setStatus(isOn = true) setLeadingIconResource(R.drawable.ic_identity_blocked_pir_color_24) isClickable = true binding.pirSettings.setClickListener { viewModel.onPir() } } - Expired, Activating -> { + is Disabled -> { isVisible = true isClickable = false setStatus(isOn = false) + binding.pirSettings.setClickListener(null) setLeadingIconResource(R.drawable.ic_identity_blocked_pir_grayscale_color_24) } Hidden -> isGone = true diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModel.kt index ab54d099eba8..07c6145a6bae 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModel.kt @@ -24,13 +24,8 @@ import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.subscriptions.api.Product.PIR -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager.ProductStatus.ACTIVE -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager.ProductStatus.EXPIRED -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager.ProductStatus.INACTIVE -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager.ProductStatus.INELIGIBLE -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager.ProductStatus.SIGNED_OUT -import com.duckduckgo.subscriptions.impl.ProductSubscriptionManager.ProductStatus.WAITING +import com.duckduckgo.subscriptions.api.SubscriptionStatus +import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.Command.OpenPir import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState.PirState @@ -41,6 +36,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update @@ -49,8 +45,8 @@ import kotlinx.coroutines.launch @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle @ContributesViewModel(ViewScope::class) class PirSettingViewModel @Inject constructor( - private val productSubscriptionManager: ProductSubscriptionManager, private val pixelSender: SubscriptionPixelSender, + private val subscriptions: Subscriptions, ) : ViewModel(), DefaultLifecycleObserver { sealed class Command { @@ -64,9 +60,8 @@ class PirSettingViewModel @Inject constructor( sealed class PirState { data object Hidden : PirState() - data object Subscribed : PirState() - data object Expired : PirState() - data object Activating : PirState() + data object Enabled : PirState() + data object Disabled : PirState() } } @@ -81,16 +76,51 @@ class PirSettingViewModel @Inject constructor( override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) - productSubscriptionManager.entitlementStatus(PIR).onEach { status -> - val pirState = when (status) { - ACTIVE -> PirState.Subscribed - INACTIVE, EXPIRED -> PirState.Expired - WAITING -> PirState.Activating - SIGNED_OUT, INELIGIBLE -> PirState.Hidden + subscriptions.getEntitlementStatus().map { entitledProducts -> entitledProducts.contains(PIR) } + .onEach { hasValidEntitlement -> + + val subscriptionStatus = subscriptions.getSubscriptionStatus() + + val pirState = getPirState(hasValidEntitlement, subscriptionStatus) + + _viewState.update { it.copy(pirState = pirState) } } + .launchIn(viewModelScope) + } + + private suspend fun getPirState( + hasValidEntitlement: Boolean, + subscriptionStatus: SubscriptionStatus, + ): PirState { + return when (subscriptionStatus) { + SubscriptionStatus.UNKNOWN -> PirState.Hidden + + SubscriptionStatus.INACTIVE, + SubscriptionStatus.EXPIRED, + SubscriptionStatus.WAITING, + -> { + if (isPirAvailable()) { + PirState.Disabled + } else { + PirState.Hidden + } + } + + SubscriptionStatus.AUTO_RENEWABLE, + SubscriptionStatus.NOT_AUTO_RENEWABLE, + SubscriptionStatus.GRACE_PERIOD, + -> { + if (hasValidEntitlement) { + PirState.Enabled + } else { + PirState.Hidden + } + } + } + } - _viewState.update { it.copy(pirState = pirState) } - }.launchIn(viewModelScope) + private suspend fun isPirAvailable(): Boolean { + return subscriptions.getAvailableProducts().contains(PIR) } private fun sendCommand(newCommand: Command) { diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt index 4ce3f911ae70..0046709ced79 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt @@ -141,6 +141,7 @@ class ProSettingView @JvmOverloads constructor( subscribedSubscriptionSetting.isVisible = true subscriptionSettingContainer.isVisible = true + subscribedSubscriptionSetting.isVisible = true } } WAITING -> { @@ -150,6 +151,7 @@ class ProSettingView @JvmOverloads constructor( subscribedSubscriptionSetting.isGone = true subscriptionSettingContainer.isVisible = true + subscriptionSetting.isVisible = true subscriptionSetting.setSecondaryText(context.getString(R.string.subscriptionSettingActivating)) } } @@ -160,6 +162,7 @@ class ProSettingView @JvmOverloads constructor( subscribedSubscriptionSetting.isGone = true subscriptionSettingContainer.isVisible = true + subscriptionSetting.isVisible = true subscriptionSetting.setSecondaryText(context.getString(R.string.subscriptionSettingExpired)) subscriptionSetting.setTrailingIconResource(CommonR.drawable.ic_exclamation_red_16) } diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealProductSubscriptionManagerTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealProductSubscriptionManagerTest.kt index 82bacb20f2dc..cb283faca1b4 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealProductSubscriptionManagerTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealProductSubscriptionManagerTest.kt @@ -265,6 +265,8 @@ private class FakeSubscriptions( override suspend fun getSubscriptionStatus(): SubscriptionStatus = subscriptionStatus + override suspend fun getAvailableProducts(): Set = emptySet() + override fun shouldLaunchPrivacyProForUrl(url: String): Boolean = false override fun launchPrivacyPro( diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/ItrSettingViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/ItrSettingViewModelTest.kt new file mode 100644 index 000000000000..c71196566f54 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/ItrSettingViewModelTest.kt @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.subscriptions.impl.settings + +import app.cash.turbine.test +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.subscriptions.api.Product +import com.duckduckgo.subscriptions.api.SubscriptionStatus.AUTO_RENEWABLE +import com.duckduckgo.subscriptions.api.SubscriptionStatus.EXPIRED +import com.duckduckgo.subscriptions.api.SubscriptionStatus.GRACE_PERIOD +import com.duckduckgo.subscriptions.api.SubscriptionStatus.INACTIVE +import com.duckduckgo.subscriptions.api.SubscriptionStatus.NOT_AUTO_RENEWABLE +import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN +import com.duckduckgo.subscriptions.api.SubscriptionStatus.WAITING +import com.duckduckgo.subscriptions.api.Subscriptions +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender +import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel +import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.ViewState.ItrState.Disabled +import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.ViewState.ItrState.Enabled +import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.ViewState.ItrState.Hidden +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class ItrSettingViewModelTest { + + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val subscriptionPixelSender: SubscriptionPixelSender = mock() + private val subscriptions: Subscriptions = mock() + private lateinit var itrSettingViewModel: ItrSettingViewModel + + @Before + fun before() { + itrSettingViewModel = ItrSettingViewModel( + subscriptions, + subscriptionPixelSender, + ) + } + + @Test + fun `when onItr then report app settings pixel sent`() = runTest { + itrSettingViewModel.onItr() + verify(subscriptionPixelSender).reportAppSettingsIdtrClick() + } + + @Test + fun `when subscription state is unknown then ItrState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(UNKNOWN) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is inactive and no itr product available then ItrState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(INACTIVE) + whenever(subscriptions.getAvailableProducts()).thenReturn(emptySet()) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is inactive and itr product available then ItrState is disabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(INACTIVE) + whenever(subscriptions.getAvailableProducts()).thenReturn(setOf(Product.ITR)) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Disabled, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is inactive and row_itr product available then ItrState is disabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(INACTIVE) + whenever(subscriptions.getAvailableProducts()).thenReturn(setOf(Product.ROW_ITR)) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Disabled, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is exitred and no itr product available then ItrState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(EXPIRED) + whenever(subscriptions.getAvailableProducts()).thenReturn(emptySet()) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is exitred and itr product available then ItrState is disabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(EXPIRED) + whenever(subscriptions.getAvailableProducts()).thenReturn(setOf(Product.ITR)) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Disabled, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is exitred and row_itr product available then ItrState is disabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(EXPIRED) + whenever(subscriptions.getAvailableProducts()).thenReturn(setOf(Product.ROW_ITR)) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Disabled, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is waiting and no itr product available then ItrState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(WAITING) + whenever(subscriptions.getAvailableProducts()).thenReturn(emptySet()) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is waiting and itr product available then ItrState is disabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(WAITING) + whenever(subscriptions.getAvailableProducts()).thenReturn(setOf(Product.ITR)) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Disabled, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is waiting and row_itr product available then ItrState is disabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(WAITING) + whenever(subscriptions.getAvailableProducts()).thenReturn(setOf(Product.ROW_ITR)) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Disabled, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is auto renewable and itr entitled then ItrState is enabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.ITR))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(AUTO_RENEWABLE) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Enabled, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is auto renewable and row_itr entitled then ItrState is enabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.ROW_ITR))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(AUTO_RENEWABLE) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Enabled, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is auto renewable and not entitled then ItrState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(AUTO_RENEWABLE) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is not auto renewable and itr entitled then ItrState is enabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.ITR))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(NOT_AUTO_RENEWABLE) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Enabled, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is not auto renewable and row_itr entitled then ItrState is enabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.ROW_ITR))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(NOT_AUTO_RENEWABLE) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Enabled, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is not auto renewable and not entitled then ItrState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(NOT_AUTO_RENEWABLE) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is grace period and itr entitled then ItrState is enabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.ITR))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(GRACE_PERIOD) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Enabled, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is grace period and row_itr entitled then ItrState is enabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.ROW_ITR))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(GRACE_PERIOD) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Enabled, + expectMostRecentItem().itrState, + ) + } + } + + @Test + fun `when subscription state is grace period and not entitled then ItrState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(GRACE_PERIOD) + + itrSettingViewModel.onCreate(mock()) + + itrSettingViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().itrState, + ) + } + } +} diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModelTest.kt new file mode 100644 index 000000000000..7d5f27eba9b7 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModelTest.kt @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.subscriptions.impl.settings.views + +import app.cash.turbine.test +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.subscriptions.api.Product +import com.duckduckgo.subscriptions.api.SubscriptionStatus.AUTO_RENEWABLE +import com.duckduckgo.subscriptions.api.SubscriptionStatus.EXPIRED +import com.duckduckgo.subscriptions.api.SubscriptionStatus.GRACE_PERIOD +import com.duckduckgo.subscriptions.api.SubscriptionStatus.INACTIVE +import com.duckduckgo.subscriptions.api.SubscriptionStatus.NOT_AUTO_RENEWABLE +import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN +import com.duckduckgo.subscriptions.api.SubscriptionStatus.WAITING +import com.duckduckgo.subscriptions.api.Subscriptions +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState.PirState.Disabled +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState.PirState.Enabled +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState.PirState.Hidden +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class PirSettingViewModelTest { + + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val subscriptionPixelSender: SubscriptionPixelSender = mock() + private val subscriptions: Subscriptions = mock() + private lateinit var pirSettingsViewModel: PirSettingViewModel + + @Before + fun before() { + pirSettingsViewModel = PirSettingViewModel( + subscriptionPixelSender, + subscriptions, + ) + } + + @Test + fun `when onPir then report app settings pixel sent`() = runTest { + pirSettingsViewModel.onPir() + verify(subscriptionPixelSender).reportAppSettingsPirClick() + } + + @Test + fun `when subscription state is unknown then PirState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(UNKNOWN) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().pirState, + ) + } + } + + @Test + fun `when subscription state is inactive and no pir product available then PirState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(INACTIVE) + whenever(subscriptions.getAvailableProducts()).thenReturn(emptySet()) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().pirState, + ) + } + } + + @Test + fun `when subscription state is inactive and pir product available then PirState is disabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(INACTIVE) + whenever(subscriptions.getAvailableProducts()).thenReturn(setOf(Product.PIR)) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Disabled, + expectMostRecentItem().pirState, + ) + } + } + + @Test + fun `when subscription state is expired and no pir product available then PirState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(EXPIRED) + whenever(subscriptions.getAvailableProducts()).thenReturn(emptySet()) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().pirState, + ) + } + } + + @Test + fun `when subscription state is expired and pir product available then PirState is disabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(EXPIRED) + whenever(subscriptions.getAvailableProducts()).thenReturn(setOf(Product.PIR)) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Disabled, + expectMostRecentItem().pirState, + ) + } + } + + @Test + fun `when subscription state is waiting and no pir product available then PirState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(WAITING) + whenever(subscriptions.getAvailableProducts()).thenReturn(emptySet()) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().pirState, + ) + } + } + + @Test + fun `when subscription state is waiting and pir product available then PirState is disabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(WAITING) + whenever(subscriptions.getAvailableProducts()).thenReturn(setOf(Product.PIR)) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Disabled, + expectMostRecentItem().pirState, + ) + } + } + + @Test + fun `when subscription state is auto renewable and entitled then PirState is enabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.PIR))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(AUTO_RENEWABLE) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Enabled, + expectMostRecentItem().pirState, + ) + } + } + + @Test + fun `when subscription state is auto renewable and not entitled then PirState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(AUTO_RENEWABLE) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().pirState, + ) + } + } + + @Test + fun `when subscription state is not auto renewable and entitled then PirState is enabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.PIR))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(NOT_AUTO_RENEWABLE) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Enabled, + expectMostRecentItem().pirState, + ) + } + } + + @Test + fun `when subscription state is not auto renewable and not entitled then PirState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(NOT_AUTO_RENEWABLE) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().pirState, + ) + } + } + + @Test + fun `when subscription state is grace period and entitled then PirState is enabled`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(listOf(Product.PIR))) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(GRACE_PERIOD) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Enabled, + expectMostRecentItem().pirState, + ) + } + } + + @Test + fun `when subscription state is grace period and not entitled then PirState is hidden`() = runTest { + whenever(subscriptions.getEntitlementStatus()).thenReturn(flowOf(emptyList())) + whenever(subscriptions.getSubscriptionStatus()).thenReturn(GRACE_PERIOD) + + pirSettingsViewModel.onCreate(mock()) + + pirSettingsViewModel.viewState.test { + assertEquals( + Hidden, + expectMostRecentItem().pirState, + ) + } + } +}