From f0859c2ad7bf9c69a9bedf8e2b5158314898fc28 Mon Sep 17 00:00:00 2001 From: nalcalag Date: Thu, 10 Jul 2025 14:16:56 +0100 Subject: [PATCH 1/2] Updated onboarding entry points --- .../java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt | 1 + .../main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt | 9 ++++++++- .../ExtendedOnboardingFeatureToggles.kt | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index eb71b9267c23..931b8cd66882 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -875,6 +875,7 @@ class CtaViewModelTest { whenever(mockSubscriptions.isEligible()).thenReturn(true) whenever(mockExtendedOnboardingFeatureToggles.noBrowserCtas()).thenReturn(mockEnabledToggle) whenever(mockExtendedOnboardingFeatureToggles.privacyProCta()).thenReturn(mockEnabledToggle) + whenever(mockExtendedOnboardingFeatureToggles.freeTrialCopy()).thenReturn(mockDisabledToggle) whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO)).thenReturn(true) whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO_VISIT_SITE)).thenReturn(true) whenever(mockDismissedCtaDao.exists(CtaId.DAX_END)).thenReturn(true) diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index dd3e10093014..f0d533d22094 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -275,7 +275,11 @@ class CtaViewModel @Inject constructor( // Privacy Pro canShowPrivacyProCta() -> { val titleRes: Int = R.string.onboardingPrivacyProDaxDialogTitle - val descriptionRes: Int = R.string.onboardingPrivacyProDaxDialogDescription + val descriptionRes: Int = if (freeTrialCopyAvailable()) { + R.string.onboardingPrivacyProDaxDialogFreeTrialDescription + } else { + R.string.onboardingPrivacyProDaxDialogDescription + } DaxBubbleCta.DaxPrivacyProCta(onboardingStore, appInstallStore, titleRes, descriptionRes) } @@ -320,6 +324,9 @@ class CtaViewModel @Inject constructor( return !widgetCapabilities.hasInstalledWidgets && !dismissedCtaDao.exists(CtaId.ADD_WIDGET) } + private suspend fun freeTrialCopyAvailable(): Boolean = + extendedOnboardingFeatureToggles.freeTrialCopy().isEnabled() && subscriptions.isFreeTrialEligible() + @WorkerThread private suspend fun getBrowserCta(site: Site?, detectedRefreshPatterns: Set): Cta? { val nonNullSite = site ?: return null diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt index 91753f505c4b..d17e6987b6dc 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt @@ -39,5 +39,5 @@ interface ExtendedOnboardingFeatureToggles { @Toggle.DefaultValue(DefaultFeatureValue.FALSE) @Experiment - fun highlights(): Toggle + fun freeTrialCopy(): Toggle } From d4e7b9d38ef0a6d65616025bdb55f62c98925c88 Mon Sep 17 00:00:00 2001 From: nalcalag Date: Thu, 10 Jul 2025 14:17:19 +0100 Subject: [PATCH 2/2] Updated appTP entry points --- .../view/message/AppTPStateMessageToggle.kt | 34 +++++++++++++++++++ .../view/message/PProUpsellBannerPlugin.kt | 16 +++++++-- .../PproUpsellDisabledMessagePlugin.kt | 10 +++++- .../message/PproUpsellRevokedMessagePlugin.kt | 10 +++++- .../message/PProUpsellBannerPluginTest.kt | 11 ++++-- .../PproUpsellDisabledMessagePluginTest.kt | 11 ++++-- .../PproUpsellRevokedMessagePluginTest.kt | 11 ++++-- 7 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/AppTPStateMessageToggle.kt diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/AppTPStateMessageToggle.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/AppTPStateMessageToggle.kt new file mode 100644 index 000000000000..f4592cc0061e --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/AppTPStateMessageToggle.kt @@ -0,0 +1,34 @@ +/* + * 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.mobile.android.vpn.ui.tracker_activity.view.message + +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue + +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "freeTrialInPProUpsell", +) +interface AppTPStateMessageToggle { + @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + fun self(): Toggle + + @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + fun freeTrialCopy(): Toggle +} diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPlugin.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPlugin.kt index f52dfbc8a979..0ce5b7c53221 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPlugin.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPlugin.kt @@ -47,6 +47,7 @@ class PProUpsellBannerPlugin @Inject constructor( private val browserNav: BrowserNav, private val vpnStore: VpnStore, private val deviceShieldPixels: DeviceShieldPixels, + private val appTPStateMessageToggle: AppTPStateMessageToggle, ) : AppTPStateMessagePlugin { override fun getView( context: Context, @@ -55,14 +56,25 @@ class PProUpsellBannerPlugin @Inject constructor( ): View? { val isEligible = runBlocking { subscriptions.isUpsellEligible() && !vpnStore.isPproUpsellBannerDismised() } return if (isEligible) { + val subtitle: String + val actionText: String + runBlocking { + if (subscriptions.isFreeTrialEligible() && appTPStateMessageToggle.freeTrialCopy().isEnabled()) { + subtitle = context.getString(R.string.apptp_PproUpsellBannerMessage_freeTrial) + actionText = context.getString(R.string.apptp_PproUpsellBannerAction_freeTrial) + } else { + subtitle = context.getString(R.string.apptp_PproUpsellBannerMessage) + actionText = context.getString(R.string.apptp_PproUpsellBannerAction) + } + } MessageCta(context) .apply { this.setMessage( Message( topIllustration = com.duckduckgo.mobile.android.R.drawable.ic_privacy_pro, title = context.getString(R.string.apptp_PproUpsellBannerTitle), - subtitle = context.getString(R.string.apptp_PproUpsellBannerMessage), - action = context.getString(R.string.apptp_PproUpsellBannerAction), + subtitle = subtitle, + action = actionText, messageType = REMOTE_MESSAGE, ), ) diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePlugin.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePlugin.kt index 0023d1ade7cc..c97bab01265b 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePlugin.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePlugin.kt @@ -45,6 +45,7 @@ class PproUpsellDisabledMessagePlugin @Inject constructor( private val vpnDetector: ExternalVpnDetector, private val browserNav: BrowserNav, private val deviceShieldPixels: DeviceShieldPixels, + private val appTPStateMessageToggle: AppTPStateMessageToggle, ) : AppTPStateMessagePlugin { override fun getView( context: Context, @@ -53,10 +54,17 @@ class PproUpsellDisabledMessagePlugin @Inject constructor( ): View? { val isEligible = runBlocking { vpnDetector.isExternalVpnDetected() && subscriptions.isUpsellEligible() } return if (vpnState.state == DISABLED && vpnState.stopReason is SELF_STOP && isEligible) { + val messageRes = runBlocking { + if (subscriptions.isFreeTrialEligible() && appTPStateMessageToggle.freeTrialCopy().isEnabled()) { + R.string.apptp_PproUpsellInfoDisabled_freeTrial + } else { + R.string.apptp_PproUpsellInfoDisabled + } + } AppTpDisabledInfoPanel(context).apply { setClickableLink( PPRO_UPSELL_ANNOTATION, - context.getText(R.string.apptp_PproUpsellInfoDisabled), + context.getText(messageRes), ) { context.launchPPro() } doOnAttach { deviceShieldPixels.reportPproUpsellDisabledInfoShown() diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePlugin.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePlugin.kt index 280f5affcd8a..31809669c5e6 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePlugin.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePlugin.kt @@ -43,6 +43,7 @@ class PproUpsellRevokedMessagePlugin @Inject constructor( private val subscriptions: Subscriptions, private val browserNav: BrowserNav, private val deviceShieldPixels: DeviceShieldPixels, + private val appTPStateMessageToggle: AppTPStateMessageToggle, ) : AppTPStateMessagePlugin { override fun getView( context: Context, @@ -51,10 +52,17 @@ class PproUpsellRevokedMessagePlugin @Inject constructor( ): View? { val isEligible = runBlocking { subscriptions.isUpsellEligible() } return if (vpnState.state == DISABLED && vpnState.stopReason == REVOKED && isEligible) { + val messageRes = runBlocking { + if (subscriptions.isFreeTrialEligible() && appTPStateMessageToggle.freeTrialCopy().isEnabled()) { + R.string.apptp_PproUpsellInfoRevoked_freeTrial + } else { + R.string.apptp_PproUpsellInfoRevoked + } + } AppTpDisabledInfoPanel(context).apply { setClickableLink( PPRO_UPSELL_ANNOTATION, - context.getText(R.string.apptp_PproUpsellInfoRevoked), + context.getText(messageRes), ) { context.launchPPro() } doOnAttach { deviceShieldPixels.reportPproUpsellRevokedInfoShown() diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPluginTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPluginTest.kt index 2830df3f0d01..73ab7b74e2d2 100644 --- a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPluginTest.kt +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPluginTest.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.tabs.BrowserNav +import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.DISABLED import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.ENABLED @@ -14,6 +15,7 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -23,13 +25,18 @@ class PProUpsellBannerPluginTest { private val browserNav: BrowserNav = mock() private val subscriptions: Subscriptions = mock() private val deviceShieldPixels: DeviceShieldPixels = mock() + private val appTPStateMessageToggle: AppTPStateMessageToggle = mock() private lateinit var vpnStore: FakeVPNStore private lateinit var plugin: PProUpsellBannerPlugin + private val mockDisabledToggle: Toggle = mock { on { it.isEnabled() } doReturn false } + @Before - fun setUp() { + fun setUp() = runTest { vpnStore = FakeVPNStore(pproUpsellBannerDismissed = false) - plugin = PProUpsellBannerPlugin(subscriptions, browserNav, vpnStore, deviceShieldPixels) + whenever(subscriptions.isFreeTrialEligible()).thenReturn(false) + whenever(appTPStateMessageToggle.freeTrialCopy()).thenReturn(mockDisabledToggle) + plugin = PProUpsellBannerPlugin(subscriptions, browserNav, vpnStore, deviceShieldPixels, appTPStateMessageToggle) } @Test diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePluginTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePluginTest.kt index 9c2f5fcedd5d..0bd91a0fc8c4 100644 --- a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePluginTest.kt +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePluginTest.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.tabs.BrowserNav +import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.mobile.android.vpn.network.ExternalVpnDetector import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.DISABLED @@ -18,6 +19,7 @@ import org.junit.Assert.* import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -28,11 +30,16 @@ class PproUpsellDisabledMessagePluginTest { private val subscriptions: Subscriptions = mock() private val deviceShieldPixels: DeviceShieldPixels = mock() private val vpnDetector: ExternalVpnDetector = mock() + private val appTPStateMessageToggle: AppTPStateMessageToggle = mock() private lateinit var plugin: PproUpsellDisabledMessagePlugin + private val mockDisabledToggle: Toggle = mock { on { it.isEnabled() } doReturn false } + @Before - fun setUp() { - plugin = PproUpsellDisabledMessagePlugin(subscriptions, vpnDetector, browserNav, deviceShieldPixels) + fun setUp() = runTest { + whenever(subscriptions.isFreeTrialEligible()).thenReturn(false) + whenever(appTPStateMessageToggle.freeTrialCopy()).thenReturn(mockDisabledToggle) + plugin = PproUpsellDisabledMessagePlugin(subscriptions, vpnDetector, browserNav, deviceShieldPixels, appTPStateMessageToggle) } @Test diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePluginTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePluginTest.kt index 5f1b5590214b..3f87e2119e81 100644 --- a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePluginTest.kt +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePluginTest.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.tabs.BrowserNav +import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.DISABLED import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.ENABLED @@ -17,6 +18,7 @@ import org.junit.Assert.* import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -26,11 +28,16 @@ class PproUpsellRevokedMessagePluginTest { private val browserNav: BrowserNav = mock() private val subscriptions: Subscriptions = mock() private val deviceShieldPixels: DeviceShieldPixels = mock() + private val appTPStateMessageToggle: AppTPStateMessageToggle = mock() private lateinit var plugin: PproUpsellRevokedMessagePlugin + private val mockDisabledToggle: Toggle = mock { on { it.isEnabled() } doReturn false } + @Before - fun setUp() { - plugin = PproUpsellRevokedMessagePlugin(subscriptions, browserNav, deviceShieldPixels) + fun setUp() = runTest { + whenever(subscriptions.isFreeTrialEligible()).thenReturn(false) + whenever(appTPStateMessageToggle.freeTrialCopy()).thenReturn(mockDisabledToggle) + plugin = PproUpsellRevokedMessagePlugin(subscriptions, browserNav, deviceShieldPixels, appTPStateMessageToggle) } @Test