From 38fc930518533dd8ff489929efc2fb588077f9fb Mon Sep 17 00:00:00 2001 From: Noelia Alcala Date: Sat, 21 Dec 2024 01:53:54 +0000 Subject: [PATCH] Privacy Pro Subscriptions: Update some internal models and methods (#5402) Task/Issue URL: https://app.asana.com/0/1201807753394693/1209007672205184/f ### Description Update some subscriptions models in order to support free trials on mobile: - `SubscriptionOffer` model - `OptionsJson` model - `getSubscriptionOffer()` method ### Steps to test this PR _Pre steps_ - [x] Apply patch to being able to test subscriptions in staging _Privacy Pro Eligible US_ - [x] Make sure you are US eligible - [x] Install form branch - [x] Check browser works as expected - [x] Go to Settings - [x] Check you can see Privacy Pro option - [x] Tap on that option - [x] Check Subscription page looks as expected - [x] Purchase any of the options and check it works well - [x] Go to Subscription Settings - [x] Check everything works as expected _Privacy Pro Eligible ROW (Optional)_ - [x] Make sure you are ROW eligible - [x] Install form branch - [x] Check browser works as expected - [x] Go to Settings - [x] Check you can see Privacy Pro option - [x] Tap on that option - [x] Check Subscription page looks as expected - [x] Purchase any of the options and check it works well - [x] Go to Subscription Settings - [x] Check everything works as expected _Privacy Pro No Eligible_ - [x] Install form branch - [x] Check browser works as expected - [x] Go to Settings - [x] Check you can't see Privacy Pro option ### No UI changes Co-authored-by: Lukasz Macionczyk --- .../subscriptions/impl/RealSubscriptions.kt | 2 +- .../impl/SubscriptionsConstants.kt | 4 + .../impl/SubscriptionsManager.kt | 86 ++++++++----- .../views/LegacyProSettingViewModel.kt | 9 +- .../settings/views/ProSettingViewModel.kt | 9 +- .../impl/ui/SubscriptionWebViewViewModel.kt | 119 ++++++++++++++---- .../impl/RealSubscriptionsManagerTest.kt | 66 ++++++---- .../impl/RealSubscriptionsTest.kt | 68 +++------- .../views/LegacyProSettingViewModelTest.kt | 1 + .../settings/views/ProSettingViewModelTest.kt | 1 + .../ui/SubscriptionWebViewViewModelTest.kt | 45 ++++--- 11 files changed, 258 insertions(+), 152 deletions(-) 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 4b62a7aa5f2c..fbd17b79bd1f 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 @@ -77,7 +77,7 @@ class RealSubscriptions @Inject constructor( override suspend fun isEligible(): Boolean { val supportsEncryption = subscriptionsManager.canSupportEncryption() val isActive = subscriptionsManager.subscriptionStatus().isActiveOrWaiting() - val isEligible = subscriptionsManager.getSubscriptionOffer() != null + val isEligible = subscriptionsManager.getSubscriptionOffer().isNotEmpty() return isActive || (isEligible && supportsEncryption) } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsConstants.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsConstants.kt index c1ffcb942bf7..252d093aca5c 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsConstants.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsConstants.kt @@ -33,6 +33,10 @@ object SubscriptionsConstants { const val YEARLY_PLAN_ROW = "ddg-privacy-pro-yearly-renews-row" const val MONTHLY_PLAN_ROW = "ddg-privacy-pro-monthly-renews-row" + // List of offers + const val MONTHLY_FREE_TRIAL_OFFER_US = "ddg-privacy-pro-sandbox-freetrial-monthly-renews-us" + const val YEARLY_FREE_TRIAL_OFFER_US = "ddg-privacy-pro-sandbox-freetrial-yearly-renews-us" + // List of features const val LEGACY_FE_NETP = "vpn" const val LEGACY_FE_ITR = "identity-theft-restoration" 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 35003f189f75..5128b240e557 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 @@ -100,7 +100,7 @@ interface SubscriptionsManager { /** * Returns available purchase options retrieved from Play Store */ - suspend fun getSubscriptionOffer(): SubscriptionOffer? + suspend fun getSubscriptionOffer(): List /** * Launches the purchase flow for a given plan id @@ -309,6 +309,7 @@ class RealSubscriptionsManager @Inject constructor( removeExpiredSubscriptionOnCancelledPurchase = false } } + else -> { // NOOP } @@ -612,6 +613,7 @@ class RealSubscriptionsManager @Inject constructor( RecoverSubscriptionResult.Failure(SUBSCRIPTION_NOT_FOUND_ERROR) } } + is StoreLoginResult.Failure -> { RecoverSubscriptionResult.Failure("") } @@ -653,43 +655,48 @@ class RealSubscriptionsManager @Inject constructor( data class Failure(val message: String) : RecoverSubscriptionResult() } - override suspend fun getSubscriptionOffer(): SubscriptionOffer? = + private suspend fun activePlanIds(): List = + if (isLaunchedRow()) { + listOf(YEARLY_PLAN_US, MONTHLY_PLAN_US, YEARLY_PLAN_ROW, MONTHLY_PLAN_ROW) + } else { + listOf(YEARLY_PLAN_US, MONTHLY_PLAN_US) + } + + override suspend fun getSubscriptionOffer(): List = playBillingManager.products .find { it.productId == BASIC_SUBSCRIPTION } ?.subscriptionOfferDetails .orEmpty() - .associateBy { it.basePlanId } + .filter { activePlanIds().contains(it.basePlanId) } .let { availablePlans -> - when { - availablePlans.keys.containsAll(listOf(MONTHLY_PLAN_US, YEARLY_PLAN_US)) -> { - availablePlans.getValue(MONTHLY_PLAN_US) to availablePlans.getValue(YEARLY_PLAN_US) - } - availablePlans.keys.containsAll(listOf(MONTHLY_PLAN_ROW, YEARLY_PLAN_ROW)) && isLaunchedRow() -> { - availablePlans.getValue(MONTHLY_PLAN_ROW) to availablePlans.getValue(YEARLY_PLAN_ROW) + availablePlans.map { offer -> + val pricingPhases = offer.pricingPhases.pricingPhaseList.map { phase -> + PricingPhase( + formattedPrice = phase.formattedPrice, + billingPeriod = phase.billingPeriod, + + ) } - else -> null - } - } - ?.let { (monthlyOffer, yearlyOffer) -> - val features = if (privacyProFeature.get().featuresApi().isEnabled()) { - authRepository.getFeatures(monthlyOffer.basePlanId) - } else { - when (monthlyOffer.basePlanId) { - MONTHLY_PLAN_US -> setOf(LEGACY_FE_NETP, LEGACY_FE_PIR, LEGACY_FE_ITR) - MONTHLY_PLAN_ROW -> setOf(NETP, ROW_ITR) - else -> throw IllegalStateException() + + 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() + } } - } - if (features.isEmpty()) return@let null + if (features.isEmpty()) return@let emptyList() - SubscriptionOffer( - monthlyPlanId = monthlyOffer.basePlanId, - monthlyFormattedPrice = monthlyOffer.pricingPhases.pricingPhaseList.first().formattedPrice, - yearlyPlanId = yearlyOffer.basePlanId, - yearlyFormattedPrice = yearlyOffer.pricingPhases.pricingPhaseList.first().formattedPrice, - features = features, - ) + SubscriptionOffer( + planId = offer.basePlanId, + pricingPhases = pricingPhases, + offerId = offer.offerId, + features = features, + ) + } } override suspend fun purchase( @@ -945,13 +952,26 @@ sealed class CurrentPurchase { } data class SubscriptionOffer( - val monthlyPlanId: String, - val monthlyFormattedPrice: String, - val yearlyPlanId: String, - val yearlyFormattedPrice: String, + val planId: String, + val offerId: String?, + val pricingPhases: List, val features: Set, ) +data class PricingPhase( + val formattedPrice: String, + val billingPeriod: String, + +) { + internal fun getBillingPeriodInDays(): Int? = + when (billingPeriod) { + "P1W" -> 7 + "P1M" -> 30 + "P1Y" -> 365 + else -> null + } +} + data class ValidatedTokenPair( val accessToken: String, val accessTokenClaims: AccessTokenClaims, diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyProSettingViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyProSettingViewModel.kt index a824f85d7e50..42270ce6c622 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyProSettingViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyProSettingViewModel.kt @@ -27,6 +27,8 @@ import com.duckduckgo.subscriptions.api.SubscriptionStatus import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN_ROW import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN_US +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_PLAN_ROW +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_PLAN_US import com.duckduckgo.subscriptions.impl.SubscriptionsManager import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.settings.views.LegacyProSettingViewModel.Command.OpenBuyScreen @@ -88,9 +90,10 @@ class LegacyProSettingViewModel @Inject constructor( subscriptionsManager.subscriptionStatus .distinctUntilChanged() .onEach { subscriptionStatus -> - val region = when (subscriptionsManager.getSubscriptionOffer()?.monthlyPlanId) { - MONTHLY_PLAN_ROW -> SubscriptionRegion.ROW - MONTHLY_PLAN_US -> SubscriptionRegion.US + val offer = subscriptionsManager.getSubscriptionOffer().firstOrNull() + val region = when (offer?.planId) { + MONTHLY_PLAN_ROW, YEARLY_PLAN_ROW -> SubscriptionRegion.ROW + MONTHLY_PLAN_US, YEARLY_PLAN_US -> SubscriptionRegion.US else -> null } _viewState.emit(viewState.value.copy(status = subscriptionStatus, region = region)) diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt index 40bed01b9209..4bcab0dd205a 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt @@ -27,6 +27,8 @@ import com.duckduckgo.subscriptions.api.SubscriptionStatus import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN_ROW import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN_US +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_PLAN_ROW +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_PLAN_US import com.duckduckgo.subscriptions.impl.SubscriptionsManager import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenBuyScreen @@ -88,9 +90,10 @@ class ProSettingViewModel @Inject constructor( subscriptionsManager.subscriptionStatus .distinctUntilChanged() .onEach { subscriptionStatus -> - val region = when (subscriptionsManager.getSubscriptionOffer()?.monthlyPlanId) { - MONTHLY_PLAN_ROW -> SubscriptionRegion.ROW - MONTHLY_PLAN_US -> SubscriptionRegion.US + val offer = subscriptionsManager.getSubscriptionOffer().firstOrNull() + val region = when (offer?.planId) { + MONTHLY_PLAN_ROW, YEARLY_PLAN_ROW -> SubscriptionRegion.ROW + MONTHLY_PLAN_US, YEARLY_PLAN_US -> SubscriptionRegion.US else -> null } _viewState.emit(viewState.value.copy(status = subscriptionStatus, region = region)) diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt index 30adf937beee..e2906cf82b12 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt @@ -30,17 +30,24 @@ import com.duckduckgo.subscriptions.api.SubscriptionStatus import com.duckduckgo.subscriptions.impl.CurrentPurchase import com.duckduckgo.subscriptions.impl.JSONObjectAdapter import com.duckduckgo.subscriptions.impl.PrivacyProFeature +import com.duckduckgo.subscriptions.impl.SubscriptionOffer import com.duckduckgo.subscriptions.impl.SubscriptionsChecker import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.ITR import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.LEGACY_FE_ITR import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.LEGACY_FE_NETP import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.LEGACY_FE_PIR import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_FREE_TRIAL_OFFER_US +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN_ROW +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN_US import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.NETP import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.PIR import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.PLATFORM import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.ROW_ITR import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_FREE_TRIAL_OFFER_US +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_PLAN_ROW +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_PLAN_US import com.duckduckgo.subscriptions.impl.SubscriptionsManager import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.repository.isActive @@ -229,38 +236,87 @@ class SubscriptionWebViewViewModel @Inject constructor( } private fun getSubscriptionOptions(featureName: String, method: String, id: String) { + suspend fun sendOptionJson(optionsJson: SubscriptionOptionsJson) { + val response = JsCallbackData( + featureName = featureName, + method = method, + id = id, + params = JSONObject(jsonAdapter.toJson(optionsJson)), + ) + command.send(SendResponseToJs(response)) + } + viewModelScope.launch(dispatcherProvider.io()) { - var subscriptionOptions = SubscriptionOptionsJson( + val defaultOptions = SubscriptionOptionsJson( options = emptyList(), features = emptyList(), ) - if (privacyProFeature.allowPurchase().isEnabled()) { - subscriptionsManager.getSubscriptionOffer()?.let { offer -> - val yearlyJson = OptionsJson( - id = offer.yearlyPlanId, - cost = CostJson(displayPrice = offer.yearlyFormattedPrice, recurrence = YEARLY), - ) + val subscriptionOptions = if (privacyProFeature.allowPurchase().isEnabled()) { + val subscriptionOffers = subscriptionsManager.getSubscriptionOffer().associateBy { it.offerId ?: it.planId } + when { + subscriptionOffers.keys.containsAll(listOf(MONTHLY_PLAN_US, YEARLY_PLAN_US)) -> { + createSubscriptionOptions( + monthlyOffer = subscriptionOffers.getValue(MONTHLY_PLAN_US), + yearlyOffer = subscriptionOffers.getValue(YEARLY_PLAN_US), + ) + } + + subscriptionOffers.keys.containsAll(listOf(MONTHLY_PLAN_ROW, YEARLY_PLAN_ROW)) -> { + createSubscriptionOptions( + monthlyOffer = subscriptionOffers.getValue(MONTHLY_PLAN_ROW), + yearlyOffer = subscriptionOffers.getValue(YEARLY_PLAN_ROW), + ) + } + + else -> defaultOptions + } + } else { + defaultOptions + } - val monthlyJson = OptionsJson( - id = offer.monthlyPlanId, - cost = CostJson(displayPrice = offer.monthlyFormattedPrice, recurrence = MONTHLY), - ) + sendOptionJson(subscriptionOptions) + } + } - subscriptionOptions = SubscriptionOptionsJson( - options = listOf(yearlyJson, monthlyJson), - features = offer.features.map(::FeatureJson), - ) - } + private fun createSubscriptionOptions( + monthlyOffer: SubscriptionOffer, + yearlyOffer: SubscriptionOffer, + ): SubscriptionOptionsJson { + return SubscriptionOptionsJson( + options = listOf( + createOptionsJson(yearlyOffer, YEARLY), + createOptionsJson(monthlyOffer, MONTHLY), + ), + features = monthlyOffer.features.map(::FeatureJson), + ) + } + + private fun createOptionsJson(offer: SubscriptionOffer, recurrence: String): OptionsJson { + val offerDisplayPrice: String = offer.offerId?.let { + offer.pricingPhases.getOrNull(1)?.formattedPrice ?: offer.pricingPhases.first().formattedPrice + } ?: offer.pricingPhases.first().formattedPrice + + return OptionsJson( + id = offer.planId, + cost = CostJson(displayPrice = offerDisplayPrice, recurrence = recurrence), + offer = getOfferJson(offer), + ) + } + + private fun getOfferJson(offer: SubscriptionOffer): OfferJson? { + return offer.offerId?.let { + val offerType = when (offer.offerId) { + MONTHLY_FREE_TRIAL_OFFER_US, YEARLY_FREE_TRIAL_OFFER_US -> OfferType.FREE_TRIAL + else -> OfferType.UNKNOWN } - val response = JsCallbackData( - featureName = featureName, - method = method, - id = id, - params = JSONObject(jsonAdapter.toJson(subscriptionOptions)), + OfferJson( + type = offerType.type, + id = it, + durationInDays = offer.pricingPhases.first().getBillingPeriodInDays(), + isUserEligible = true, // TODO Noelia: Need to check if they already had a free trial before to return false ) - command.send(SendResponseToJs(response)) } } @@ -293,11 +349,28 @@ class SubscriptionWebViewViewModel @Inject constructor( data class OptionsJson( val id: String, val cost: CostJson, + val offer: OfferJson?, + ) + + data class CostJson( + val displayPrice: String, + val recurrence: String, + ) + + data class OfferJson( + val type: String, + val id: String, + val durationInDays: Int?, + val isUserEligible: Boolean, ) - data class CostJson(val displayPrice: String, val recurrence: String) data class FeatureJson(val name: String) + enum class OfferType(val type: String) { + FREE_TRIAL("freeTrial"), + UNKNOWN("unknown"), + } + sealed class PurchaseStateView { data object Inactive : PurchaseStateView() data object InProgress : PurchaseStateView() diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt index ac595767adc5..7d0273dc020c 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt @@ -15,7 +15,13 @@ import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle.State import com.duckduckgo.subscriptions.api.Product.NetP import com.duckduckgo.subscriptions.api.SubscriptionStatus -import com.duckduckgo.subscriptions.api.SubscriptionStatus.* +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.impl.RealSubscriptionsManager.RecoverSubscriptionResult import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN_ROW import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN_US @@ -51,7 +57,6 @@ import com.duckduckgo.subscriptions.impl.services.SubscriptionResponse import com.duckduckgo.subscriptions.impl.services.SubscriptionsService import com.duckduckgo.subscriptions.impl.services.ValidateTokenResponse import com.duckduckgo.subscriptions.impl.store.SubscriptionsDataStore -import java.lang.Exception import java.time.Duration import java.time.Instant import java.time.LocalDateTime @@ -62,7 +67,10 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.ResponseBody.Companion.toResponseBody -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Assume.assumeFalse import org.junit.Assume.assumeTrue import org.junit.Before @@ -71,6 +79,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -1125,51 +1134,55 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { @Test fun whenGetSubscriptionOfferThenReturnValue() = runTest { authRepository.setFeatures(MONTHLY_PLAN_US, setOf(NETP)) + authRepository.setFeatures(YEARLY_PLAN_US, setOf(NETP)) givenPlansAvailable(MONTHLY_PLAN_US, YEARLY_PLAN_US) - val subscriptionOffer = subscriptionsManager.getSubscriptionOffer()!! + val subscriptionOffers = subscriptionsManager.getSubscriptionOffer() - with(subscriptionOffer) { - assertEquals(MONTHLY_PLAN_US, monthlyPlanId) - assertEquals("1$", monthlyFormattedPrice) - assertEquals(YEARLY_PLAN_US, yearlyPlanId) - assertEquals("1$", yearlyFormattedPrice) - assertEquals(setOf(NETP), features) + with(subscriptionOffers) { + assertTrue(any { it.planId == MONTHLY_PLAN_US }) + assertEquals("1$", find { it.planId == MONTHLY_PLAN_US }?.pricingPhases?.first()?.formattedPrice) + assertTrue(any { it.planId == YEARLY_PLAN_US }) + assertEquals("1$", find { it.planId == YEARLY_PLAN_US }?.pricingPhases?.first()?.formattedPrice) + assertEquals(setOf(NETP), first().features) } } @Test - fun whenGetSubscriptionOfferAndNoFeaturesThenReturnNull() = runTest { + fun whenGetSubscriptionOfferAndNoFeaturesThenReturnEmptyList() = runTest { authRepository.setFeatures(MONTHLY_PLAN_US, emptySet()) + authRepository.setFeatures(YEARLY_PLAN_US, emptySet()) givenPlansAvailable(MONTHLY_PLAN_US, YEARLY_PLAN_US) - assertNull(subscriptionsManager.getSubscriptionOffer()) + assertEquals(emptyList(), subscriptionsManager.getSubscriptionOffer()) } @Test fun whenGetSubscriptionOfferAndRowPlansAvailableThenReturnValue() = runTest { authRepository.setFeatures(MONTHLY_PLAN_ROW, setOf(NETP)) + authRepository.setFeatures(YEARLY_PLAN_ROW, setOf(NETP)) givenPlansAvailable(MONTHLY_PLAN_ROW, YEARLY_PLAN_ROW) givenIsLaunchedRow(true) - val subscriptionOffer = subscriptionsManager.getSubscriptionOffer()!! + val subscriptionOffers = subscriptionsManager.getSubscriptionOffer() - with(subscriptionOffer) { - assertEquals(MONTHLY_PLAN_ROW, monthlyPlanId) - assertEquals("1$", monthlyFormattedPrice) - assertEquals(YEARLY_PLAN_ROW, yearlyPlanId) - assertEquals("1$", yearlyFormattedPrice) - assertEquals(setOf(NETP), features) + with(subscriptionOffers) { + assertTrue(any { it.planId == MONTHLY_PLAN_ROW }) + assertEquals("1$", find { it.planId == MONTHLY_PLAN_ROW }?.pricingPhases?.first()?.formattedPrice) + assertTrue(any { it.planId == YEARLY_PLAN_ROW }) + assertEquals("1$", find { it.planId == YEARLY_PLAN_ROW }?.pricingPhases?.first()?.formattedPrice) + assertEquals(setOf(NETP), first().features) } } @Test - fun whenGetSubscriptionAndRowPlansAvailableAndFeatureDisabledThenReturnNull() = runTest { - authRepository.setFeatures(MONTHLY_PLAN_US, emptySet()) + fun whenGetSubscriptionAndRowPlansAvailableAndFeatureDisabledThenReturnEmptyList() = runTest { + authRepository.setFeatures(MONTHLY_PLAN_ROW, setOf(NETP)) + authRepository.setFeatures(YEARLY_PLAN_ROW, setOf(NETP)) givenPlansAvailable(MONTHLY_PLAN_ROW, YEARLY_PLAN_ROW) givenIsLaunchedRow(false) - assertNull(subscriptionsManager.getSubscriptionOffer()) + assertEquals(emptyList(), subscriptionsManager.getSubscriptionOffer()) } @Test @@ -1514,9 +1527,12 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { val productDetails: ProductDetails = mock { productDetails -> whenever(productDetails.productId).thenReturn(SubscriptionsConstants.BASIC_SUBSCRIPTION) - val pricingPhaseList: List = listOf( - mock { pricingPhase -> whenever(pricingPhase.formattedPrice).thenReturn("1$") }, - ) + val mockPricingPhase: PricingPhase = mock { + on { formattedPrice } doReturn "1$" + on { billingPeriod } doReturn "P1M" + } + + val pricingPhaseList: List = listOf(mockPricingPhase) val pricingPhases: PricingPhases = mock { pricingPhases -> whenever(pricingPhases.pricingPhaseList).thenReturn(pricingPhaseList) diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsTest.kt index bd1462c68e29..149d648290e8 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsTest.kt @@ -34,7 +34,10 @@ import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.ui.SubscriptionsWebViewActivityWithParams import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -60,9 +63,19 @@ class RealSubscriptionsTest { private val pixel: SubscriptionPixelSender = mock() private lateinit var subscriptions: RealSubscriptions + private val testSubscriptionOfferList = listOf( + SubscriptionOffer( + planId = "test", + offerId = null, + pricingPhases = emptyList(), + features = setOf(SubscriptionsConstants.NETP), + ), + ) + @Before fun before() = runTest { whenever(mockSubscriptionsManager.canSupportEncryption()).thenReturn(true) + whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList()) subscriptions = RealSubscriptions(mockSubscriptionsManager, globalActivityStarter, pixel) } @@ -104,36 +117,25 @@ class RealSubscriptionsTest { @Test fun whenIsEligibleIfOffersReturnedThenReturnTrueRegardlessOfStatus() = runTest { whenever(mockSubscriptionsManager.subscriptionStatus()).thenReturn(UNKNOWN) - whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn( - SubscriptionOffer( - monthlyPlanId = "test", - yearlyFormattedPrice = "test", - yearlyPlanId = "test", - monthlyFormattedPrice = "test", - features = setOf(SubscriptionsConstants.NETP), - ), - ) + whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn(testSubscriptionOfferList) assertTrue(subscriptions.isEligible()) } @Test fun whenIsEligibleIfNotOffersReturnedThenReturnFalseIfNotActiveOrWaiting() = runTest { whenever(mockSubscriptionsManager.subscriptionStatus()).thenReturn(UNKNOWN) - whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn(null) assertFalse(subscriptions.isEligible()) } @Test fun whenIsEligibleIfNotOffersReturnedThenReturnTrueIfWaiting() = runTest { whenever(mockSubscriptionsManager.subscriptionStatus()).thenReturn(WAITING) - whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn(null) assertTrue(subscriptions.isEligible()) } @Test fun whenIsEligibleIfNotOffersReturnedThenReturnTrueIfActive() = runTest { whenever(mockSubscriptionsManager.subscriptionStatus()).thenReturn(AUTO_RENEWABLE) - whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn(null) assertTrue(subscriptions.isEligible()) } @@ -141,15 +143,7 @@ class RealSubscriptionsTest { fun whenIsEligibleIfNotEncryptionThenReturnTrueIfActive() = runTest { whenever(mockSubscriptionsManager.canSupportEncryption()).thenReturn(false) whenever(mockSubscriptionsManager.subscriptionStatus()).thenReturn(AUTO_RENEWABLE) - whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn( - SubscriptionOffer( - monthlyPlanId = "test", - yearlyFormattedPrice = "test", - yearlyPlanId = "test", - monthlyFormattedPrice = "test", - features = setOf(SubscriptionsConstants.NETP), - ), - ) + whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn(testSubscriptionOfferList) assertTrue(subscriptions.isEligible()) } @@ -157,29 +151,13 @@ class RealSubscriptionsTest { fun whenIsEligibleIfNotEncryptionAndNotActiveThenReturnFalse() = runTest { whenever(mockSubscriptionsManager.canSupportEncryption()).thenReturn(false) whenever(mockSubscriptionsManager.subscriptionStatus()).thenReturn(UNKNOWN) - whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn( - SubscriptionOffer( - monthlyPlanId = "test", - yearlyFormattedPrice = "test", - yearlyPlanId = "test", - monthlyFormattedPrice = "test", - features = setOf(SubscriptionsConstants.NETP), - ), - ) + whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn(testSubscriptionOfferList) assertFalse(subscriptions.isEligible()) } @Test fun whenShouldLaunchPrivacyProForUrlThenReturnCorrectValue() = runTest { - whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn( - SubscriptionOffer( - monthlyPlanId = "test", - yearlyFormattedPrice = "test", - yearlyPlanId = "test", - monthlyFormattedPrice = "test", - features = setOf(SubscriptionsConstants.NETP), - ), - ) + whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn(testSubscriptionOfferList) whenever(mockSubscriptionsManager.subscriptionStatus()).thenReturn(UNKNOWN) assertTrue(subscriptions.shouldLaunchPrivacyProForUrl("https://duckduckgo.com/pro")) @@ -194,15 +172,7 @@ class RealSubscriptionsTest { @Test fun whenShouldLaunchPrivacyProForUrlThenReturnTrue() = runTest { - whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn( - SubscriptionOffer( - monthlyPlanId = "test", - yearlyFormattedPrice = "test", - yearlyPlanId = "test", - monthlyFormattedPrice = "test", - features = setOf(SubscriptionsConstants.NETP), - ), - ) + whenever(mockSubscriptionsManager.getSubscriptionOffer()).thenReturn(testSubscriptionOfferList) whenever(mockSubscriptionsManager.subscriptionStatus()).thenReturn(UNKNOWN) assertTrue(subscriptions.shouldLaunchPrivacyProForUrl("https://duckduckgo.com/pro")) diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyProSettingViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyProSettingViewModelTest.kt index 5cac8f322433..87a48f06dafd 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyProSettingViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/LegacyProSettingViewModelTest.kt @@ -62,6 +62,7 @@ class LegacyProSettingViewModelTest { @Test fun whenOnResumeEmitViewState() = runTest { whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.EXPIRED)) + whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList()) viewModel.onCreate(mock()) viewModel.viewState.test { diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModelTest.kt index fd1a9f387a39..8e35b5277bfb 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModelTest.kt @@ -62,6 +62,7 @@ class ProSettingViewModelTest { @Test fun whenOnResumeEmitViewState() = runTest { whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.EXPIRED)) + whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList()) viewModel.onCreate(mock()) viewModel.viewState.test { diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt index 287e0dbf0fed..68c1c2ad47f3 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt @@ -15,10 +15,13 @@ import com.duckduckgo.subscriptions.api.SubscriptionStatus.INACTIVE import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN import com.duckduckgo.subscriptions.impl.CurrentPurchase import com.duckduckgo.subscriptions.impl.JSONObjectAdapter +import com.duckduckgo.subscriptions.impl.PricingPhase import com.duckduckgo.subscriptions.impl.PrivacyProFeature import com.duckduckgo.subscriptions.impl.SubscriptionOffer import com.duckduckgo.subscriptions.impl.SubscriptionsChecker import com.duckduckgo.subscriptions.impl.SubscriptionsConstants +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN_US +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_PLAN_US import com.duckduckgo.subscriptions.impl.SubscriptionsManager import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command @@ -196,17 +199,22 @@ class SubscriptionWebViewViewModelTest { @Test fun whenGetSubscriptionOptionsThenSendCommand() = runTest { - privacyProFeature.allowPurchase().setRawStoredState(Toggle.State(enable = true)) - - whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn( + val testSubscriptionOfferList = listOf( + SubscriptionOffer( + planId = MONTHLY_PLAN_US, + offerId = null, + pricingPhases = listOf(PricingPhase(formattedPrice = "$1", billingPeriod = "P1M")), + features = setOf(SubscriptionsConstants.NETP), + ), SubscriptionOffer( - monthlyPlanId = "monthly", - monthlyFormattedPrice = "$1", - yearlyPlanId = "yearly", - yearlyFormattedPrice = "$10", + planId = YEARLY_PLAN_US, + offerId = null, + pricingPhases = listOf(PricingPhase(formattedPrice = "$10", billingPeriod = "P1Y")), features = setOf(SubscriptionsConstants.NETP), ), ) + whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(testSubscriptionOfferList) + privacyProFeature.allowPurchase().setRawStoredState(Toggle.State(enable = true)) viewModel.commands().test { viewModel.processJsCallbackMessage("test", "getSubscriptionOptions", "id", JSONObject("{}")) @@ -218,14 +226,15 @@ class SubscriptionWebViewViewModelTest { assertEquals("id", response.id) assertEquals("test", response.featureName) assertEquals("getSubscriptionOptions", response.method) - assertEquals("yearly", params?.options?.first()?.id) - assertEquals("monthly", params?.options?.last()?.id) + assertEquals(YEARLY_PLAN_US, params?.options?.first()?.id) + assertEquals(MONTHLY_PLAN_US, params?.options?.last()?.id) } } @Test fun whenGetSubscriptionsAndNoSubscriptionOfferThenSendCommandWithEmptyData() = runTest { privacyProFeature.allowPurchase().setRawStoredState(Toggle.State(enable = true)) + whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList()) viewModel.commands().test { viewModel.processJsCallbackMessage("test", "getSubscriptionOptions", "id", JSONObject("{}")) @@ -246,16 +255,22 @@ class SubscriptionWebViewViewModelTest { @Test fun whenGetSubscriptionsAndToggleOffThenSendCommandWithEmptyData() = runTest { - privacyProFeature.allowPurchase().setRawStoredState(Toggle.State(enable = false)) - whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn( + val testSubscriptionOfferList = listOf( SubscriptionOffer( - monthlyPlanId = "monthly", - monthlyFormattedPrice = "$1", - yearlyPlanId = "yearly", - yearlyFormattedPrice = "$10", + planId = MONTHLY_PLAN_US, + offerId = null, + pricingPhases = listOf(PricingPhase(formattedPrice = "$1", billingPeriod = "P1M")), + features = setOf(SubscriptionsConstants.NETP), + ), + SubscriptionOffer( + planId = YEARLY_PLAN_US, + offerId = null, + pricingPhases = listOf(PricingPhase(formattedPrice = "$10", billingPeriod = "P1Y")), features = setOf(SubscriptionsConstants.NETP), ), ) + privacyProFeature.allowPurchase().setRawStoredState(Toggle.State(enable = false)) + whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(testSubscriptionOfferList) viewModel.commands().test { viewModel.processJsCallbackMessage("test", "getSubscriptionOptions", "id", JSONObject("{}"))