Skip to content

Commit ce6bccb

Browse files
authored
Privacy Pro Rebranding settings banner (#6484)
Task/Issue URL: https://app.asana.com/1/137249556945/task/1210888667158098?focus=true ### Description Added rebranding info banner in Subscription Settings screen ### Steps to test this PR _New rebranding info banner_ - [ ] Apply patch in https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true - [ ] Install from branch - [ ] Purchase a subscription - [ ] Go to Settings screen - [ ] Check you see the new rebranding info banner - [ ] Close the app - [ ] Go back to subscription settings screen and make sure is still there - [ ] Dismiss the banner - [ ] Make sure it doesn't appear again _FF disabled (Optional)_ - [ ] Go to Settings > Feature Flag Inventory - [ ] Disable `subscriptionRebranding` FF - [ ] Go to Subscription Settings screen - [ ] Check the rebranding info banner doesn't appear ### UI changes | Before | After | | ------ | ----- | <img width="519" height="1135" alt="Screenshot 2025-07-29 at 21 15 39" src="https://github.com/user-attachments/assets/823fde1b-3d86-4cb1-8914-8fc86758234d" />|<img width="511" height="1140" alt="Screenshot 2025-07-29 at 21 15 46" src="https://github.com/user-attachments/assets/6d80acd1-bfb4-491a-ac7f-38daae7c8fe8" />|
1 parent 7e45f9b commit ce6bccb

File tree

8 files changed

+212
-0
lines changed

8 files changed

+212
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2023 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.repository
18+
19+
import com.duckduckgo.common.utils.DispatcherProvider
20+
import com.duckduckgo.di.scopes.AppScope
21+
import com.duckduckgo.subscriptions.impl.store.RebrandingDataStore
22+
import com.squareup.anvil.annotations.ContributesBinding
23+
import javax.inject.Inject
24+
import kotlinx.coroutines.withContext
25+
26+
interface RebrandingRepository {
27+
suspend fun isRebrandingBannerShown(): Boolean
28+
suspend fun setRebrandingBannerAsViewed()
29+
}
30+
31+
@ContributesBinding(AppScope::class)
32+
class RebrandingRepositoryImpl @Inject constructor(
33+
private val rebrandingDataStore: RebrandingDataStore,
34+
private val dispatcherProvider: DispatcherProvider,
35+
) : RebrandingRepository {
36+
37+
override suspend fun isRebrandingBannerShown(): Boolean = withContext(dispatcherProvider.io()) {
38+
rebrandingDataStore.rebrandingBannerShown
39+
}
40+
41+
override suspend fun setRebrandingBannerAsViewed() = withContext(dispatcherProvider.io()) {
42+
rebrandingDataStore.rebrandingBannerShown = true
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2023 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.store
18+
19+
import android.content.SharedPreferences
20+
import androidx.core.content.edit
21+
import com.duckduckgo.data.store.api.SharedPreferencesProvider
22+
import com.duckduckgo.di.scopes.AppScope
23+
import com.squareup.anvil.annotations.ContributesBinding
24+
import javax.inject.Inject
25+
26+
interface RebrandingDataStore {
27+
var rebrandingBannerShown: Boolean
28+
}
29+
30+
@ContributesBinding(AppScope::class)
31+
class SharedPreferencesRebrandingDataStore @Inject constructor(
32+
private val sharedPreferencesProvider: SharedPreferencesProvider,
33+
) : RebrandingDataStore {
34+
private val sharedPreferences: SharedPreferences? by lazy { sharedPreferences() }
35+
36+
private fun sharedPreferences(): SharedPreferences? {
37+
return try {
38+
sharedPreferencesProvider.getSharedPreferences(FILENAME)
39+
} catch (e: Exception) {
40+
null
41+
}
42+
}
43+
44+
override var rebrandingBannerShown: Boolean
45+
get() = sharedPreferences?.getBoolean(KEY_REBRANDING_BANNER_SHOWN, false) ?: false
46+
set(value) {
47+
sharedPreferences?.edit(commit = true) {
48+
putBoolean(KEY_REBRANDING_BANNER_SHOWN, value)
49+
}
50+
}
51+
52+
companion object {
53+
const val FILENAME = "com.duckduckgo.subscriptions.rebranding.store"
54+
const val KEY_REBRANDING_BANNER_SHOWN = "KEY_REBRANDING_BANNER_SHOWN"
55+
}
56+
}

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import com.duckduckgo.subscriptions.impl.databinding.ActivitySubscriptionSetting
5252
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
5353
import com.duckduckgo.subscriptions.impl.ui.ChangePlanActivity.Companion.ChangePlanScreenWithEmptyParams
5454
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command
55+
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.DismissRebrandingBanner
5556
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.FinishSignOut
5657
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.GoToActivationScreen
5758
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.GoToEditEmailScreen
@@ -177,6 +178,14 @@ class SubscriptionSettingsActivity : DuckDuckGoActivity() {
177178
binding.faq.setPrimaryText(getString(string.privacyProFaq))
178179
binding.faq.setSecondaryText(getString(string.privacyProFaqSecondary))
179180
}
181+
if (viewState.showRebrandingBanner) {
182+
binding.includePrivacyProRebrandingBanner.root.show()
183+
binding.includePrivacyProRebrandingBanner.settingsBannerClose.setOnClickListener {
184+
viewModel.rebrandingBannerDismissed()
185+
}
186+
} else {
187+
binding.includePrivacyProRebrandingBanner.root.gone()
188+
}
180189

181190
if (viewState.status in listOf(INACTIVE, EXPIRED)) {
182191
binding.viewPlans.isVisible = true
@@ -291,6 +300,8 @@ class SubscriptionSettingsActivity : DuckDuckGoActivity() {
291300
),
292301
)
293302
}
303+
304+
is DismissRebrandingBanner -> dismissRebrandingBanner()
294305
}
295306
}
296307

