diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pir/PirSubscriptionManager.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pir/PirSubscriptionManager.kt new file mode 100644 index 000000000000..52d213cc6ef6 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pir/PirSubscriptionManager.kt @@ -0,0 +1,60 @@ +/* + * 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.pir + +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.subscriptions.api.Product.PIR +import com.duckduckgo.subscriptions.api.SubscriptionStatus +import com.duckduckgo.subscriptions.api.Subscriptions +import com.duckduckgo.subscriptions.impl.pir.PirSubscriptionManager.PirStatus +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +interface PirSubscriptionManager { + fun pirStatus(): Flow + + enum class PirStatus { + ACTIVE, + EXPIRED, + SIGNED_OUT, + INACTIVE, + WAITING, + INELIGIBLE, + } +} + +@ContributesBinding(AppScope::class) +class RealPirSubscriptionManager @Inject constructor( + private val subscriptions: Subscriptions, +) : PirSubscriptionManager { + + override fun pirStatus(): Flow = hasPirEntitlement().map { getPirStatusInternal(it) } + + private fun hasPirEntitlement(): Flow = subscriptions.getEntitlementStatus().map { it.contains(PIR) } + + private suspend fun getPirStatusInternal(hasValidEntitlement: Boolean): PirStatus = when { + !hasValidEntitlement -> PirStatus.INELIGIBLE + else -> when (subscriptions.getSubscriptionStatus()) { + SubscriptionStatus.INACTIVE, SubscriptionStatus.EXPIRED -> PirStatus.EXPIRED + SubscriptionStatus.UNKNOWN -> PirStatus.SIGNED_OUT + SubscriptionStatus.AUTO_RENEWABLE, SubscriptionStatus.NOT_AUTO_RENEWABLE, SubscriptionStatus.GRACE_PERIOD -> PirStatus.ACTIVE + SubscriptionStatus.WAITING -> PirStatus.WAITING + } + } +} diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt index 53c521b7532c..1772be48727b 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt @@ -25,6 +25,7 @@ import com.duckduckgo.settings.api.NewSettingsFeature import com.duckduckgo.settings.api.ProSettingsPlugin import com.duckduckgo.subscriptions.impl.R import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingView +import com.duckduckgo.subscriptions.impl.settings.views.LegacyPirSettingView import com.duckduckgo.subscriptions.impl.settings.views.LegacyProSettingView import com.duckduckgo.subscriptions.impl.settings.views.PirSettingView import com.duckduckgo.subscriptions.impl.settings.views.ProSettingView @@ -55,9 +56,13 @@ class ProSettings @Inject constructor(private val newSettingsFeature: NewSetting @ContributesMultibinding(scope = ActivityScope::class) @PriorityKey(300) -class PIRSettings @Inject constructor() : ProSettingsPlugin { +class PIRSettings @Inject constructor(private val newSettingsFeature: NewSettingsFeature) : ProSettingsPlugin { override fun getView(context: Context): View { - return PirSettingView(context) + return if (newSettingsFeature.self().isEnabled()) { + PirSettingView(context) + } else { + LegacyPirSettingView(context) + } } } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyPirSettingView.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyPirSettingView.kt new file mode 100644 index 000000000000..9acc53b3c04f --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyPirSettingView.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 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 android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.findViewTreeViewModelStoreOwner +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.common.ui.view.gone +import com.duckduckgo.common.ui.view.show +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.common.utils.ConflatedJob +import com.duckduckgo.common.utils.ViewViewModelFactory +import com.duckduckgo.di.scopes.ViewScope +import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.subscriptions.impl.databinding.LegacyViewPirSettingsBinding +import com.duckduckgo.subscriptions.impl.pir.PirActivity.Companion.PirScreenWithEmptyParams +import com.duckduckgo.subscriptions.impl.settings.views.LegacyPirSettingViewModel.Command +import com.duckduckgo.subscriptions.impl.settings.views.LegacyPirSettingViewModel.Command.OpenPir +import com.duckduckgo.subscriptions.impl.settings.views.LegacyPirSettingViewModel.ViewState +import dagger.android.support.AndroidSupportInjection +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@InjectWith(ViewScope::class) +class LegacyPirSettingView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : FrameLayout(context, attrs, defStyle) { + + @Inject + lateinit var viewModelFactory: ViewViewModelFactory + + @Inject + lateinit var globalActivityStarter: GlobalActivityStarter + + private var coroutineScope: CoroutineScope? = null + + private val binding: LegacyViewPirSettingsBinding by viewBinding() + + private val viewModel: LegacyPirSettingViewModel by lazy { + ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[LegacyPirSettingViewModel::class.java] + } + + private var job: ConflatedJob = ConflatedJob() + + override fun onAttachedToWindow() { + AndroidSupportInjection.inject(this) + super.onAttachedToWindow() + + findViewTreeLifecycleOwner()?.lifecycle?.addObserver(viewModel) + + binding.pirSettings.setClickListener { + viewModel.onPir() + } + + @SuppressLint("NoHardcodedCoroutineDispatcher") + coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + job += viewModel.commands() + .onEach { processCommands(it) } + .launchIn(coroutineScope!!) + + viewModel.viewState + .onEach { renderView(it) } + .launchIn(coroutineScope!!) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + findViewTreeLifecycleOwner()?.lifecycle?.removeObserver(viewModel) + coroutineScope?.cancel() + job.cancel() + coroutineScope = null + } + + private fun renderView(viewState: ViewState) { + if (viewState.hasSubscription) { + binding.pirSettings.show() + } else { + binding.pirSettings.gone() + } + } + + private fun processCommands(command: Command) { + when (command) { + is OpenPir -> { + globalActivityStarter.start(context, PirScreenWithEmptyParams) + } + } + } +} diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyPirSettingViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyPirSettingViewModel.kt new file mode 100644 index 000000000000..873b9efacbe8 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyPirSettingViewModel.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 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 android.annotation.SuppressLint +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel +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.api.Subscriptions +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender +import com.duckduckgo.subscriptions.impl.settings.views.LegacyPirSettingViewModel.Command.OpenPir +import javax.inject.Inject +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle +@ContributesViewModel(ViewScope::class) +class LegacyPirSettingViewModel @Inject constructor( + private val subscriptions: Subscriptions, + private val pixelSender: SubscriptionPixelSender, +) : ViewModel(), DefaultLifecycleObserver { + + sealed class Command { + data object OpenPir : Command() + } + + private val command = Channel(1, BufferOverflow.DROP_OLDEST) + internal fun commands(): Flow = command.receiveAsFlow() + data class ViewState(val hasSubscription: Boolean = false) + + private val _viewState = MutableStateFlow(ViewState()) + val viewState = _viewState.asStateFlow() + + fun onPir() { + pixelSender.reportAppSettingsPirClick() + sendCommand(OpenPir) + } + + override fun onCreate(owner: LifecycleOwner) { + super.onCreate(owner) + subscriptions.getEntitlementStatus().onEach { + _viewState.emit(viewState.value.copy(hasSubscription = it.contains(PIR))) + }.launchIn(viewModelScope) + } + + private fun sendCommand(newCommand: Command) { + viewModelScope.launch { + command.send(newCommand) + } + } +} 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 17c9316993ec..620e7172f5e7 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 @@ -20,22 +20,27 @@ import android.annotation.SuppressLint import android.content.Context import android.util.AttributeSet import android.widget.FrameLayout +import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner import com.duckduckgo.anvil.annotations.InjectWith -import com.duckduckgo.common.ui.view.gone -import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.common.utils.ConflatedJob import com.duckduckgo.common.utils.ViewViewModelFactory import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.subscriptions.impl.R import com.duckduckgo.subscriptions.impl.databinding.ViewPirSettingsBinding import com.duckduckgo.subscriptions.impl.pir.PirActivity.Companion.PirScreenWithEmptyParams 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.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 @@ -74,10 +79,6 @@ class PirSettingView @JvmOverloads constructor( findViewTreeLifecycleOwner()?.lifecycle?.addObserver(viewModel) - binding.pirSettings.setClickListener { - viewModel.onPir() - } - @SuppressLint("NoHardcodedCoroutineDispatcher") coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) @@ -99,10 +100,23 @@ class PirSettingView @JvmOverloads constructor( } private fun renderView(viewState: ViewState) { - if (viewState.hasSubscription) { - binding.pirSettings.show() - } else { - binding.pirSettings.gone() + with(binding.pirSettings) { + when (viewState.pirState) { + is Subscribed -> { + isVisible = true + setStatus(isOn = true) + setLeadingIconResource(R.drawable.ic_identity_blocked_pir_color_24) + isClickable = true + binding.pirSettings.setClickListener { viewModel.onPir() } + } + Expired, Activating -> { + isVisible = true + isClickable = false + setStatus(isOn = false) + 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 43ae9e425988..9f2220c295b2 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 @@ -23,10 +23,17 @@ import androidx.lifecycle.ViewModel 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.api.Subscriptions +import com.duckduckgo.subscriptions.impl.pir.PirSubscriptionManager +import com.duckduckgo.subscriptions.impl.pir.PirSubscriptionManager.PirStatus.ACTIVE +import com.duckduckgo.subscriptions.impl.pir.PirSubscriptionManager.PirStatus.EXPIRED +import com.duckduckgo.subscriptions.impl.pir.PirSubscriptionManager.PirStatus.INACTIVE +import com.duckduckgo.subscriptions.impl.pir.PirSubscriptionManager.PirStatus.INELIGIBLE +import com.duckduckgo.subscriptions.impl.pir.PirSubscriptionManager.PirStatus.SIGNED_OUT +import com.duckduckgo.subscriptions.impl.pir.PirSubscriptionManager.PirStatus.WAITING 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 +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState.PirState.Hidden import javax.inject.Inject import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel @@ -36,12 +43,13 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle @ContributesViewModel(ViewScope::class) class PirSettingViewModel @Inject constructor( - private val subscriptions: Subscriptions, + private val pirSubscriptionManager: PirSubscriptionManager, private val pixelSender: SubscriptionPixelSender, ) : ViewModel(), DefaultLifecycleObserver { @@ -51,7 +59,16 @@ class PirSettingViewModel @Inject constructor( private val command = Channel(1, BufferOverflow.DROP_OLDEST) internal fun commands(): Flow = command.receiveAsFlow() - data class ViewState(val hasSubscription: Boolean = false) + data class ViewState(val pirState: PirState = Hidden) { + + sealed class PirState { + + data object Hidden : PirState() + data object Subscribed : PirState() + data object Expired : PirState() + data object Activating : PirState() + } + } private val _viewState = MutableStateFlow(ViewState()) val viewState = _viewState.asStateFlow() @@ -63,8 +80,16 @@ class PirSettingViewModel @Inject constructor( override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) - subscriptions.getEntitlementStatus().onEach { - _viewState.emit(viewState.value.copy(hasSubscription = it.contains(PIR))) + + pirSubscriptionManager.pirStatus().onEach { status -> + val pirState = when (status) { + ACTIVE -> PirState.Subscribed + INACTIVE, EXPIRED -> PirState.Expired + WAITING -> PirState.Activating + SIGNED_OUT, INELIGIBLE -> PirState.Hidden + } + + _viewState.update { it.copy(pirState = pirState) } }.launchIn(viewModelScope) } diff --git a/subscriptions/subscriptions-impl/src/main/res/layout/legacy_view_pir_settings.xml b/subscriptions/subscriptions-impl/src/main/res/layout/legacy_view_pir_settings.xml new file mode 100644 index 000000000000..93a12f1a12d0 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/res/layout/legacy_view_pir_settings.xml @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/subscriptions/subscriptions-impl/src/main/res/layout/view_pir_settings.xml b/subscriptions/subscriptions-impl/src/main/res/layout/view_pir_settings.xml index 93a12f1a12d0..3dacac16be47 100644 --- a/subscriptions/subscriptions-impl/src/main/res/layout/view_pir_settings.xml +++ b/subscriptions/subscriptions-impl/src/main/res/layout/view_pir_settings.xml @@ -14,13 +14,11 @@ ~ limitations under the License. --> - \ No newline at end of file + \ No newline at end of file diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/pir/RealPirSubscriptionManagerTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/pir/RealPirSubscriptionManagerTest.kt new file mode 100644 index 000000000000..4fa282dd96d1 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/pir/RealPirSubscriptionManagerTest.kt @@ -0,0 +1,161 @@ +/* + * 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.pir + +import android.content.Context +import android.net.Uri +import app.cash.turbine.test +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.subscriptions.api.Product +import com.duckduckgo.subscriptions.api.SubscriptionStatus +import com.duckduckgo.subscriptions.api.Subscriptions +import com.duckduckgo.subscriptions.impl.pir.PirSubscriptionManager.PirStatus +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +class RealPirSubscriptionManagerTest { + + @get:Rule + var coroutineRule = CoroutineTestRule() + + @Test + fun `when user does not have pir entitlement then pir status returns ineligible`() = runTest { + val subscriptions: Subscriptions = FakeSubscriptions(SubscriptionStatus.UNKNOWN) + + val pirSubscriptionManager = RealPirSubscriptionManager(subscriptions) + + pirSubscriptionManager.pirStatus().test { + assertEquals(PirStatus.INELIGIBLE, awaitItem()) + awaitComplete() + } + } + + @Test + fun `when user has pir entitlement but subscription is inactive then pir status returns expired`() = runTest { + val subscriptions: Subscriptions = FakeSubscriptions(SubscriptionStatus.INACTIVE, listOf(Product.PIR)) + + val pirSubscriptionManager = RealPirSubscriptionManager(subscriptions) + + pirSubscriptionManager.pirStatus().test { + assertEquals(PirStatus.EXPIRED, awaitItem()) + awaitComplete() + } + } + + @Test + fun `when user has pir entitlement but subscription is expired then pir status returns expired`() = runTest { + val subscriptions: Subscriptions = FakeSubscriptions(SubscriptionStatus.EXPIRED, listOf(Product.PIR)) + + val pirSubscriptionManager = RealPirSubscriptionManager(subscriptions) + + pirSubscriptionManager.pirStatus().test { + assertEquals(PirStatus.EXPIRED, awaitItem()) + awaitComplete() + } + } + + @Test + fun `when user has pir entitlement but subscription is unknown then pir status returns signed out`() = runTest { + val subscriptions: Subscriptions = FakeSubscriptions(SubscriptionStatus.UNKNOWN, listOf(Product.PIR)) + + val pirSubscriptionManager = RealPirSubscriptionManager(subscriptions) + + pirSubscriptionManager.pirStatus().test { + assertEquals(PirStatus.SIGNED_OUT, awaitItem()) + awaitComplete() + } + } + + @Test + fun `when user has pir entitlement and subscription is auto renewable then pir status returns active`() = runTest { + val subscriptions: Subscriptions = FakeSubscriptions(SubscriptionStatus.AUTO_RENEWABLE, listOf(Product.PIR)) + + val pirSubscriptionManager = RealPirSubscriptionManager(subscriptions) + + pirSubscriptionManager.pirStatus().test { + assertEquals(PirStatus.ACTIVE, awaitItem()) + awaitComplete() + } + } + + @Test + fun `when user has pir entitlement and subscription is not auto renewable then pir status returns active`() = runTest { + val subscriptions: Subscriptions = FakeSubscriptions(SubscriptionStatus.NOT_AUTO_RENEWABLE, listOf(Product.PIR)) + + val pirSubscriptionManager = RealPirSubscriptionManager(subscriptions) + + pirSubscriptionManager.pirStatus().test { + assertEquals(PirStatus.ACTIVE, awaitItem()) + awaitComplete() + } + } + + @Test + fun `when user has pir entitlement and subscription is grace period then pir status returns active`() = runTest { + val subscriptions: Subscriptions = FakeSubscriptions(SubscriptionStatus.GRACE_PERIOD, listOf(Product.PIR)) + + val pirSubscriptionManager = RealPirSubscriptionManager(subscriptions) + + pirSubscriptionManager.pirStatus().test { + assertEquals(PirStatus.ACTIVE, awaitItem()) + awaitComplete() + } + } + + @Test + fun `when user has pir entitlement and subscription is waiting then pir status returns waiting`() = runTest { + val subscriptions: Subscriptions = FakeSubscriptions(SubscriptionStatus.WAITING, listOf(Product.PIR)) + + val pirSubscriptionManager = RealPirSubscriptionManager(subscriptions) + + pirSubscriptionManager.pirStatus().test { + assertEquals(PirStatus.WAITING, awaitItem()) + awaitComplete() + } + } +} + +private class FakeSubscriptions( + private val subscriptionStatus: SubscriptionStatus, + private val entitlements: List = emptyList(), +) : Subscriptions { + + override suspend fun isSignedIn(): Boolean = true + + override suspend fun getAccessToken(): String = "fake_access_token" + + override fun getEntitlementStatus(): Flow> = flowOf(entitlements) + + override suspend fun isEligible(): Boolean = true + + override suspend fun getSubscriptionStatus(): SubscriptionStatus = subscriptionStatus + + override fun shouldLaunchPrivacyProForUrl(url: String): Boolean = false + + override fun launchPrivacyPro( + context: Context, + uri: Uri?, + ) { + // no-op + } + + override fun isPrivacyProUrl(uri: Uri): Boolean = false +} 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/LegacyPirSettingViewModelTest.kt similarity index 87% rename from subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModelTest.kt rename to subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyPirSettingViewModelTest.kt index ed5b5010d19e..78f7f1d26fc1 100644 --- 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/LegacyPirSettingViewModelTest.kt @@ -5,7 +5,7 @@ import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.subscriptions.api.Product.PIR 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.LegacyPirSettingViewModel.Command.OpenPir import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -18,17 +18,17 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi -class PirSettingViewModelTest { +class LegacyPirSettingViewModelTest { @get:Rule val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() private val subscriptions: Subscriptions = mock() private val pixelSender: SubscriptionPixelSender = mock() - private lateinit var viewModel: PirSettingViewModel + private lateinit var viewModel: LegacyPirSettingViewModel @Before fun before() { - viewModel = PirSettingViewModel(subscriptions, pixelSender) + viewModel = LegacyPirSettingViewModel(subscriptions, pixelSender) } @Test