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,