@@ -352,6 +363,10 @@ class SubscriptionSettingsActivity : DuckDuckGoActivity() {
352363
)
353364
}
354365

366+
private fun dismissRebrandingBanner() {
367+
binding.includePrivacyProRebrandingBanner.root.gone()
368+
}
369+
355370
companion object {
356371
const val URL = "https://play.google.com/store/account/subscriptions?sku=%s&package=%s"
357372
const val MANAGE_URL = "https://duckduckgo.com/subscriptions/manage"

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN_ROW
3333
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN_US
3434
import com.duckduckgo.subscriptions.impl.SubscriptionsManager
3535
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
36+
import com.duckduckgo.subscriptions.impl.repository.RebrandingRepository
37+
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.DismissRebrandingBanner
3638
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.FinishSignOut
3739
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.GoToActivationScreen
3840
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.GoToEditEmailScreen
@@ -60,6 +62,7 @@ class SubscriptionSettingsViewModel @Inject constructor(
6062
private val pixelSender: SubscriptionPixelSender,
6163
private val privacyProUnifiedFeedback: PrivacyProUnifiedFeedback,
6264
private val subscriptionRebrandingFeatureToggle: SubscriptionRebrandingFeatureToggle,
65+
private val rebrandingRepository: RebrandingRepository,
6366
) : ViewModel(), DefaultLifecycleObserver {
6467

6568
private val command = Channel<Command>(1, DROP_OLDEST)
@@ -103,10 +106,14 @@ class SubscriptionSettingsViewModel @Inject constructor(
103106
showFeedback = privacyProUnifiedFeedback.shouldUseUnifiedFeedback(source = SUBSCRIPTION_SETTINGS),
104107
activeOffers = subscription.activeOffers,
105108
rebrandingEnabled = subscriptionRebrandingFeatureToggle.isSubscriptionRebrandingEnabled(),
109+
showRebrandingBanner = shouldShowRebrandingBanner(),
106110
),
107111
)
108112
}
109113

