Skip to content

Commit 1a1270d

Browse files
authored
Free Trial: Update Privacy Pro entry points in all locales (#6370)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1201807753394693/task/1209708166734695?focus=true ### Description Add Free Trial copy to app Privacy Pro entry points if eligible and FF enabled ### Steps to test this PR _Pre-steps_ - [x] Apply PPro patch -> https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true - [x] Default freeTrialCopy() Toggle to TRUE in `ExtendedOnboardingFeatureToggles` and `AppTPStateMessageToggle` _Onboarding_ - [ ] Fresh install - [ ] Go through onboarding - [ ] Check last context dialog (Privacy Pro) is showing Free Trial copy _Banner_ - [x] Fresh install - [x] Go to Settings > App Tracking Protection - [x] Do AppTP onboarding - [x] Check Privacy Pro banner shows free trial copy _Revoked_ - [x] Enable AppTP - [x] Enable an external VPN on your phone - [x] Go back to AppTP screen - [x] Check revoked banner shows free trial copy _Disabled_ - [x] Disable AppTP - [x] Enable an external VPN on your phone - [x] Go back to AppTP screen - [x] Check disabled banner shows free trial copy _Translations (Optional)_ - [ ] Switch your device language to something other than English - [ ] Check that all previous entry points have the Free Trial copy translated _FF disabled_ - [x] Remove the second pre-step modification and set the feature flags to FALSE by default again - [x] Check none of the previous entry points have the free trial copy ### No UI changes
1 parent a64924f commit 1a1270d

File tree

14 files changed

+117
-16
lines changed

14 files changed

+117
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.mobile.android.vpn.ui.tracker_activity.view.message
18+
19+
import com.duckduckgo.anvil.annotations.ContributesRemoteFeature
20+
import com.duckduckgo.di.scopes.AppScope
21+
import com.duckduckgo.feature.toggles.api.Toggle
22+
import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue
23+
24+
@ContributesRemoteFeature(
25+
scope = AppScope::class,
26+
featureName = "freeTrialInPProUpsell",
27+
)
28+
interface AppTPStateMessageToggle {
29+
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
30+
fun self(): Toggle
31+
32+
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
33+
fun freeTrialCopy(): Toggle
34+
}

app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPlugin.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class PProUpsellBannerPlugin @Inject constructor(
4747
private val browserNav: BrowserNav,
4848
private val vpnStore: VpnStore,
4949
private val deviceShieldPixels: DeviceShieldPixels,
50+
private val appTPStateMessageToggle: AppTPStateMessageToggle,
5051
) : AppTPStateMessagePlugin {
5152
override fun getView(
5253
context: Context,
@@ -55,14 +56,25 @@ class PProUpsellBannerPlugin @Inject constructor(
5556
): View? {
5657
val isEligible = runBlocking { subscriptions.isUpsellEligible() && !vpnStore.isPproUpsellBannerDismised() }
5758
return if (isEligible) {
59+
val subtitle: String
60+
val actionText: String
61+
runBlocking {
62+
if (subscriptions.isFreeTrialEligible() && appTPStateMessageToggle.freeTrialCopy().isEnabled()) {
63+
subtitle = context.getString(R.string.apptp_PproUpsellBannerMessage_freeTrial)
64+
actionText = context.getString(R.string.apptp_PproUpsellBannerAction_freeTrial)
65+
} else {
66+
subtitle = context.getString(R.string.apptp_PproUpsellBannerMessage)
67+
actionText = context.getString(R.string.apptp_PproUpsellBannerAction)
68+
}
69+
}
5870
MessageCta(context)
5971
.apply {
6072
this.setMessage(
6173
Message(
6274
topIllustration = com.duckduckgo.mobile.android.R.drawable.ic_privacy_pro,
6375
title = context.getString(R.string.apptp_PproUpsellBannerTitle),
64-
subtitle = context.getString(R.string.apptp_PproUpsellBannerMessage),
65-
action = context.getString(R.string.apptp_PproUpsellBannerAction),
76+
subtitle = subtitle,
77+
action = actionText,
6678
messageType = REMOTE_MESSAGE,
6779
),
6880
)

app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePlugin.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class PproUpsellDisabledMessagePlugin @Inject constructor(
4545
private val vpnDetector: ExternalVpnDetector,
4646
private val browserNav: BrowserNav,
4747
private val deviceShieldPixels: DeviceShieldPixels,
48+
private val appTPStateMessageToggle: AppTPStateMessageToggle,
4849
) : AppTPStateMessagePlugin {
4950
override fun getView(
5051
context: Context,
@@ -53,10 +54,17 @@ class PproUpsellDisabledMessagePlugin @Inject constructor(
5354
): View? {
5455
val isEligible = runBlocking { vpnDetector.isExternalVpnDetected() && subscriptions.isUpsellEligible() }
5556
return if (vpnState.state == DISABLED && vpnState.stopReason is SELF_STOP && isEligible) {
57+
val messageRes = runBlocking {
58+
if (subscriptions.isFreeTrialEligible() && appTPStateMessageToggle.freeTrialCopy().isEnabled()) {
59+
R.string.apptp_PproUpsellInfoDisabled_freeTrial
60+
} else {
61+
R.string.apptp_PproUpsellInfoDisabled
62+
}
63+
}
5664
AppTpDisabledInfoPanel(context).apply {
5765
setClickableLink(
5866
PPRO_UPSELL_ANNOTATION,
59-
context.getText(R.string.apptp_PproUpsellInfoDisabled),
67+
context.getText(messageRes),
6068
) { context.launchPPro() }
6169
doOnAttach {
6270
deviceShieldPixels.reportPproUpsellDisabledInfoShown()

app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePlugin.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class PproUpsellRevokedMessagePlugin @Inject constructor(
4343
private val subscriptions: Subscriptions,
4444
private val browserNav: BrowserNav,
4545
private val deviceShieldPixels: DeviceShieldPixels,
46+
private val appTPStateMessageToggle: AppTPStateMessageToggle,
4647
) : AppTPStateMessagePlugin {
4748
override fun getView(
4849
context: Context,
@@ -51,10 +52,17 @@ class PproUpsellRevokedMessagePlugin @Inject constructor(
5152
): View? {
5253
val isEligible = runBlocking { subscriptions.isUpsellEligible() }
5354
return if (vpnState.state == DISABLED && vpnState.stopReason == REVOKED && isEligible) {
55+
val messageRes = runBlocking {
56+
if (subscriptions.isFreeTrialEligible() && appTPStateMessageToggle.freeTrialCopy().isEnabled()) {
57+
R.string.apptp_PproUpsellInfoRevoked_freeTrial
58+
} else {
59+
R.string.apptp_PproUpsellInfoRevoked
60+
}
61+
}
5462
AppTpDisabledInfoPanel(context).apply {
5563
setClickableLink(
5664
PPRO_UPSELL_ANNOTATION,
57-
context.getText(R.string.apptp_PproUpsellInfoRevoked),
65+
context.getText(messageRes),
5866
) { context.launchPPro() }
5967
doOnAttach {
6068
deviceShieldPixels.reportPproUpsellRevokedInfoShown()

app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPluginTest.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import androidx.test.ext.junit.runners.AndroidJUnit4
55
import androidx.test.platform.app.InstrumentationRegistry
66
import com.duckduckgo.app.tabs.BrowserNav
7+
import com.duckduckgo.feature.toggles.api.Toggle
78
import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels
89
import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.DISABLED
910
import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.ENABLED
@@ -14,6 +15,7 @@ import org.junit.Assert.assertNull
1415
import org.junit.Before
1516
import org.junit.Test
1617
import org.junit.runner.RunWith
18+
import org.mockito.kotlin.doReturn
1719
import org.mockito.kotlin.mock
1820
import org.mockito.kotlin.whenever
1921

@@ -23,13 +25,18 @@ class PProUpsellBannerPluginTest {
2325
private val browserNav: BrowserNav = mock()
2426
private val subscriptions: Subscriptions = mock()
2527
private val deviceShieldPixels: DeviceShieldPixels = mock()
28+
private val appTPStateMessageToggle: AppTPStateMessageToggle = mock()
2629
private lateinit var vpnStore: FakeVPNStore
2730
private lateinit var plugin: PProUpsellBannerPlugin
2831

32+
private val mockDisabledToggle: Toggle = mock { on { it.isEnabled() } doReturn false }
33+
2934
@Before
30-
fun setUp() {
35+
fun setUp() = runTest {
3136
vpnStore = FakeVPNStore(pproUpsellBannerDismissed = false)
32-
plugin = PProUpsellBannerPlugin(subscriptions, browserNav, vpnStore, deviceShieldPixels)
37+
whenever(subscriptions.isFreeTrialEligible()).thenReturn(false)
38+
whenever(appTPStateMessageToggle.freeTrialCopy()).thenReturn(mockDisabledToggle)
39+
plugin = PProUpsellBannerPlugin(subscriptions, browserNav, vpnStore, deviceShieldPixels, appTPStateMessageToggle)
3340
}
3441

3542
@Test

app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePluginTest.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import androidx.test.ext.junit.runners.AndroidJUnit4
55
import androidx.test.platform.app.InstrumentationRegistry
66
import com.duckduckgo.app.tabs.BrowserNav
7+
import com.duckduckgo.feature.toggles.api.Toggle
78
import com.duckduckgo.mobile.android.vpn.network.ExternalVpnDetector
89
import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels
910
import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.DISABLED
@@ -18,6 +19,7 @@ import org.junit.Assert.*
1819
import org.junit.Before
1920
import org.junit.Test
2021
import org.junit.runner.RunWith
22+
import org.mockito.kotlin.doReturn
2123
import org.mockito.kotlin.mock
2224
import org.mockito.kotlin.whenever
2325

@@ -28,11 +30,16 @@ class PproUpsellDisabledMessagePluginTest {
2830
private val subscriptions: Subscriptions = mock()
2931
private val deviceShieldPixels: DeviceShieldPixels = mock()
3032
private val vpnDetector: ExternalVpnDetector = mock()
33+
private val appTPStateMessageToggle: AppTPStateMessageToggle = mock()
3134
private lateinit var plugin: PproUpsellDisabledMessagePlugin
3235

36+
private val mockDisabledToggle: Toggle = mock { on { it.isEnabled() } doReturn false }
37+
3338
@Before
34-
fun setUp() {
35-
plugin = PproUpsellDisabledMessagePlugin(subscriptions, vpnDetector, browserNav, deviceShieldPixels)
39+
fun setUp() = runTest {
40+
whenever(subscriptions.isFreeTrialEligible()).thenReturn(false)
41+
whenever(appTPStateMessageToggle.freeTrialCopy()).thenReturn(mockDisabledToggle)
42+
plugin = PproUpsellDisabledMessagePlugin(subscriptions, vpnDetector, browserNav, deviceShieldPixels, appTPStateMessageToggle)
3643
}
3744

3845
@Test

app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePluginTest.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import androidx.test.ext.junit.runners.AndroidJUnit4
55
import androidx.test.platform.app.InstrumentationRegistry
66
import com.duckduckgo.app.tabs.BrowserNav
7+
import com.duckduckgo.feature.toggles.api.Toggle
78
import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels
89
import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.DISABLED
910
import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.ENABLED
@@ -17,6 +18,7 @@ import org.junit.Assert.*
1718
import org.junit.Before
1819
import org.junit.Test
1920
import org.junit.runner.RunWith
21+
import org.mockito.kotlin.doReturn
2022
import org.mockito.kotlin.mock
2123
import org.mockito.kotlin.whenever
2224

@@ -26,11 +28,16 @@ class PproUpsellRevokedMessagePluginTest {
2628
private val browserNav: BrowserNav = mock()
2729
private val subscriptions: Subscriptions = mock()
2830
private val deviceShieldPixels: DeviceShieldPixels = mock()
31+
private val appTPStateMessageToggle: AppTPStateMessageToggle = mock()
2932
private lateinit var plugin: PproUpsellRevokedMessagePlugin
3033

34+
private val mockDisabledToggle: Toggle = mock { on { it.isEnabled() } doReturn false }
35+
3136
@Before
32-
fun setUp() {
33-
plugin = PproUpsellRevokedMessagePlugin(subscriptions, browserNav, deviceShieldPixels)
37+
fun setUp() = runTest {
38+
whenever(subscriptions.isFreeTrialEligible()).thenReturn(false)
39+
whenever(appTPStateMessageToggle.freeTrialCopy()).thenReturn(mockDisabledToggle)
40+
plugin = PproUpsellRevokedMessagePlugin(subscriptions, browserNav, deviceShieldPixels, appTPStateMessageToggle)
3441
}
3542

3643
@Test

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2782,6 +2782,7 @@ class BrowserTabViewModelTest {
27822782
mockAppInstallStore,
27832783
R.string.onboardingPrivacyProDaxDialogTitle,
27842784
R.string.onboardingPrivacyProDaxDialogDescription,
2785+
R.string.onboardingPrivacyProDaxDialogOkButton,
27852786
)
27862787
setCta(cta)
27872788
testee.onUserClickCtaOkButton(cta)

app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,7 @@ class CtaViewModelTest {
875875
whenever(mockSubscriptions.isEligible()).thenReturn(true)
876876
whenever(mockExtendedOnboardingFeatureToggles.noBrowserCtas()).thenReturn(mockEnabledToggle)
877877
whenever(mockExtendedOnboardingFeatureToggles.privacyProCta()).thenReturn(mockEnabledToggle)
878+
whenever(mockExtendedOnboardingFeatureToggles.freeTrialCopy()).thenReturn(mockDisabledToggle)
878879
whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO)).thenReturn(true)
879880
whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO_VISIT_SITE)).thenReturn(true)
880881
whenever(mockDismissedCtaDao.exists(CtaId.DAX_END)).thenReturn(true)

app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,12 +716,13 @@ sealed class DaxBubbleCta(
716716
override val appInstallStore: AppInstallStore,
717717
val titleRes: Int,
718718
val descriptionRes: Int,
719+
val primaryCtaRes: Int,
719720
) : DaxBubbleCta(
720721
ctaId = CtaId.DAX_INTRO_PRIVACY_PRO,
721722
title = titleRes,
722723
description = descriptionRes,
723724
placeholder = com.duckduckgo.mobile.android.R.drawable.ic_privacy_pro_128,
724-
primaryCta = R.string.onboardingPrivacyProDaxDialogOkButton,
725+
primaryCta = primaryCtaRes,
725726
shownPixel = AppPixelName.ONBOARDING_DAX_CTA_SHOWN,
726727
okPixel = AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON,
727728
ctaPixelParam = Pixel.PixelValues.DAX_PRIVACY_PRO,

0 commit comments

Comments
 (0)