Skip to content

Commit 4da988a

Browse files
authored
Update settings for Black Friday (#7135)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1201807753394693/task/1211642005243050?focus=true ### Description Added subscription entry point copy for black Friday campaign ### Steps to test this PR _Pre steps_ - [x] Apply patch from https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true - [x] Set `PRIVACY_REMOTE_CONFIG_URL` to `https://jsonblob.com/019a9d9c-ab2d-7887-89a0-f4a37206bea8` _Settings US_ - [x] Set device locale to en-US - [x] Fresh install - [x] Go to Settings - [x] Check you see "Save 40% on First Year" _Settings UK_ - [x] Set device locale to en-GB - [x] Fresh install - [x] Go to Settings - [x] Check you **don't** see "Save 40% on First Year" but "Try Free" _FF disabled_ - [x] Set device locale back to en-US - [x] Go to Feature Flag Inventory - [x] Disable `blackFridayOffer2025` sub-feature - [x] Go to Settings - [x] Check you **don't** see "Save 40% on First Year" but "Try Free" ### UI changes | Before | After | | ------ | ----- | <img width="376" height="841" alt="Screenshot 2025-11-17 at 14 21 59" src="https://github.com/user-attachments/assets/98ce8387-c8f9-41bf-a667-a7508d438991" />|<img width="383" height="843" alt="Screenshot 2025-11-17 at 14 09 07" src="https://github.com/user-attachments/assets/b0ec6e9f-a194-41e5-9276-992a30c2119b" />|
1 parent 956864f commit 4da988a

File tree

7 files changed

+84
-11
lines changed

7 files changed

+84
-11
lines changed

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ interface PrivacyProFeature {
260260

261261
@Toggle.DefaultValue(defaultValue = DefaultFeatureValue.INTERNAL)
262262
fun supportsSwitchSubscription(): Toggle
263+
264+
@Toggle.DefaultValue(defaultValue = DefaultFeatureValue.INTERNAL)
265+
fun blackFridayOffer2025(): Toggle
263266
}
264267

265268
@ContributesBinding(AppScope::class)

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,11 @@ interface SubscriptionsManager {
268268
* @return [SwitchPlanPricingInfo] containing current price, target price, and yearly monthly equivalent, or null if unavailable
269269
*/
270270
suspend fun getSwitchPlanPricing(isUpgrade: Boolean): SwitchPlanPricingInfo?
271+
272+
/**
273+
* @return `true` if the Black Friday offer is available, `false` otherwise
274+
*/
275+
suspend fun blackFridayOfferAvailable(): Boolean
271276
}
272277

273278
@SingleInstanceIn(AppScope::class)
@@ -409,6 +414,10 @@ class RealSubscriptionsManager @Inject constructor(
409414
return@withContext hasActiveSubscription && !isOnFreeTrial && isSwitchFeatureEnabled
410415
}
411416

417+
override suspend fun blackFridayOfferAvailable(): Boolean = withContext(dispatcherProvider.io()) {
418+
return@withContext privacyProFeature.get().blackFridayOffer2025().isEnabled()
419+
}
420+
412421
override suspend fun getSwitchPlanPricing(isUpgrade: Boolean): SwitchPlanPricingInfo? = withContext(dispatcherProvider.io()) {
413422
return@withContext try {
414423
val currentSubscription = getSubscription() ?: return@withContext null

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,10 @@ class ProSettingView @JvmOverloads constructor(
187187
}
188188
}
189189

190-
private fun getActionButtonText(viewState: ViewState) = when (viewState.freeTrialEligible) {
191-
true -> R.string.subscriptionSettingTryFreeTrial
192-
false -> R.string.subscriptionSettingGet
190+
private fun getActionButtonText(viewState: ViewState) = when {
191+
viewState.blackFridayOfferAvailable -> R.string.subscriptionSettingBlackFridayOffer
192+
viewState.freeTrialEligible -> R.string.subscriptionSettingTryFreeTrial
193+
else -> R.string.subscriptionSettingGet
193194
}
194195

195196
private fun getSubscriptionSecondaryText(viewState: ViewState) = if (viewState.duckAiPlusAvailable) {

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class ProSettingViewModel @Inject constructor(
7373
val region: SubscriptionRegion? = null,
7474
val duckAiPlusAvailable: Boolean = false,
7575
val freeTrialEligible: Boolean = false,
76+
val blackFridayOfferAvailable: Boolean = false,
7677
) {
7778
enum class SubscriptionRegion { US, ROW }
7879
}
@@ -99,7 +100,7 @@ class ProSettingViewModel @Inject constructor(
99100
subscriptionsManager.subscriptionStatus
100101
.distinctUntilChanged()
101102
.onEach { subscriptionStatus ->
102-
withContext(dispatcherProvider.io()) {
103+
val newViewState = withContext(dispatcherProvider.io()) {
103104
val offer = subscriptionsManager.getSubscriptionOffer().firstOrNull()
104105
val region = when (offer?.planId) {
105106
MONTHLY_PLAN_ROW, YEARLY_PLAN_ROW -> SubscriptionRegion.ROW
@@ -112,15 +113,15 @@ class ProSettingViewModel @Inject constructor(
112113
feature == DuckAiPlus.value
113114
} ?: false
114115

115-
_viewState.emit(
116-
viewState.value.copy(
117-
status = subscriptionStatus,
118-
region = region,
119-
duckAiPlusAvailable = duckAiAvailable,
120-
freeTrialEligible = subscriptionsManager.isFreeTrialEligible(),
121-
),
116+
viewState.value.copy(
117+
status = subscriptionStatus,
118+
region = region,
119+
duckAiPlusAvailable = duckAiAvailable,
120+
freeTrialEligible = subscriptionsManager.isFreeTrialEligible(),
121+
blackFridayOfferAvailable = subscriptionsManager.blackFridayOfferAvailable(),
122122
)
123123
}
124+
_viewState.emit(newViewState)
124125
}.launchIn(viewModelScope)
125126
}
126127

subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@
2121
<string name="pirStorageUnavailableDialogMessage">Personal Information Removal is not available at this moment. Please restart the app and try again.</string>
2222
<string name="pirStorageUnavailableDialogButton">OK</string>
2323

24+
<!-- Black Friday offer-->
25+
<string name="subscriptionSettingBlackFridayOffer">Save 40% on First Year</string>
26+
2427
</resources>

subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2181,6 +2181,29 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) {
21812181
whenever(playBillingManager.products).thenReturn(listOf(productDetails))
21822182
}
21832183

2184+
@Test
2185+
fun whenBlackFridayOfferAvailableWithFeatureFlagEnabledThenReturnTrue() = runTest {
2186+
givenBlackFridayFeatureFlagEnabled(true)
2187+
2188+
val result = subscriptionsManager.blackFridayOfferAvailable()
2189+
2190+
assertTrue(result)
2191+
}
2192+
2193+
@Test
2194+
fun whenBlackFridayOfferAvailableWithFeatureFlagDisabledThenReturnFalse() = runTest {
2195+
givenBlackFridayFeatureFlagEnabled(false)
2196+
2197+
val result = subscriptionsManager.blackFridayOfferAvailable()
2198+
2199+
assertFalse(result)
2200+
}
2201+
2202+
@SuppressLint("DenyListedApi")
2203+
private fun givenBlackFridayFeatureFlagEnabled(value: Boolean) {
2204+
privacyProFeature.blackFridayOffer2025().setRawStoredState(State(remoteEnableState = value))
2205+
}
2206+
21842207
private companion object {
21852208
@JvmStatic
21862209
@Parameterized.Parameters(name = "authApiV2Enabled={0}")

subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModelTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class ProSettingViewModelTest {
8181
whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.EXPIRED))
8282
whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList())
8383
whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(false)
84+
whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(false)
8485

8586
viewModel.onCreate(mock())
8687
viewModel.viewState.test {
@@ -104,6 +105,7 @@ class ProSettingViewModelTest {
104105
whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.INACTIVE))
105106
whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList())
106107
whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(true)
108+
whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(false)
107109

108110
viewModel.onCreate(mock())
109111
viewModel.viewState.test {
@@ -118,6 +120,7 @@ class ProSettingViewModelTest {
118120
whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.AUTO_RENEWABLE))
119121
whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(listOf(subscriptionOffer.copy(features = setOf(Product.DuckAiPlus.value))))
120122
whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(true)
123+
whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(false)
121124

122125
viewModel.onCreate(mock())
123126
viewModel.viewState.test {
@@ -132,6 +135,7 @@ class ProSettingViewModelTest {
132135
whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.AUTO_RENEWABLE))
133136
whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(listOf(subscriptionOffer.copy(features = setOf(Product.NetP.value))))
134137
whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(true)
138+
whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(false)
135139

136140
viewModel.onCreate(mock())
137141
viewModel.viewState.test {
@@ -146,6 +150,7 @@ class ProSettingViewModelTest {
146150
whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.AUTO_RENEWABLE))
147151
whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(listOf(subscriptionOffer.copy(features = setOf(Product.DuckAiPlus.value))))
148152
whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(true)
153+
whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(false)
149154

150155
viewModel.onCreate(mock())
151156
viewModel.viewState.test {
@@ -154,6 +159,34 @@ class ProSettingViewModelTest {
154159
}
155160
}
156161

162+
@Test
163+
fun whenBlackFridayOfferAvailableThenViewStateBlackFridayOfferAvailableTrue() = runTest {
164+
whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.INACTIVE))
165+
whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList())
166+
whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(false)
167+
whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(true)
168+
169+
viewModel.onCreate(mock())
170+
viewModel.viewState.test {
171+
assertTrue(awaitItem().blackFridayOfferAvailable)
172+
cancelAndConsumeRemainingEvents()
173+
}
174+
}
175+
176+
@Test
177+
fun whenBlackFridayOfferNotAvailableThenViewStateBlackFridayOfferAvailableFalse() = runTest {
178+
whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.INACTIVE))
179+
whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList())
180+
whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(false)
181+
whenever(subscriptionsManager.blackFridayOfferAvailable()).thenReturn(false)
182+
183+
viewModel.onCreate(mock())
184+
viewModel.viewState.test {
185+
assertFalse(awaitItem().blackFridayOfferAvailable)
186+
cancelAndConsumeRemainingEvents()
187+
}
188+
}
189+
157190
private val subscriptionOffer = SubscriptionOffer(
158191
planId = "test",
159192
offerId = null,

0 commit comments

Comments
 (0)