diff --git a/duckchat/duckchat-impl/src/main/res/layout/view_duck_ai_settings.xml b/duckchat/duckchat-impl/src/main/res/layout/view_duck_ai_settings.xml
index 4eaf155253ce..84f30ae551e4 100644
--- a/duckchat/duckchat-impl/src/main/res/layout/view_duck_ai_settings.xml
+++ b/duckchat/duckchat-impl/src/main/res/layout/view_duck_ai_settings.xml
@@ -21,4 +21,5 @@
android:layout_height="wrap_content"
app:indicatorStatus="on"
app:leadingIcon="@drawable/ic_ai_chat_color_24"
+ app:pillIcon="newPill"
app:primaryText="@string/duck_ai_paid_settings_title" />
\ No newline at end of file
diff --git a/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/SubscriptionRebrandingFeatureToggle.kt b/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/SubscriptionRebrandingFeatureToggle.kt
new file mode 100644
index 000000000000..85520b83169e
--- /dev/null
+++ b/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/SubscriptionRebrandingFeatureToggle.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2025 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.duckduckgo.subscriptions.api
+
+interface SubscriptionRebrandingFeatureToggle {
+ /**
+ * This method is safe to call from the main thread.
+ * @return true if the subscription rebranding feature is enabled, false otherwise
+ */
+ fun isSubscriptionRebrandingEnabled(): Boolean
+}
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 f9426e2b8d54..28df3d5f11a4 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
@@ -206,6 +206,9 @@ interface PrivacyProFeature {
*/
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
fun duckAISubscriptionMessaging(): Toggle
+
+ @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
+ fun subscriptionRebranding(): Toggle
}
@ContributesBinding(AppScope::class)
diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionRebrandingFeatureToggle.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionRebrandingFeatureToggle.kt
new file mode 100644
index 000000000000..254a04ec9fc2
--- /dev/null
+++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionRebrandingFeatureToggle.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2025 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.duckduckgo.subscriptions.impl
+
+import androidx.lifecycle.LifecycleOwner
+import com.duckduckgo.app.di.AppCoroutineScope
+import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver
+import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.di.scopes.AppScope
+import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin
+import com.duckduckgo.subscriptions.api.SubscriptionRebrandingFeatureToggle
+import com.squareup.anvil.annotations.ContributesBinding
+import com.squareup.anvil.annotations.ContributesMultibinding
+import dagger.SingleInstanceIn
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import logcat.logcat
+
+@ContributesBinding(
+ scope = AppScope::class,
+ boundType = SubscriptionRebrandingFeatureToggle::class,
+)
+@ContributesMultibinding(
+ scope = AppScope::class,
+ boundType = PrivacyConfigCallbackPlugin::class,
+)
+@ContributesMultibinding(
+ scope = AppScope::class,
+ boundType = MainProcessLifecycleObserver::class,
+)
+@SingleInstanceIn(AppScope::class)
+class SubscriptionRebrandingFeatureToggleImpl @Inject constructor(
+ private val privacyProFeature: PrivacyProFeature,
+ @AppCoroutineScope private val appCoroutineScope: CoroutineScope,
+ private val dispatcherProvider: DispatcherProvider,
+) : SubscriptionRebrandingFeatureToggle, PrivacyConfigCallbackPlugin, MainProcessLifecycleObserver {
+
+ private var cachedValue: Boolean = false
+
+ override fun isSubscriptionRebrandingEnabled(): Boolean {
+ return cachedValue
+ }
+
+ override fun onCreate(owner: LifecycleOwner) {
+ super.onCreate(owner)
+ logcat { "SubscriptionRebrandingFeatureToggle: App created, prefetching feature flag" }
+ prefetchFeatureFlag()
+ }
+
+ override fun onPrivacyConfigDownloaded() {
+ logcat { "SubscriptionRebrandingFeatureToggle: Privacy config downloaded, refreshing feature flag" }
+ prefetchFeatureFlag()
+ }
+
+ private fun prefetchFeatureFlag() {
+ appCoroutineScope.launch(dispatcherProvider.io()) {
+ val isEnabled = privacyProFeature.subscriptionRebranding().isEnabled()
+ cachedValue = isEnabled
+ logcat { "SubscriptionRebrandingFeatureToggle: Feature flag cached, value = $isEnabled" }
+ }
+ }
+}
diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/feedback/SubscriptionFeedbackGeneralFragment.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/feedback/SubscriptionFeedbackGeneralFragment.kt
index 2b433df83d00..90d51c1f2934 100644
--- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/feedback/SubscriptionFeedbackGeneralFragment.kt
+++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/feedback/SubscriptionFeedbackGeneralFragment.kt
@@ -21,11 +21,17 @@ import android.view.View
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.di.scopes.FragmentScope
+import com.duckduckgo.subscriptions.api.SubscriptionRebrandingFeatureToggle
import com.duckduckgo.subscriptions.impl.R
import com.duckduckgo.subscriptions.impl.databinding.ContentFeedbackGeneralBinding
+import javax.inject.Inject
@InjectWith(FragmentScope::class)
class SubscriptionFeedbackGeneralFragment : SubscriptionFeedbackFragment(R.layout.content_feedback_general) {
+
+ @Inject
+ lateinit var subscriptionRebrandingFeatureToggle: SubscriptionRebrandingFeatureToggle
+
private val binding: ContentFeedbackGeneralBinding by viewBinding()
override fun onViewCreated(
@@ -39,6 +45,11 @@ class SubscriptionFeedbackGeneralFragment : SubscriptionFeedbackFragment(R.layou
listener.onBrowserFeedbackClicked()
}
+ if (subscriptionRebrandingFeatureToggle.isSubscriptionRebrandingEnabled()) {
+ binding.pproFeedback.setPrimaryText(getString(R.string.feedbackGeneralSubscription))
+ } else {
+ binding.pproFeedback.setPrimaryText(getString(R.string.feedbackGeneralPpro))
+ }
binding.pproFeedback.setOnClickListener {
listener.onPproFeedbackClicked()
}
diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt
index 83afe4f3a592..c695d92dcab1 100644
--- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt
+++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt
@@ -22,6 +22,7 @@ import com.duckduckgo.anvil.annotations.PriorityKey
import com.duckduckgo.common.ui.view.listitem.SectionHeaderListItem
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.settings.api.ProSettingsPlugin
+import com.duckduckgo.subscriptions.api.SubscriptionRebrandingFeatureToggle
import com.duckduckgo.subscriptions.impl.R
import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingView
import com.duckduckgo.subscriptions.impl.settings.views.PirSettingView
@@ -31,10 +32,16 @@ import javax.inject.Inject
@ContributesMultibinding(ActivityScope::class)
@PriorityKey(100)
-class ProSettingsTitle @Inject constructor() : ProSettingsPlugin {
+class ProSettingsTitle @Inject constructor(
+ private val subscriptionRebrandingFeatureToggle: SubscriptionRebrandingFeatureToggle,
+) : ProSettingsPlugin {
override fun getView(context: Context): View {
return SectionHeaderListItem(context).apply {
- primaryText = context.getString(R.string.privacyPro)
+ if (subscriptionRebrandingFeatureToggle.isSubscriptionRebrandingEnabled()) {
+ primaryText = context.getString(R.string.subscriptionSettingSectionTitle)
+ } else {
+ primaryText = context.getString(R.string.privacyPro)
+ }
}
}
}
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 6c76534bc5cc..51a456ebcb48 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
@@ -167,14 +167,13 @@ class ProSettingView @JvmOverloads constructor(
}
else -> {
with(binding) {
- subscriptionBuy.setPrimaryText(context.getString(R.string.subscriptionSettingSubscribe))
+ if (viewState.duckAiEnabled) {
+ subscriptionBuy.setPrimaryText(context.getString(R.string.subscriptionSettingSubscribeSecure))
+ } else {
+ subscriptionBuy.setPrimaryText(context.getString(R.string.subscriptionSettingSubscribe))
+ }
subscriptionBuy.setSecondaryText(getSubscriptionSecondaryText(viewState))
- subscriptionGet.setText(
- when (viewState.freeTrialEligible) {
- true -> R.string.subscriptionSettingTryFreeTrial
- false -> R.string.subscriptionSettingGet
- },
- )
+ subscriptionGet.setText(getActionButtonText(viewState))
subscriptionBuyContainer.isVisible = true
subscriptionRestoreContainer.isVisible = true
@@ -185,6 +184,24 @@ class ProSettingView @JvmOverloads constructor(
}
}
+ private fun getActionButtonText(viewState: ViewState) = when (viewState.freeTrialEligible) {
+ true -> {
+ if (viewState.rebrandingEnabled) {
+ R.string.subscriptionSettingTryFreeTrialRebranding
+ } else {
+ R.string.subscriptionSettingTryFreeTrial
+ }
+ }
+
+ false -> {
+ if (viewState.rebrandingEnabled) {
+ R.string.subscriptionSettingGetRebranding
+ } else {
+ R.string.subscriptionSettingGet
+ }
+ }
+ }
+
private fun getSubscriptionSecondaryText(viewState: ViewState) = if (viewState.duckAiPlusAvailable) {
when (viewState.region) {
ROW -> context.getString(R.string.subscriptionSettingSubscribeWithDuckAiSubtitleRow)
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 aae09ea1e63a..5400a7eb7703 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
@@ -25,6 +25,7 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ViewScope
import com.duckduckgo.subscriptions.api.Product.DuckAiPlus
+import com.duckduckgo.subscriptions.api.SubscriptionRebrandingFeatureToggle
import com.duckduckgo.subscriptions.api.SubscriptionStatus
import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN
import com.duckduckgo.subscriptions.impl.PrivacyProFeature
@@ -54,6 +55,7 @@ import kotlinx.coroutines.withContext
@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle
@ContributesViewModel(ViewScope::class)
class ProSettingViewModel @Inject constructor(
+ private val subscriptionRebrandingFeatureToggle: SubscriptionRebrandingFeatureToggle,
private val subscriptionsManager: SubscriptionsManager,
private val pixelSender: SubscriptionPixelSender,
private val privacyProFeature: PrivacyProFeature,
@@ -71,6 +73,8 @@ class ProSettingViewModel @Inject constructor(
data class ViewState(
val status: SubscriptionStatus = UNKNOWN,
val region: SubscriptionRegion? = null,
+ val duckAiEnabled: Boolean = false,
+ val rebrandingEnabled: Boolean = false,
val duckAiPlusAvailable: Boolean = false,
val freeTrialEligible: Boolean = false,
) {
@@ -111,11 +115,14 @@ class ProSettingViewModel @Inject constructor(
val duckAiAvailable = duckAiEnabled && offer?.features?.any { feature ->
feature == DuckAiPlus.value
} ?: false
+ val rebrandingEnabled = subscriptionRebrandingFeatureToggle.isSubscriptionRebrandingEnabled()
_viewState.emit(
viewState.value.copy(
status = subscriptionStatus,
region = region,
+ duckAiEnabled = duckAiEnabled,
+ rebrandingEnabled = rebrandingEnabled,
duckAiPlusAvailable = duckAiAvailable,
freeTrialEligible = subscriptionsManager.isFreeTrialEligible(),
),
diff --git a/subscriptions/subscriptions-impl/src/main/res/layout/content_feedback_general.xml b/subscriptions/subscriptions-impl/src/main/res/layout/content_feedback_general.xml
index 6069ea158fbd..3cc3ad525864 100644
--- a/subscriptions/subscriptions-impl/src/main/res/layout/content_feedback_general.xml
+++ b/subscriptions/subscriptions-impl/src/main/res/layout/content_feedback_general.xml
@@ -38,8 +38,7 @@
+ android:layout_height="wrap_content"/>
\ No newline at end of file
diff --git a/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml b/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml
index 00a6d30b33ad..21529ea0aecf 100644
--- a/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml
+++ b/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml
@@ -15,11 +15,18 @@
-->
- Includes our VPN, Duck.ai Plus, Personal Information Removal, and Identity Theft Restoration.
- Includes our VPN, Duck.ai Plus and Identity Theft Restoration.
+ Subscribers get our VPN, advanced AI models in Duck.ai, Personal Information Removal, and Identity Theft Restoration.
+ Subscribers get our VPN, advanced AI models in Duck.ai, and Identity Theft Restoration.
Duck.ai
Unable to access the subscriber-only Duck.ai models
Can\'t access Duck.ai with my subscription in other browsers
Other Duck.ai feedback
+
+ DuckDuckGo Subscription
+ Secure your Wi-Fi, and chat privately with advanced AI models
+
+ Subscribe to DuckDuckGo
+ Try Free
+ Subscription
\ No newline at end of file
diff --git a/subscriptions/subscriptions-impl/src/main/res/values/strings-subscriptions.xml b/subscriptions/subscriptions-impl/src/main/res/values/strings-subscriptions.xml
index fc443b45dd0c..eb04f6d4c342 100644
--- a/subscriptions/subscriptions-impl/src/main/res/values/strings-subscriptions.xml
+++ b/subscriptions/subscriptions-impl/src/main/res/values/strings-subscriptions.xml
@@ -68,8 +68,8 @@
Subscription Settings
Protect your connection and identity with Privacy Pro
- Includes our VPN, Personal Information Removal, and Identity Theft Restoration.
- Includes our VPN and Identity Theft Restoration.
+ Subscribers get our VPN, Personal Information Removal, and Identity Theft Restoration.
+ Subscribers get our VPN and Identity Theft Restoration.
Get Privacy Pro
Try Privacy Pro Free
I Have a Subscription
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 717d5bf478f2..a182acf0d97e 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
@@ -6,6 +6,7 @@ import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
import com.duckduckgo.feature.toggles.api.Toggle.State
import com.duckduckgo.subscriptions.api.Product
+import com.duckduckgo.subscriptions.api.SubscriptionRebrandingFeatureToggle
import com.duckduckgo.subscriptions.api.SubscriptionStatus
import com.duckduckgo.subscriptions.impl.PrivacyProFeature
import com.duckduckgo.subscriptions.impl.SubscriptionOffer
@@ -32,12 +33,19 @@ class ProSettingViewModelTest {
private val subscriptionsManager: SubscriptionsManager = mock()
private val pixelSender: SubscriptionPixelSender = mock()
+ private val subscriptionRebrandingFeatureToggle: SubscriptionRebrandingFeatureToggle = mock()
private lateinit var viewModel: ProSettingViewModel
private val privacyProFeature = FakeFeatureToggleFactory.create(PrivacyProFeature::class.java)
@Before
fun before() {
- viewModel = ProSettingViewModel(subscriptionsManager, pixelSender, privacyProFeature, coroutineTestRule.testDispatcherProvider)
+ viewModel = ProSettingViewModel(
+ subscriptionRebrandingFeatureToggle,
+ subscriptionsManager,
+ pixelSender,
+ privacyProFeature,
+ coroutineTestRule.testDispatcherProvider,
+ )
}
@Test
@@ -147,6 +155,34 @@ class ProSettingViewModelTest {
}
}
+ @Test
+ fun whenRebrandingEnabledThenRebrandingEnabledViewStateTrue() = runTest {
+ whenever(subscriptionRebrandingFeatureToggle.isSubscriptionRebrandingEnabled()).thenReturn(true)
+ whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.AUTO_RENEWABLE))
+ whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList())
+ whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(true)
+
+ viewModel.onCreate(mock())
+ viewModel.viewState.test {
+ assertTrue(awaitItem().rebrandingEnabled)
+ cancelAndConsumeRemainingEvents()
+ }
+ }
+
+ @Test
+ fun whenRebrandingDisabledThenRebrandingEnabledViewStateFalse() = runTest {
+ whenever(subscriptionRebrandingFeatureToggle.isSubscriptionRebrandingEnabled()).thenReturn(false)
+ whenever(subscriptionsManager.subscriptionStatus).thenReturn(flowOf(SubscriptionStatus.AUTO_RENEWABLE))
+ whenever(subscriptionsManager.getSubscriptionOffer()).thenReturn(emptyList())
+ whenever(subscriptionsManager.isFreeTrialEligible()).thenReturn(true)
+
+ viewModel.onCreate(mock())
+ viewModel.viewState.test {
+ assertFalse(awaitItem().rebrandingEnabled)
+ cancelAndConsumeRemainingEvents()
+ }
+ }
+
private val subscriptionOffer = SubscriptionOffer(
planId = "test",
offerId = null,