Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand All @@ -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
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@
<string name="pirStorageUnavailableDialogMessage">Personal Information Removal is not available at this moment. Please restart the app and try again.</string>
<string name="pirStorageUnavailableDialogButton">OK</string>

<!-- Black Friday offer-->
<string name="subscriptionSettingBlackFridayOffer">Save 40% on First Year</string>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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,
Expand Down
Loading