114+
private suspend fun shouldShowRebrandingBanner(): Boolean =
115+
subscriptionRebrandingFeatureToggle.isSubscriptionRebrandingEnabled() && !rebrandingRepository.isRebrandingBannerShown()
116+
110117
fun onEditEmailButtonClicked() {
111118
viewModelScope.launch {
112119
command.send(GoToEditEmailScreen)
@@ -135,6 +142,13 @@ class SubscriptionSettingsViewModel @Inject constructor(
135142
}
136143
}
137144

145+
fun rebrandingBannerDismissed() {
146+
viewModelScope.launch {
147+
rebrandingRepository.setRebrandingBannerAsViewed()
148+
command.send(DismissRebrandingBanner)
149+
}
150+
}
151+
138152
sealed class SubscriptionDuration {
139153
data object Monthly : SubscriptionDuration()
140154
data object Yearly : SubscriptionDuration()
@@ -145,6 +159,7 @@ class SubscriptionSettingsViewModel @Inject constructor(
145159
data object GoToEditEmailScreen : Command()
146160
data object GoToActivationScreen : Command()
147161
data class GoToPortal(val url: String) : Command()
162+
data object DismissRebrandingBanner : Command()
148163
}
149164

150165
sealed class ViewState {
@@ -159,6 +174,7 @@ class SubscriptionSettingsViewModel @Inject constructor(
159174
val showFeedback: Boolean = false,
160175
val activeOffers: List<ActiveOfferType>,
161176
val rebrandingEnabled: Boolean = false,
177+
val showRebrandingBanner: Boolean = false,
162178
) : ViewState()
163179
}
164180
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
android:orientation="vertical"
3838
android:paddingBottom="@dimen/keyline_5">
3939

40+
<include
41+
android:id="@+id/includePrivacyProRebrandingBanner"
42+
layout="@layout/view_settings_info_banner" />
43+
4044
<ImageView
4145
android:layout_width="wrap_content"
4246
android:layout_height="wrap_content"
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
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+
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:app="http://schemas.android.com/apk/res-auto"
19+
xmlns:tools="http://schemas.android.com/tools"
20+
android:layout_width="match_parent"
21+
android:layout_height="wrap_content">
22+
23+
<com.google.android.material.card.MaterialCardView
24+
android:id="@+id/settingsBannerCard"
25+
android:layout_width="match_parent"
26+
android:layout_height="wrap_content"
27+
android:layout_marginBottom="@dimen/horizontalDividerHeight"
28+
app:cardCornerRadius="@dimen/keyline_empty"
29+
app:cardElevation="@dimen/keyline_4">
30+
31+
<androidx.constraintlayout.widget.ConstraintLayout
32+
android:id="@+id/settingsBannerContent"
33+
android:layout_width="match_parent"
34+
android:layout_height="match_parent">
35+
36+
<com.duckduckgo.common.ui.view.text.DaxTextView
37+
android:id="@+id/settingsBannerText"
38+
android:layout_width="@dimen/keyline_empty"
39+
android:layout_height="wrap_content"
40+
android:padding="@dimen/keyline_4"
41+
android:text="@string/subscriptionSettingRebrandingMessage"
42+
app:layout_constraintEnd_toStartOf="@id/settingsBannerClose"
43+
app:layout_constraintStart_toStartOf="parent"
44+
app:layout_constraintTop_toTopOf="parent"
45+
app:typography="h3" />
46+
47+
<ImageView
48+
android:id="@+id/settingsBannerClose"
49+
android:layout_width="wrap_content"
50+
android:layout_height="wrap_content"
51+
android:layout_marginTop="@dimen/keyline_2"
52+
android:layout_marginEnd="@dimen/keyline_2"
53+
android:background="?selectableItemBackgroundBorderless"
54+
android:src="@drawable/ic_close_24"
55+
app:layout_constraintEnd_toEndOf="parent"
56+
app:layout_constraintStart_toEndOf="@id/settingsBannerText"
57+
app:layout_constraintTop_toTopOf="parent"
58+
tools:ignore="ContentDescription" />
59+
60+
</androidx.constraintlayout.widget.ConstraintLayout>
61+
</com.google.android.material.card.MaterialCardView>
62+
</FrameLayout>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@
4040
<string name="activateOnOtherDevicesRebranding">Add Your Subscription to Other Devices</string>
4141
<string name="addToDeviceSecondaryTextWithoutEmailRebranding">Add your subscription to your other devices via Google Play or by linking an email address.</string>
4242
<string name="addToDeviceSecondaryTextWithEmailRebranding">Use the email above to add your subscription on your other devices in the DuckDuckGo app. Go to Settings > Subscription Settings > I Have a Subscription.</string>
43+
<string name="subscriptionSettingRebrandingMessage">Privacy Pro is now just called the DuckDuckGo subscription</string>"
4344
</resources>

subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import com.duckduckgo.subscriptions.impl.SubscriptionsConstants
1111
import com.duckduckgo.subscriptions.impl.SubscriptionsManager
1212
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
1313
import com.duckduckgo.subscriptions.impl.repository.Account
14+
import com.duckduckgo.subscriptions.impl.repository.RebrandingRepository
1415
import com.duckduckgo.subscriptions.impl.repository.Subscription
16+
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.DismissRebrandingBanner
1517
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.FinishSignOut
1618
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.GoToActivationScreen
1719
import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.GoToEditEmailScreen
@@ -42,6 +44,7 @@ class SubscriptionSettingsViewModelTest {
4244
private val pixelSender: SubscriptionPixelSender = mock()
4345
private val privacyProUnifiedFeedback: PrivacyProUnifiedFeedback = mock()
4446
private val mockSubscriptionRebrandingFeatureToggle: SubscriptionRebrandingFeatureToggle = mock()
47+
private val mockRebrandingRepository: RebrandingRepository = mock()
4548

4649
private lateinit var viewModel: SubscriptionSettingsViewModel
4750

@@ -52,6 +55,7 @@ class SubscriptionSettingsViewModelTest {
5255
pixelSender,
5356
privacyProUnifiedFeedback,
5457
mockSubscriptionRebrandingFeatureToggle,
58+
mockRebrandingRepository,
5559
)
5660
}
5761

@@ -225,4 +229,14 @@ class SubscriptionSettingsViewModelTest {
225229
viewModel.removeFromDevice()
226230
verify(pixelSender).reportSubscriptionSettingsRemoveFromDeviceClick()
227231
}
232+
233+
@Test
234+
fun whenDismissRebrandingBannerThenSetRebrandingAsViewedAndDismissBannerCommandIsSent() = runTest {
235+
viewModel.commands().test {
236+
viewModel.rebrandingBannerDismissed()
237+
verify(mockRebrandingRepository).setRebrandingBannerAsViewed()
238+
assertEquals(DismissRebrandingBanner, awaitItem())
239+
cancelAndIgnoreRemainingEvents()
240+
}
241+
}
228242
}

0 commit comments

Comments
 (0)