Skip to content

Commit 941067a

Browse files
committed
Subscription copy changes for duck.ai and plans (#6383)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1149059203486286/task/1210704593867822?focus=true ### Description Updates copies/icons/visuals to integrate with duck.ai changes ### Steps to test this PR _Feature 1_ - [x] Apply latest staging patch from this task https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true - [x] Fresh install - [x] Go to settings - [x] In the subscription section validate the copies are the ones in "NEW" column in https://app.asana.com/1/137249556945/project/1149059203486286/task/1210704593867822?focus=true - [x] Purchase the subscription - [x] Ensure "New" pill appears in Duck.ai - [x] Go to settings -> send feedback - [x] Ensure we say "subscription" according to https://app.asana.com/1/137249556945/project/1149059203486286/task/1210704593867822?focus=true (last table) _Feature 2_ - [x] Cancel and remove your subscription - [x] Go to Feature flags inventory - [x] Disable privacypro - duckaiplus - [x] Go to the browser - [x] Access settings again (you need to enter so it re-evaluates the FF) - [x] Ensure you don't see ai models in the subscription section (description field) - [x] The title mentions privacy pro _Feature 3_ - [x] Cancel and remove your subscription (if any) - [x] Go to Feature flags inventory - [x] Disable privacypro - subscriptionRebranding - [x] Restart the browser (we need to restart the process since value is cached) - [x] Navigate to settings - [x] Ensure you see privacy pro copies ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)|
1 parent 8ce9831 commit 941067a

File tree

12 files changed

+206
-16
lines changed

12 files changed

+206
-16
lines changed

duckchat/duckchat-impl/src/main/res/layout/view_duck_ai_settings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@
2121
android:layout_height="wrap_content"
2222
app:indicatorStatus="on"
2323
app:leadingIcon="@drawable/ic_ai_chat_color_24"
24+
app:pillIcon="newPill"
2425
app:primaryText="@string/duck_ai_paid_settings_title" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.subscriptions.api
18+
19+
interface SubscriptionRebrandingFeatureToggle {
20+
/**
21+
* This method is safe to call from the main thread.
22+
* @return true if the subscription rebranding feature is enabled, false otherwise
23+
*/
24+
fun isSubscriptionRebrandingEnabled(): Boolean
25+
}

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
@@ -206,6 +206,9 @@ interface PrivacyProFeature {
206206
*/
207207
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
208208
fun duckAISubscriptionMessaging(): Toggle
209+
210+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
211+
fun subscriptionRebranding(): Toggle
209212
}
210213

211214
@ContributesBinding(AppScope::class)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.subscriptions.impl
18+
19+
import androidx.lifecycle.LifecycleOwner
20+
import com.duckduckgo.app.di.AppCoroutineScope
21+
import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver
22+
import com.duckduckgo.common.utils.DispatcherProvider
23+
import com.duckduckgo.di.scopes.AppScope
24+
import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin
25+
import com.duckduckgo.subscriptions.api.SubscriptionRebrandingFeatureToggle
26+
import com.squareup.anvil.annotations.ContributesBinding
27+
import com.squareup.anvil.annotations.ContributesMultibinding
28+
import dagger.SingleInstanceIn
29+
import javax.inject.Inject
30+
import kotlinx.coroutines.CoroutineScope
31+
import kotlinx.coroutines.launch
32+
import logcat.logcat
33+
34+
@ContributesBinding(
35+
scope = AppScope::class,
36+
boundType = SubscriptionRebrandingFeatureToggle::class,
37+
)
38+
@ContributesMultibinding(
39+
scope = AppScope::class,
40+
boundType = PrivacyConfigCallbackPlugin::class,
41+
)
42+
@ContributesMultibinding(
43+
scope = AppScope::class,
44+
boundType = MainProcessLifecycleObserver::class,
45+
)
46+
@SingleInstanceIn(AppScope::class)
47+
class SubscriptionRebrandingFeatureToggleImpl @Inject constructor(
48+
private val privacyProFeature: PrivacyProFeature,
49+
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
50+
private val dispatcherProvider: DispatcherProvider,
51+
) : SubscriptionRebrandingFeatureToggle, PrivacyConfigCallbackPlugin, MainProcessLifecycleObserver {
52+
53+
private var cachedValue: Boolean = false
54+
55+
override fun isSubscriptionRebrandingEnabled(): Boolean {
56+
return cachedValue
57+
}
58+
59+
override fun onCreate(owner: LifecycleOwner) {
60+
super.onCreate(owner)
61+
logcat { "SubscriptionRebrandingFeatureToggle: App created, prefetching feature flag" }
62+
prefetchFeatureFlag()
63+
}
64+
65+
override fun onPrivacyConfigDownloaded() {
66+
logcat { "SubscriptionRebrandingFeatureToggle: Privacy config downloaded, refreshing feature flag" }
67+
prefetchFeatureFlag()
68+
}
69+
70+
private fun prefetchFeatureFlag() {
71+
appCoroutineScope.launch(dispatcherProvider.io()) {
72+
val isEnabled = privacyProFeature.subscriptionRebranding().isEnabled()
73+
cachedValue = isEnabled
74+
logcat { "SubscriptionRebrandingFeatureToggle: Feature flag cached, value = $isEnabled" }
75+
}
76+
}
77+
}

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/feedback/SubscriptionFeedbackGeneralFragment.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ import android.view.View
2121
import com.duckduckgo.anvil.annotations.InjectWith
2222
import com.duckduckgo.common.ui.viewbinding.viewBinding
2323
import com.duckduckgo.di.scopes.FragmentScope
24+
import com.duckduckgo.subscriptions.api.SubscriptionRebrandingFeatureToggle
2425
import com.duckduckgo.subscriptions.impl.R
2526
import com.duckduckgo.subscriptions.impl.databinding.ContentFeedbackGeneralBinding
27+
import javax.inject.Inject
2628

2729
@InjectWith(FragmentScope::class)
2830
class SubscriptionFeedbackGeneralFragment : SubscriptionFeedbackFragment(R.layout.content_feedback_general) {
31+
32+
@Inject
33+
lateinit var subscriptionRebrandingFeatureToggle: SubscriptionRebrandingFeatureToggle
34+
2935
private val binding: ContentFeedbackGeneralBinding by viewBinding()
3036

3137
override fun onViewCreated(
@@ -39,6 +45,11 @@ class SubscriptionFeedbackGeneralFragment : SubscriptionFeedbackFragment(R.layou
3945
listener.onBrowserFeedbackClicked()
4046
}
4147

48+
if (subscriptionRebrandingFeatureToggle.isSubscriptionRebrandingEnabled()) {
49+
binding.pproFeedback.setPrimaryText(getString(R.string.feedbackGeneralSubscription))
50+
} else {
51+
binding.pproFeedback.setPrimaryText(getString(R.string.feedbackGeneralPpro))
52+
}
4253
binding.pproFeedback.setOnClickListener {
4354
listener.onPproFeedbackClicked()
4455
}

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.duckduckgo.anvil.annotations.PriorityKey
2222
import com.duckduckgo.common.ui.view.listitem.SectionHeaderListItem
2323
import com.duckduckgo.di.scopes.ActivityScope
2424
import com.duckduckgo.settings.api.ProSettingsPlugin
25+
import com.duckduckgo.subscriptions.api.SubscriptionRebrandingFeatureToggle
2526
import com.duckduckgo.subscriptions.impl.R
2627
import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingView
2728
import com.duckduckgo.subscriptions.impl.settings.views.PirSettingView
@@ -31,10 +32,16 @@ import javax.inject.Inject
3132

3233
@ContributesMultibinding(ActivityScope::class)
3334
@PriorityKey(100)
34-
class ProSettingsTitle @Inject constructor() : ProSettingsPlugin {
35+
class ProSettingsTitle @Inject constructor(
36+
private val subscriptionRebrandingFeatureToggle: SubscriptionRebrandingFeatureToggle,
37+
) : ProSettingsPlugin {
3538
override fun getView(context: Context): View {
3639
return SectionHeaderListItem(context).apply {
37-
primaryText = context.getString(R.string.privacyPro)
40+
if (subscriptionRebrandingFeatureToggle.isSubscriptionRebrandingEnabled()) {
41+
primaryText = context.getString(R.string.subscriptionSettingSectionTitle)
42+
} else {
43+
primaryText = context.getString(R.string.privacyPro)
44+
}
3845
}
3946
}
4047
}

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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,13 @@ class ProSettingView @JvmOverloads constructor(
167167
}
168168
else -> {
169169
with(binding) {
170-
subscriptionBuy.setPrimaryText(context.getString(R.string.subscriptionSettingSubscribe))
170+
if (viewState.duckAiEnabled) {
171+
subscriptionBuy.setPrimaryText(context.getString(R.string.subscriptionSettingSubscribeSecure))
172+
} else {
173+
subscriptionBuy.setPrimaryText(context.getString(R.string.subscriptionSettingSubscribe))
174+
}
171175
subscriptionBuy.setSecondaryText(getSubscriptionSecondaryText(viewState))
172-
subscriptionGet.setText(
173-
when (viewState.freeTrialEligible) {
174-
true -> R.string.subscriptionSettingTryFreeTrial
175-
false -> R.string.subscriptionSettingGet
176-
},
177-
)
176+
subscriptionGet.setText(getActionButtonText(viewState))
178177

179178
subscriptionBuyContainer.isVisible = true
180179
subscriptionRestoreContainer.isVisible = true
@@ -185,6 +184,24 @@ class ProSettingView @JvmOverloads constructor(
185184
}
186185
}
187186

187+
private fun getActionButtonText(viewState: ViewState) = when (viewState.freeTrialEligible) {
188+
true -> {
189+
if (viewState.rebrandingEnabled) {
190+
R.string.subscriptionSettingTryFreeTrialRebranding
191+
} else {
192+
R.string.subscriptionSettingTryFreeTrial
193+
}
194+
}
195+
196+
false -> {
197+
if (viewState.rebrandingEnabled) {
198+
R.string.subscriptionSettingGetRebranding
199+
} else {
200+
R.string.subscriptionSettingGet
201+
}
202+
}
203+
}
204+
188205
private fun getSubscriptionSecondaryText(viewState: ViewState) = if (viewState.duckAiPlusAvailable) {
189206
when (viewState.region) {
190207
ROW -> context.getString(R.string.subscriptionSettingSubscribeWithDuckAiSubtitleRow)

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel
2525
import com.duckduckgo.common.utils.DispatcherProvider
2626
import com.duckduckgo.di.scopes.ViewScope
2727
import com.duckduckgo.subscriptions.api.Product.DuckAiPlus
28+
import com.duckduckgo.subscriptions.api.SubscriptionRebrandingFeatureToggle
2829
import com.duckduckgo.subscriptions.api.SubscriptionStatus
2930
import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN
3031
import com.duckduckgo.subscriptions.impl.PrivacyProFeature
@@ -54,6 +55,7 @@ import kotlinx.coroutines.withContext
5455
@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle
5556
@ContributesViewModel(ViewScope::class)
5657
class ProSettingViewModel @Inject constructor(
58+
private val subscriptionRebrandingFeatureToggle: SubscriptionRebrandingFeatureToggle,
5759
private val subscriptionsManager: SubscriptionsManager,
5860
private val pixelSender: SubscriptionPixelSender,
5961
private val privacyProFeature: PrivacyProFeature,
@@ -71,6 +73,8 @@ class ProSettingViewModel @Inject constructor(
7173
data class ViewState(
7274
val status: SubscriptionStatus = UNKNOWN,
7375
val region: SubscriptionRegion? = null,
76+
val duckAiEnabled: Boolean = false,
77+
val rebrandingEnabled: Boolean = false,
7478
val duckAiPlusAvailable: Boolean = false,
7579
val freeTrialEligible: Boolean = false,
7680
) {
@@ -111,11 +115,14 @@ class ProSettingViewModel @Inject constructor(
111115
val duckAiAvailable = duckAiEnabled && offer?.features?.any { feature ->
112116
feature == DuckAiPlus.value
113117
} ?: false
118+
val rebrandingEnabled = subscriptionRebrandingFeatureToggle.isSubscriptionRebrandingEnabled()
114119

115120
_viewState.emit(
116121
viewState.value.copy(
117122
status = subscriptionStatus,
118123
region = region,
124+
duckAiEnabled = duckAiEnabled,
125+
rebrandingEnabled = rebrandingEnabled,
119126
duckAiPlusAvailable = duckAiAvailable,
120127
freeTrialEligible = subscriptionsManager.isFreeTrialEligible(),
121128
),

subscriptions/subscriptions-impl/src/main/res/layout/content_feedback_general.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@
3838
<com.duckduckgo.common.ui.view.listitem.OneLineListItem
3939
android:id="@+id/pproFeedback"
4040
android:layout_width="match_parent"
41-
android:layout_height="wrap_content"
42-
app:primaryText="@string/feedbackGeneralPpro" />
41+
android:layout_height="wrap_content"/>
4342

4443
</LinearLayout>
4544
</androidx.core.widget.NestedScrollView>

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,18 @@
1515
-->
1616

1717
<resources>
18-
<string name="subscriptionSettingSubscribeWithDuckAiSubtitle">Includes our VPN, Duck.ai Plus, Personal Information Removal, and Identity Theft Restoration.</string>
19-
<string name="subscriptionSettingSubscribeWithDuckAiSubtitleRow">Includes our VPN, Duck.ai Plus and Identity Theft Restoration.</string>
18+
<string name="subscriptionSettingSubscribeWithDuckAiSubtitle">Subscribers get our VPN, advanced AI models in Duck.ai, Personal Information Removal, and Identity Theft Restoration.</string>
19+
<string name="subscriptionSettingSubscribeWithDuckAiSubtitleRow">Subscribers get our VPN, advanced AI models in Duck.ai, and Identity Theft Restoration.</string>
2020

2121
<string name="feedbackCategoryDuckAi">Duck.ai</string>
2222
<string name="feedbackSubCategoryDuckAiSubscriberModels">Unable to access the subscriber-only Duck.ai models</string>
2323
<string name="feedbackSubCategoryDuckAiLoginThirdPartyBrowser">Can\'t access Duck.ai with my subscription in other browsers</string>
2424
<string name="feedbackSubCategoryDuckAiOther">Other Duck.ai feedback</string>
25+
26+
<string name="subscriptionSettingSectionTitle" translatable="false">DuckDuckGo Subscription</string>
27+
<string name="subscriptionSettingSubscribeSecure">Secure your Wi-Fi, and chat privately with advanced AI models</string>
28+
29+
<string name="subscriptionSettingGetRebranding">Subscribe to DuckDuckGo</string>
30+
<string name="subscriptionSettingTryFreeTrialRebranding">Try Free</string>
31+
<string name="feedbackGeneralSubscription">Subscription</string>
2532
</resources>

0 commit comments

Comments
 (0)