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 f415333829ab..ea8c267888f1 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 @@ -260,6 +260,9 @@ interface PrivacyProFeature { @Toggle.DefaultValue(defaultValue = DefaultFeatureValue.INTERNAL) fun supportsSwitchSubscription(): Toggle + + @Toggle.DefaultValue(defaultValue = DefaultFeatureValue.INTERNAL) + fun blackFridayOffer2025(): Toggle } @ContributesBinding(AppScope::class) 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 ab6bea48259d..14b936f9c679 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 @@ -268,6 +268,11 @@ interface SubscriptionsManager { * @return [SwitchPlanPricingInfo] containing current price, target price, and yearly monthly equivalent, or null if unavailable */ suspend fun getSwitchPlanPricing(isUpgrade: Boolean): SwitchPlanPricingInfo? + + /** + * @return `true` if the Black Friday offer is available, `false` otherwise + */ + suspend fun blackFridayOfferAvailable(): Boolean } @SingleInstanceIn(AppScope::class) @@ -409,6 +414,10 @@ class RealSubscriptionsManager @Inject constructor( return@withContext hasActiveSubscription && !isOnFreeTrial && isSwitchFeatureEnabled } + override suspend fun blackFridayOfferAvailable(): Boolean = withContext(dispatcherProvider.io()) { + return@withContext privacyProFeature.get().blackFridayOffer2025().isEnabled() + } + override suspend fun getSwitchPlanPricing(isUpgrade: Boolean): SwitchPlanPricingInfo? = withContext(dispatcherProvider.io()) { return@withContext try { val currentSubscription = getSubscription() ?: return@withContext null 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 c18d9721b9ea..0ad5645610d5 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 @@ -187,9 +187,10 @@ class ProSettingView @JvmOverloads constructor( } } - private fun getActionButtonText(viewState: ViewState) = when (viewState.freeTrialEligible) { - true -> R.string.subscriptionSettingTryFreeTrial - false -> R.string.subscriptionSettingGet + private fun getActionButtonText(viewState: ViewState) = when { + viewState.blackFridayOfferAvailable -> R.string.subscriptionSettingBlackFridayOffer + viewState.freeTrialEligible -> R.string.subscriptionSettingTryFreeTrial + else -> R.string.subscriptionSettingGet } private fun getSubscriptionSecondaryText(viewState: ViewState) = if (viewState.duckAiPlusAvailable) { 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 baef7b9838ce..90092e8ddf37 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 @@ -73,6 +73,7 @@ class ProSettingViewModel @Inject constructor( val region: SubscriptionRegion? = null, val duckAiPlusAvailable: Boolean = false, val freeTrialEligible: Boolean = false, + val blackFridayOfferAvailable: Boolean = false, ) { enum class SubscriptionRegion { US, ROW } } @@ -99,7 +100,7 @@ class ProSettingViewModel @Inject constructor( subscriptionsManager.subscriptionStatus .distinctUntilChanged() .onEach { subscriptionStatus -> - withContext(dispatcherProvider.io()) { + val newViewState = withContext(dispatcherProvider.io()) { val offer = subscriptionsManager.getSubscriptionOffer().firstOrNull() val region = when (offer?.planId) { MONTHLY_PLAN_ROW, YEARLY_PLAN_ROW -> SubscriptionRegion.ROW @@ -112,15 +113,15 @@ class ProSettingViewModel @Inject constructor( feature == DuckAiPlus.value } ?: false - _viewState.emit( - viewState.value.copy( - status = subscriptionStatus, - region = region, - duckAiPlusAvailable = duckAiAvailable, - freeTrialEligible = subscriptionsManager.isFreeTrialEligible(), - ), + viewState.value.copy( + status = subscriptionStatus, + region = region, + duckAiPlusAvailable = duckAiAvailable, + freeTrialEligible = subscriptionsManager.isFreeTrialEligible(), + blackFridayOfferAvailable = subscriptionsManager.blackFridayOfferAvailable(), ) } + _viewState.emit(newViewState) }.launchIn(viewModelScope) } diff --git a/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml b/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml index 90476d16a39b..89243d8488bc 100644 --- a/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml +++ b/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml @@ -21,4 +21,7 @@ Personal Information Removal is not available at this moment. Please restart the app and try again. OK + + Save 40% on First Year + 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 1e0cd38d97ec..1538448891de 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 @@ -2181,6 +2181,29 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { whenever(playBillingManager.products).thenReturn(listOf(productDetails)) } + @Test + fun whenBlackFridayOfferAvailableWithFeatureFlagEnabledThenReturnTrue() = runTest { + givenBlackFridayFeatureFlagEnabled(true) + + val result = subscriptionsManager.blackFridayOfferAvailable() + + assertTrue(result) + } + + @Test + fun whenBlackFridayOfferAvailableWithFeatureFlagDisabledThenReturnFalse() = runTest { + givenBlackFridayFeatureFlagEnabled(false) + + val result = subscriptionsManager.blackFridayOfferAvailable() + + assertFalse(result) + } + + @SuppressLint("DenyListedApi") + private fun givenBlackFridayFeatureFlagEnabled(value: Boolean) { + privacyProFeature.blackFridayOffer2025().setRawStoredState(State(remoteEnableState = value)) + } + private companion object { @JvmStatic @Parameterized.Parameters(name = "authApiV2Enabled={0}") 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 5f9fd0f319ae..2131dacf45ce 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 @@ -81,6 +81,7 @@ class ProSettingViewModelTest { whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.EXPIRED)) whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList()) whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(false) + whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(false) viewModel.onCreate(mock()) viewModel.viewState.test { @@ -104,6 +105,7 @@ class ProSettingViewModelTest { whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.INACTIVE)) whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList()) whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(true) + whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(false) viewModel.onCreate(mock()) viewModel.viewState.test { @@ -118,6 +120,7 @@ class ProSettingViewModelTest { whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.AUTO_RENEWABLE)) whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(listOf(subscriptionOffer.copy(features = setOf(Product.DuckAiPlus.value)))) whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(true) + whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(false) viewModel.onCreate(mock()) viewModel.viewState.test { @@ -132,6 +135,7 @@ class ProSettingViewModelTest { whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.AUTO_RENEWABLE)) whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(listOf(subscriptionOffer.copy(features = setOf(Product.NetP.value)))) whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(true) + whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(false) viewModel.onCreate(mock()) viewModel.viewState.test { @@ -146,6 +150,7 @@ class ProSettingViewModelTest { whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.AUTO_RENEWABLE)) whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(listOf(subscriptionOffer.copy(features = setOf(Product.DuckAiPlus.value)))) whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(true) + whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(false) viewModel.onCreate(mock()) viewModel.viewState.test { @@ -154,6 +159,34 @@ class ProSettingViewModelTest { } } + @Test + fun whenBlackFridayOfferAvailableThenViewStateBlackFridayOfferAvailableTrue() = runTest { + whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.INACTIVE)) + whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList()) + whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(false) + whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(true) + + viewModel.onCreate(mock()) + viewModel.viewState.test { + assertTrue(awaitItem().blackFridayOfferAvailable) + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun whenBlackFridayOfferNotAvailableThenViewStateBlackFridayOfferAvailableFalse() = runTest { + whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.INACTIVE)) + whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList()) + whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(false) + whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(false) + + viewModel.onCreate(mock()) + viewModel.viewState.test { + assertFalse(awaitItem().blackFridayOfferAvailable) + cancelAndConsumeRemainingEvents() + } + } + private val subscriptionOffer = SubscriptionOffer( planId = "test", offerId = null,