From 360893adce4ff22baed7be5b131fc770748d54d0 Mon Sep 17 00:00:00 2001 From: Karl Dimla Date: Wed, 4 Sep 2024 10:42:09 +0200 Subject: [PATCH] Add privacy pro upsell in AppTP (#4953) Task/Issue URL: https://app.asana.com/0/38424471409662/1208075739156212/f ### Description See attached task description ### Steps to test this PR https://app.asana.com/0/38424471409662/1208168085013251/f --- app-tracking-protection/vpn-impl/build.gradle | 1 + .../vpn/pixels/DeviceShieldPixelNames.kt | 26 ++++ .../android/vpn/pixels/DeviceShieldPixels.kt | 50 +++++++ .../vpn/pixels/VpnPixelParamRemovalPlugin.kt | 3 + .../android/vpn/ui/onboarding/VpnStore.kt | 12 ++ .../DeviceShieldTrackerActivity.kt | 125 +++++++++--------- .../DeviceShieldTrackerActivityViewModel.kt | 14 -- .../ActionRequiredDisabledMessagePlugin.kt | 57 ++++++++ .../view/message/AppTPStateMessagePlugin.kt | 53 ++++++++ .../view/message/AppTpInfoPanel.kt | 58 ++++++++ .../message/DisabledBySystemMessagePlugin.kt | 57 ++++++++ .../view/message/DisabledMessagePlugin.kt | 55 ++++++++ .../NextSessionEnabledMessagePlugin.kt | 64 +++++++++ .../message/OnboardingEnabledMessagePlugin.kt | 61 +++++++++ .../view/message/PProUpsellBannerPlugin.kt | 100 ++++++++++++++ .../PproUpsellDisabledMessagePlugin.kt | 84 ++++++++++++ .../message/PproUpsellRevokedMessagePlugin.kt | 82 ++++++++++++ .../view/message/RevokedMessagePlugin.kt | 55 ++++++++ .../activity_device_shield_activity.xml | 21 +-- .../res/layout/view_message_info_disabled.xml | 23 ++++ .../res/layout/view_message_info_enabled.xml | 23 ++++ .../src/main/res/values/donottranslate.xml | 6 + ...eviceShieldTrackerActivityViewModelTest.kt | 19 --- ...ActionRequiredDisabledMessagePluginTest.kt | 48 +++++++ .../DisabledBySystemMessagePluginTest.kt | 71 ++++++++++ .../view/message/DisabledMessagePluginTest.kt | 71 ++++++++++ .../view/message/FakeVPNStore.kt | 67 ++++++++++ .../NextSessionEnabledMessagePluginTest.kt | 52 ++++++++ .../OnboardingEnabledMessagePluginTest.kt | 49 +++++++ .../message/PProUpsellBannerPluginTest.kt | 64 +++++++++ .../PproUpsellDisabledMessagePluginTest.kt | 102 ++++++++++++++ .../PproUpsellRevokedMessagePluginTest.kt | 94 +++++++++++++ .../view/message/RevokedMessagePluginTest.kt | 71 ++++++++++ 33 files changed, 1624 insertions(+), 114 deletions(-) create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/ActionRequiredDisabledMessagePlugin.kt create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/AppTPStateMessagePlugin.kt create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/AppTpInfoPanel.kt create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledBySystemMessagePlugin.kt create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledMessagePlugin.kt create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/NextSessionEnabledMessagePlugin.kt create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/OnboardingEnabledMessagePlugin.kt create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPlugin.kt create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePlugin.kt create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePlugin.kt create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/RevokedMessagePlugin.kt create mode 100644 app-tracking-protection/vpn-impl/src/main/res/layout/view_message_info_disabled.xml create mode 100644 app-tracking-protection/vpn-impl/src/main/res/layout/view_message_info_enabled.xml create mode 100644 app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/ActionRequiredDisabledMessagePluginTest.kt create mode 100644 app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledBySystemMessagePluginTest.kt create mode 100644 app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledMessagePluginTest.kt create mode 100644 app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/FakeVPNStore.kt create mode 100644 app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/NextSessionEnabledMessagePluginTest.kt create mode 100644 app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/OnboardingEnabledMessagePluginTest.kt create mode 100644 app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPluginTest.kt create mode 100644 app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePluginTest.kt create mode 100644 app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePluginTest.kt create mode 100644 app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/RevokedMessagePluginTest.kt diff --git a/app-tracking-protection/vpn-impl/build.gradle b/app-tracking-protection/vpn-impl/build.gradle index e7732139f20b..0bcb87b8ec6d 100644 --- a/app-tracking-protection/vpn-impl/build.gradle +++ b/app-tracking-protection/vpn-impl/build.gradle @@ -65,6 +65,7 @@ dependencies { implementation project(':network-protection-api') implementation project(':data-store-api') implementation project(':new-tab-page-api') + implementation project(':subscriptions-api') implementation AndroidX.core.ktx diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNames.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNames.kt index 223488adea8e..b071d578ecec 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNames.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNames.kt @@ -229,5 +229,31 @@ enum class DeviceShieldPixelNames(override val pixelName: String, val enqueue: B NEW_TAB_SECTION_TOGGLED_OFF("m_new_tab_page_customize_section_off_appTP"), NEW_TAB_SECTION_TOGGLED_ON("m_new_tab_page_customize_section_on_appTP"), + + APPTP_PPRO_UPSELL_ENABLED_BANNER_SHOWN("m_atp_ppro-upsell_banner-apptp-enabled_show_c"), + APPTP_PPRO_UPSELL_ENABLED_BANNER_SHOWN_DAILY("m_atp_ppro-upsell_banner-apptp-enabled_show_d"), + APPTP_PPRO_UPSELL_ENABLED_BANNER_SHOWN_UNIQUE("m_atp_ppro-upsell_banner-apptp-enabled_show_u"), + + APPTP_PPRO_UPSELL_ENABLED_BANNER_DISMISSED("m_atp_ppro-upsell_banner-apptp-enabled_dismissed_c"), + + APPTP_PPRO_UPSELL_ENABLED_BANNER_LINK_CLICKED("m_atp_ppro-upsell_banner-apptp-enabled_link_clicked_c"), + APPTP_PPRO_UPSELL_ENABLED_BANNER_LINK_CLICKED_DAILY("m_atp_ppro-upsell_banner-apptp-enabled_link_clicked_d"), + APPTP_PPRO_UPSELL_ENABLED_BANNER_LINK_CLICKED_UNIQUE("m_atp_ppro-upsell_banner-apptp-enabled_link_clicked_u"), + + APPTP_PPRO_UPSELL_DISABLED_INFO_SHOWN("m_atp_ppro-upsell_info-apptp-disabled_show_c"), + APPTP_PPRO_UPSELL_DISABLED_INFO_SHOWN_DAILY("m_atp_ppro-upsell_info-apptp-disabled_show_d"), + APPTP_PPRO_UPSELL_DISABLED_INFO_SHOWN_UNIQUE("m_atp_ppro-upsell_info-apptp-disabled_show_u"), + + APPTP_PPRO_UPSELL_DISABLED_INFO_LINK_CLICKED("m_atp_ppro-upsell_info-apptp-disabled_link_clicked_c"), + APPTP_PPRO_UPSELL_DISABLED_INFO_LINK_CLICKED_DAILY("m_atp_ppro-upsell_info-apptp-disabled_link_clicked_d"), + APPTP_PPRO_UPSELL_DISABLED_INFO_LINK_CLICKED_UNIQUE("m_atp_ppro-upsell_info-apptp-disabled_link_clicked_u"), + + APPTP_PPRO_UPSELL_REVOKED_INFO_SHOWN("m_atp_ppro-upsell_info-apptp-revoked_show_c"), + APPTP_PPRO_UPSELL_REVOKED_INFO_SHOWN_DAILY("m_atp_ppro-upsell_info-apptp-revoked_show_d"), + APPTP_PPRO_UPSELL_REVOKED_INFO_SHOWN_UNIQUE("m_atp_ppro-upsell_info-apptp-revoked_show_u"), + + APPTP_PPRO_UPSELL_REVOKED_INFO_LINK_CLICKED("m_atp_ppro-upsell_info-apptp-revoked_link_clicked_c"), + APPTP_PPRO_UPSELL_REVOKED_INFO_LINK_CLICKED_DAILY("m_atp_ppro-upsell_info-apptp-revoked_link_clicked_d"), + APPTP_PPRO_UPSELL_REVOKED_INFO_LINK_CLICKED_UNIQUE("m_atp_ppro-upsell_info-apptp-revoked_link_clicked_u"), ; } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixels.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixels.kt index 0aaf4bce22ec..09a213d58733 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixels.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixels.kt @@ -366,6 +366,16 @@ interface DeviceShieldPixels { // New Tab Engagement pixels https://app.asana.com/0/72649045549333/1207667088727866/f fun reportNewTabSectionToggled(enabled: Boolean) + + fun reportPproUpsellBannerShown() + fun reportPproUpsellBannerDismissed() + fun reportPproUpsellBannerLinkClicked() + + fun reportPproUpsellDisabledInfoShown() + fun reportPproUpsellDisabledInfoLinkClicked() + + fun reportPproUpsellRevokedInfoShown() + fun reportPproUpsellRevokedInfoLinkClicked() } @ContributesBinding(AppScope::class) @@ -838,6 +848,46 @@ class RealDeviceShieldPixels @Inject constructor( } } + override fun reportPproUpsellBannerShown() { + tryToFireUniquePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_ENABLED_BANNER_SHOWN_UNIQUE) + tryToFireDailyPixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_ENABLED_BANNER_SHOWN_DAILY) + firePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_ENABLED_BANNER_SHOWN) + } + + override fun reportPproUpsellBannerLinkClicked() { + tryToFireUniquePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_ENABLED_BANNER_LINK_CLICKED_UNIQUE) + tryToFireDailyPixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_ENABLED_BANNER_LINK_CLICKED_DAILY) + firePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_ENABLED_BANNER_LINK_CLICKED) + } + + override fun reportPproUpsellBannerDismissed() { + firePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_ENABLED_BANNER_DISMISSED) + } + + override fun reportPproUpsellDisabledInfoShown() { + tryToFireUniquePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_DISABLED_INFO_SHOWN_UNIQUE) + tryToFireDailyPixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_DISABLED_INFO_SHOWN_DAILY) + firePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_DISABLED_INFO_SHOWN) + } + + override fun reportPproUpsellDisabledInfoLinkClicked() { + tryToFireUniquePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_DISABLED_INFO_LINK_CLICKED_UNIQUE) + tryToFireDailyPixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_DISABLED_INFO_LINK_CLICKED_DAILY) + firePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_DISABLED_INFO_LINK_CLICKED) + } + + override fun reportPproUpsellRevokedInfoShown() { + tryToFireUniquePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_REVOKED_INFO_SHOWN_UNIQUE) + tryToFireDailyPixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_REVOKED_INFO_SHOWN_DAILY) + firePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_REVOKED_INFO_SHOWN) + } + + override fun reportPproUpsellRevokedInfoLinkClicked() { + tryToFireUniquePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_REVOKED_INFO_LINK_CLICKED_UNIQUE) + tryToFireDailyPixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_REVOKED_INFO_LINK_CLICKED_DAILY) + firePixel(DeviceShieldPixelNames.APPTP_PPRO_UPSELL_REVOKED_INFO_LINK_CLICKED) + } + private fun firePixel( p: DeviceShieldPixelNames, payload: Map = emptyMap(), diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/VpnPixelParamRemovalPlugin.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/VpnPixelParamRemovalPlugin.kt index 494a49bdd906..51c8c16fc186 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/VpnPixelParamRemovalPlugin.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/VpnPixelParamRemovalPlugin.kt @@ -31,6 +31,8 @@ class VpnPixelParamRemovalPlugin @Inject constructor() : PixelParamRemovalPlugin VPN_PIXEL_PREFIX to PixelParameter.removeAtb(), "m_atp_unprotected_apps_bucket_" to PixelParameter.removeAll(), "m_vpn_ev_moto_g_fix_" to PixelParameter.removeAll(), + ATP_PPRO_UPSELL_PREFIX to PixelParameter.removeOSVersion(), + ATP_PPRO_UPSELL_PREFIX to PixelParameter.removeAtb(), ) } @@ -38,5 +40,6 @@ class VpnPixelParamRemovalPlugin @Inject constructor() : PixelParamRemovalPlugin private const val ATP_PIXEL_PREFIX = "m_atp_" private const val NETP_PIXEL_PREFIX = "m_netp_" private const val VPN_PIXEL_PREFIX = "m_vpn_" + private const val ATP_PPRO_UPSELL_PREFIX = "m_atp_ppro-upsell" } } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/onboarding/VpnStore.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/onboarding/VpnStore.kt index 632ca45dfe8f..e27daf6ce9eb 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/onboarding/VpnStore.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/onboarding/VpnStore.kt @@ -37,6 +37,9 @@ interface VpnStore { fun dismissNotifyMeInAppTp() fun isNotifyMeInAppTpDismissed(): Boolean + + fun dismissPproUpsellBanner() + fun isPproUpsellBannerDismised(): Boolean } @ContributesBinding(AppScope::class) @@ -96,6 +99,14 @@ class SharedPreferencesVpnStore @Inject constructor( return preferences.getBoolean(KEY_NOTIFY_ME_IN_APP_TP_DISMISSED, false) } + override fun dismissPproUpsellBanner() { + preferences.edit { putBoolean(KEY_PPRO_UPSELL_BANNER_DISMISSED, true) } + } + + override fun isPproUpsellBannerDismised(): Boolean { + return preferences.getBoolean(KEY_PPRO_UPSELL_BANNER_DISMISSED, false) + } + companion object { private const val DEVICE_SHIELD_ONBOARDING_STORE_PREFS = "com.duckduckgo.android.atp.onboarding.store" @@ -103,6 +114,7 @@ class SharedPreferencesVpnStore @Inject constructor( private const val KEY_APP_TP_ONBOARDING_VPN_ENABLED_CTA_SHOWN = "KEY_APP_TP_ONBOARDING_VPN_ENABLED_CTA_SHOWN" private const val KEY_APP_TP_ONBOARDING_BANNER_EXPIRY_TIMESTAMP = "KEY_APP_TP_ONBOARDING_BANNER_EXPIRY_TIMESTAMP" private const val KEY_NOTIFY_ME_IN_APP_TP_DISMISSED = "KEY_NOTIFY_ME_IN_APP_TP_DISMISSED" + private const val KEY_PPRO_UPSELL_BANNER_DISMISSED = "KEY_PPRO_UPSELL_BANNER_DISMISSED" private const val WINDOW_INTERVAL_HOURS = 24L } } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivity.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivity.kt index 1620e44ceaa3..53ecc2ab5d3e 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivity.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivity.kt @@ -23,9 +23,11 @@ import android.net.VpnService import android.os.Bundle import android.os.ResultReceiver import android.view.Menu +import android.view.View import android.widget.CompoundButton import androidx.activity.result.ActivityResultLauncher import androidx.core.content.res.ResourcesCompat +import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope @@ -34,10 +36,9 @@ import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams import com.duckduckgo.common.ui.DuckDuckGoActivity +import com.duckduckgo.common.ui.notifyme.NotifyMeView import com.duckduckgo.common.ui.view.DaxDialogListener import com.duckduckgo.common.ui.view.DaxSwitch -import com.duckduckgo.common.ui.view.InfoPanel.Companion.APPTP_SETTINGS_ANNOTATION -import com.duckduckgo.common.ui.view.InfoPanel.Companion.REPORT_ISSUES_ANNOTATION import com.duckduckgo.common.ui.view.TypewriterDaxDialog import com.duckduckgo.common.ui.view.dialog.StackedAlertDialogBuilder import com.duckduckgo.common.ui.view.dialog.TextAlertDialogBuilder @@ -47,6 +48,8 @@ import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.extensions.launchAlwaysOnSystemSettings +import com.duckduckgo.common.utils.plugins.ActivePlugin +import com.duckduckgo.common.utils.plugins.ActivePluginPoint import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerActivityWithEmptyParams import com.duckduckgo.mobile.android.vpn.AppTpVpnFeature @@ -61,15 +64,14 @@ import com.duckduckgo.mobile.android.vpn.di.AppTpBreakageCategories import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState -import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.REVOKED -import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.SELF_STOP import com.duckduckgo.mobile.android.vpn.ui.AppBreakageCategory import com.duckduckgo.mobile.android.vpn.ui.alwayson.AlwaysOnAlertDialogFragment import com.duckduckgo.mobile.android.vpn.ui.report.DeviceShieldAppTrackersInfo -import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.DeviceShieldTrackerActivityViewModel.BannerState import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.DeviceShieldTrackerActivityViewModel.ViewEvent import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.DeviceShieldTrackerActivityViewModel.ViewEvent.StartVpn import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.DisableVpnDialogOptions +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction import com.duckduckgo.navigation.api.GlobalActivityStarter import com.google.android.material.snackbar.Snackbar import java.util.concurrent.TimeUnit @@ -111,6 +113,9 @@ class DeviceShieldTrackerActivity : @Inject lateinit var globalActivityStarter: GlobalActivityStarter + @Inject + lateinit var appTPStateMessagePluginPoint: ActivePluginPoint + private val binding: ActivityDeviceShieldActivityBinding by viewBinding() private lateinit var deviceShieldSwitch: DaxSwitch @@ -133,6 +138,16 @@ class DeviceShieldTrackerActivity : viewModel.onAppTPToggleSwitched(isChecked) } + private val onInfoMessageClick = fun(action: DefaultAppTPMessageAction) { + when (action) { + DefaultAppTPMessageAction.ReenableAppTP -> reEnableAppTrackingProtection() + DefaultAppTPMessageAction.LaunchFeedback -> launchFeedback() + DefaultAppTPMessageAction.HandleAlwaysOnActionRequired -> launchAlwaysOnLockdownEnabledDialog() + } + } + + private var currenActivePlugin: ActivePlugin? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -176,6 +191,21 @@ class DeviceShieldTrackerActivity : binding.ctaShowAll.setOnClickListener { viewModel.onViewEvent(ViewEvent.LaunchMostRecentActivity) } + + binding.deviceShieldTrackerNotifyMe.setOnVisibilityChange( + object : NotifyMeView.OnVisibilityChangedListener { + override fun onVisibilityChange( + v: View?, + isVisible: Boolean, + ) { + if (isVisible) { + binding.deviceShieldTrackerMessageContainer.gone() + } else { + binding.deviceShieldTrackerMessageContainer.show() + } + } + }, + ) } override fun onActivityResult( @@ -215,7 +245,7 @@ class DeviceShieldTrackerActivity : DeviceShieldTrackerActivityViewModel.TrackerCountInfo(trackers, apps) } .combine(viewModel.getRunningState()) { trackerCountInfo, runningState -> - DeviceShieldTrackerActivityViewModel.TrackerActivityViewState(trackerCountInfo, runningState, viewModel.bannerState()) + DeviceShieldTrackerActivityViewModel.TrackerActivityViewState(trackerCountInfo, runningState) } .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .collect { renderViewState(it) } @@ -506,7 +536,7 @@ class DeviceShieldTrackerActivity : deviceShieldSwitch.quietlySetIsChecked(state, enableAppTPSwitchListener) } - private fun renderViewState(state: DeviceShieldTrackerActivityViewModel.TrackerActivityViewState) { + private suspend fun renderViewState(state: DeviceShieldTrackerActivityViewModel.TrackerActivityViewState) { vpnCachedState = state.runningState if (::deviceShieldSwitch.isInitialized) { quietlyToggleAppTpSwitch(state.runningState.state == VpnRunningState.ENABLED) @@ -515,7 +545,7 @@ class DeviceShieldTrackerActivity : } updateCounts(state.trackerCountInfo) - updateRunningState(state.runningState, state.bannerState) + updateRunningState(state.runningState) } private fun updateCounts(trackerCountInfo: DeviceShieldTrackerActivityViewModel.TrackerCountInfo) { @@ -528,68 +558,37 @@ class DeviceShieldTrackerActivity : resources.getQuantityString(R.plurals.atp_ActivityPastWeekAppCount, trackerCountInfo.apps.value) } - private fun updateRunningState( - runningState: VpnState, - bannerState: BannerState, - ) { + private suspend fun updateRunningState(runningState: VpnState) { + if (!binding.deviceShieldTrackerNotifyMe.isVisible) { + var newActivePlugin: ActivePlugin? = null + appTPStateMessagePluginPoint.getPlugins().firstNotNullOfOrNull { + it.getView(this, runningState, onInfoMessageClick)?.apply { + newActivePlugin = it + } + }?.let { + if (currenActivePlugin == null || newActivePlugin != currenActivePlugin) { + currenActivePlugin = newActivePlugin + binding.deviceShieldTrackerMessageContainer.show() + binding.deviceShieldTrackerMessageContainer.removeAllViews() + binding.deviceShieldTrackerMessageContainer.addView(it) + } + } ?: { + currenActivePlugin = null + binding.deviceShieldTrackerMessageContainer.gone() + } + } else { + currenActivePlugin = null + binding.deviceShieldTrackerMessageContainer.gone() + } + if (runningState.state == VpnRunningState.ENABLED) { binding.deviceShieldTrackerBlockingTrackersDescription.text = resources.getString(R.string.atp_ActivityBlockingTrackersEnabledDescription) binding.deviceShieldTrackerShieldImage.setImageResource(R.drawable.apptp_shield_enabled) - if (runningState.alwaysOnState.isAlwaysOnLockedDown()) { - binding.deviceShieldTrackerLabelEnabled.gone() - - binding.deviceShieldTrackerLabelDisabled.apply { - setClickableLink( - OPEN_SETTINGS_ANNOTATION, - getText(R.string.atp_AlwaysOnLockDownEnabled), - ) { launchAlwaysOnLockdownEnabledDialog() } - show() - } - } else { - binding.deviceShieldTrackerLabelDisabled.gone() - - binding.deviceShieldTrackerLabelEnabled.apply { - if (bannerState is BannerState.OnboardingBanner) { - setClickableLink( - APPTP_SETTINGS_ANNOTATION, - getText(R.string.atp_ActivityEnabledBannerLabel), - ) { launchTrackingProtectionExclusionListActivity() } - } else { - setClickableLink( - APPTP_SETTINGS_ANNOTATION, - getText(R.string.atp_ActivityEnabledMoreThanADayLabel), - ) { launchManageAppsProtection() } - } - show() - } - } } else { binding.deviceShieldTrackerBlockingTrackersDescription.text = resources.getString(R.string.atp_ActivityBlockingTrackersDisabledDescription) binding.deviceShieldTrackerShieldImage.setImageResource(R.drawable.apptp_shield_disabled) - binding.deviceShieldTrackerLabelEnabled.gone() - - val (disabledLabel, annotation) = if (runningState.stopReason == REVOKED) { - R.string.atp_ActivityRevokedLabel to REPORT_ISSUES_ANNOTATION - } else if (runningState.stopReason is SELF_STOP) { - R.string.atp_ActivityDisabledLabel to REPORT_ISSUES_ANNOTATION - } else { - R.string.atp_ActivityDisabledBySystemLabel to RE_ENABLE_ANNOTATION - } - binding.deviceShieldTrackerLabelDisabled.apply { - setClickableLink( - annotation, - getText(disabledLabel), - ) { - if (annotation == REPORT_ISSUES_ANNOTATION) { - launchFeedback() - } else if (annotation == RE_ENABLE_ANNOTATION) { - reEnableAppTrackingProtection() - } - } - show() - } } } @@ -688,8 +687,6 @@ class DeviceShieldTrackerActivity : companion object { private const val RESULT_RECEIVER_EXTRA = "RESULT_RECEIVER_EXTRA" - private const val RE_ENABLE_ANNOTATION = "re_enable_link" - private const val OPEN_SETTINGS_ANNOTATION = "open_settings_link" private const val ON_LAUNCHED_CALLED_SUCCESS = 0 private const val MIN_ROWS_FOR_ALL_ACTIVITY = 5 private const val TAG_APPTP_PROMOTE_ALWAYS_ON_DIALOG = "AppTPPromoteAlwaysOnDialog" diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivityViewModel.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivityViewModel.kt index eb85ff48f006..ded68a60a70f 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivityViewModel.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivityViewModel.kt @@ -193,23 +193,9 @@ class DeviceShieldTrackerActivityViewModel @Inject constructor( } } - fun bannerState(): BannerState { - return if (vpnStore.getAndSetOnboardingSession()) { - BannerState.OnboardingBanner - } else { - BannerState.NextSessionBanner - } - } - - sealed class BannerState { - object OnboardingBanner : BannerState() - object NextSessionBanner : BannerState() - } - internal data class TrackerActivityViewState( val trackerCountInfo: TrackerCountInfo, val runningState: VpnState, - val bannerState: BannerState, ) internal data class TrackerCountInfo( diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/ActionRequiredDisabledMessagePlugin.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/ActionRequiredDisabledMessagePlugin.kt new file mode 100644 index 000000000000..24ab22fea3b5 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/ActionRequiredDisabledMessagePlugin.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 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 android.content.Context +import android.view.View +import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.mobile.android.vpn.R +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.ENABLED +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.Companion.PRIORITY_ACTION_REQUIRED +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction.HandleAlwaysOnActionRequired +import javax.inject.Inject + +@ContributesActivePlugin( + scope = AppScope::class, + boundType = AppTPStateMessagePlugin::class, + priority = PRIORITY_ACTION_REQUIRED, +) +class ActionRequiredDisabledMessagePlugin @Inject constructor() : AppTPStateMessagePlugin { + override fun getView( + context: Context, + vpnState: VpnState, + clickListener: (DefaultAppTPMessageAction) -> Unit, + ): View? { + return if (vpnState.state == ENABLED && vpnState.alwaysOnState.isAlwaysOnLockedDown()) { + AppTpDisabledInfoPanel(context).apply { + setClickableLink( + OPEN_SETTINGS_ANNOTATION, + context.getText(R.string.atp_AlwaysOnLockDownEnabled), + ) { clickListener.invoke(HandleAlwaysOnActionRequired) } + } + } else { + null + } + } + + companion object { + private const val OPEN_SETTINGS_ANNOTATION = "open_settings_link" + } +} diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/AppTPStateMessagePlugin.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/AppTPStateMessagePlugin.kt new file mode 100644 index 000000000000..507e178a0990 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/AppTPStateMessagePlugin.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 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 android.content.Context +import android.view.View +import com.duckduckgo.anvil.annotations.ContributesActivePluginPoint +import com.duckduckgo.common.utils.plugins.ActivePlugin +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState + +interface AppTPStateMessagePlugin : ActivePlugin { + fun getView( + context: Context, + vpnState: VpnState, + clickListener: (DefaultAppTPMessageAction) -> Unit, + ): View? + + sealed class DefaultAppTPMessageAction { + data object HandleAlwaysOnActionRequired : DefaultAppTPMessageAction() + data object ReenableAppTP : DefaultAppTPMessageAction() + data object LaunchFeedback : DefaultAppTPMessageAction() + } + + companion object { + internal const val PRIORITY_ACTION_REQUIRED = 100 + internal const val PRIORITY_ONBOARDING = 110 + internal const val PRIORITY_NEXT_SESSION = 120 + internal const val PRIORITY_REVOKED = 200 + internal const val PRIORITY_DISABLED = 210 + internal const val PRIORITY_DISABLED_BY_SYSTEM = 220 + } +} + +@ContributesActivePluginPoint( + scope = AppScope::class, + boundType = AppTPStateMessagePlugin::class, +) +private interface AppTPStateMessagePluginPoint diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/AppTpInfoPanel.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/AppTpInfoPanel.kt new file mode 100644 index 000000000000..1a372850355f --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/AppTpInfoPanel.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 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 android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.mobile.android.vpn.databinding.ViewMessageInfoDisabledBinding +import com.duckduckgo.mobile.android.vpn.databinding.ViewMessageInfoEnabledBinding + +class AppTpEnabledInfoPanel @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr) { + + private val binding: ViewMessageInfoEnabledBinding by viewBinding() + + fun setClickableLink( + annotation: String, + fullText: CharSequence, + onClick: () -> Unit, + ) { + binding.root.setClickableLink(annotation, fullText, onClick) + } +} + +class AppTpDisabledInfoPanel @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr) { + + private val binding: ViewMessageInfoDisabledBinding by viewBinding() + + fun setClickableLink( + annotation: String, + fullText: CharSequence, + onClick: () -> Unit, + ) { + binding.root.setClickableLink(annotation, fullText, onClick) + } +} diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledBySystemMessagePlugin.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledBySystemMessagePlugin.kt new file mode 100644 index 000000000000..9623c7fc8f1c --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledBySystemMessagePlugin.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 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 android.content.Context +import android.view.View +import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.mobile.android.vpn.R +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.DISABLED +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.Companion.PRIORITY_DISABLED_BY_SYSTEM +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction.ReenableAppTP +import javax.inject.Inject + +@ContributesActivePlugin( + scope = AppScope::class, + boundType = AppTPStateMessagePlugin::class, + priority = PRIORITY_DISABLED_BY_SYSTEM, +) +class DisabledBySystemMessagePlugin @Inject constructor() : AppTPStateMessagePlugin { + override fun getView( + context: Context, + vpnState: VpnState, + clickListener: (DefaultAppTPMessageAction) -> Unit, + ): View? { + return if (vpnState.state == DISABLED) { + AppTpDisabledInfoPanel(context).apply { + setClickableLink( + RE_ENABLE_ANNOTATION, + context.getText(R.string.atp_ActivityDisabledBySystemLabel), + ) { clickListener.invoke(ReenableAppTP) } + } + } else { + null + } + } + + companion object { + private const val RE_ENABLE_ANNOTATION = "re_enable_link" + } +} diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledMessagePlugin.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledMessagePlugin.kt new file mode 100644 index 000000000000..023439deea77 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledMessagePlugin.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 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 android.content.Context +import android.view.View +import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.common.ui.view.InfoPanel.Companion.REPORT_ISSUES_ANNOTATION +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.mobile.android.vpn.R +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.DISABLED +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.SELF_STOP +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.Companion.PRIORITY_DISABLED +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction.LaunchFeedback +import javax.inject.Inject + +@ContributesActivePlugin( + scope = AppScope::class, + boundType = AppTPStateMessagePlugin::class, + priority = PRIORITY_DISABLED, +) +class DisabledMessagePlugin @Inject constructor() : AppTPStateMessagePlugin { + override fun getView( + context: Context, + vpnState: VpnState, + clickListener: (DefaultAppTPMessageAction) -> Unit, + ): View? { + return if (vpnState.state == DISABLED && vpnState.stopReason is SELF_STOP) { + AppTpDisabledInfoPanel(context).apply { + setClickableLink( + REPORT_ISSUES_ANNOTATION, + context.getText(R.string.atp_ActivityDisabledLabel), + ) { clickListener.invoke(LaunchFeedback) } + } + } else { + null + } + } +} diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/NextSessionEnabledMessagePlugin.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/NextSessionEnabledMessagePlugin.kt new file mode 100644 index 000000000000..b27054b335a9 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/NextSessionEnabledMessagePlugin.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 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 android.content.Context +import android.view.View +import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.common.ui.view.InfoPanel.Companion.APPTP_SETTINGS_ANNOTATION +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.mobile.android.vpn.R +import com.duckduckgo.mobile.android.vpn.apps.ui.ManageRecentAppsProtectionActivity +import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.ENABLED +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.ui.onboarding.VpnStore +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.Companion.PRIORITY_NEXT_SESSION +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction +import javax.inject.Inject + +@ContributesActivePlugin( + scope = AppScope::class, + boundType = AppTPStateMessagePlugin::class, + priority = PRIORITY_NEXT_SESSION, +) +class NextSessionEnabledMessagePlugin @Inject constructor( + private val vpnStore: VpnStore, + private val deviceShieldPixels: DeviceShieldPixels, +) : AppTPStateMessagePlugin { + override fun getView( + context: Context, + vpnState: VpnState, + clickListener: (DefaultAppTPMessageAction) -> Unit, + ): View? { + return if (vpnState.state == ENABLED && !vpnStore.getAndSetOnboardingSession()) { + AppTpEnabledInfoPanel(context).apply { + setClickableLink( + APPTP_SETTINGS_ANNOTATION, + context.getText(R.string.atp_ActivityEnabledMoreThanADayLabel), + ) { context.launchManageAppsProtection() } + } + } else { + null + } + } + + private fun Context.launchManageAppsProtection() { + deviceShieldPixels.didOpenManageRecentAppSettings() + startActivity(ManageRecentAppsProtectionActivity.intent(this)) + } +} diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/OnboardingEnabledMessagePlugin.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/OnboardingEnabledMessagePlugin.kt new file mode 100644 index 000000000000..29d5c5601cb4 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/OnboardingEnabledMessagePlugin.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 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 android.content.Context +import android.view.View +import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.common.ui.view.InfoPanel.Companion.APPTP_SETTINGS_ANNOTATION +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.mobile.android.vpn.R +import com.duckduckgo.mobile.android.vpn.apps.ui.TrackingProtectionExclusionListActivity +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.ENABLED +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.ui.onboarding.VpnStore +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.Companion.PRIORITY_ONBOARDING +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction +import javax.inject.Inject + +@ContributesActivePlugin( + scope = AppScope::class, + boundType = AppTPStateMessagePlugin::class, + priority = PRIORITY_ONBOARDING, +) +class OnboardingEnabledMessagePlugin @Inject constructor( + private val vpnStore: VpnStore, +) : AppTPStateMessagePlugin { + override fun getView( + context: Context, + vpnState: VpnState, + clickListener: (DefaultAppTPMessageAction) -> Unit, + ): View? { + return if (vpnState.state == ENABLED && vpnStore.getAndSetOnboardingSession()) { + AppTpEnabledInfoPanel(context).apply { + setClickableLink( + APPTP_SETTINGS_ANNOTATION, + context.getText(R.string.atp_ActivityEnabledBannerLabel), + ) { context.launchTrackingProtectionExclusionListActivity() } + } + } else { + null + } + } + + private fun Context.launchTrackingProtectionExclusionListActivity() { + startActivity(TrackingProtectionExclusionListActivity.intent(this)) + } +} 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 new file mode 100644 index 000000000000..9f1c388f542b --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPlugin.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2024 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 android.content.Context +import android.view.View +import androidx.core.view.doOnAttach +import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.app.tabs.BrowserNav +import com.duckduckgo.common.ui.view.MessageCta +import com.duckduckgo.common.ui.view.MessageCta.Message +import com.duckduckgo.common.ui.view.MessageCta.MessageType.REMOTE_MESSAGE +import com.duckduckgo.common.ui.view.gone +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.mobile.android.vpn.R +import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.ui.onboarding.VpnStore +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.Companion.PRIORITY_ACTION_REQUIRED +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.PProUpsellBannerPlugin.Companion.PRIORITY_PPRO_UPSELL_BANNER +import com.duckduckgo.subscriptions.api.Subscriptions +import javax.inject.Inject +import kotlinx.coroutines.runBlocking + +@ContributesActivePlugin( + scope = AppScope::class, + boundType = AppTPStateMessagePlugin::class, + priority = PRIORITY_PPRO_UPSELL_BANNER, +) +class PProUpsellBannerPlugin @Inject constructor( + private val subscriptions: Subscriptions, + private val browserNav: BrowserNav, + private val vpnStore: VpnStore, + private val deviceShieldPixels: DeviceShieldPixels, +) : AppTPStateMessagePlugin { + override fun getView( + context: Context, + vpnState: VpnState, + clickListener: (DefaultAppTPMessageAction) -> Unit, + ): View? { + val isEligible = runBlocking { subscriptions.isUpsellEligible() && !vpnStore.isPproUpsellBannerDismised() } + return if (isEligible) { + 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), + messageType = REMOTE_MESSAGE, + ), + ) + this.onCloseButtonClicked { + deviceShieldPixels.reportPproUpsellBannerDismissed() + vpnStore.dismissPproUpsellBanner() + this.gone() + } + + this.onPrimaryActionClicked { + deviceShieldPixels.reportPproUpsellBannerLinkClicked() + context.launchPPro() + } + this.doOnAttach { + deviceShieldPixels.reportPproUpsellBannerShown() + } + } + } else { + null + } + } + + private fun Context.launchPPro() { + startActivity(browserNav.openInNewTab(this, PPRO_UPSELL_URL)) + } + + private suspend fun Subscriptions.isUpsellEligible(): Boolean { + return getAccessToken() == null && isEligible() + } + + companion object { + internal const val PRIORITY_PPRO_UPSELL_BANNER = PRIORITY_ACTION_REQUIRED - 1 + private const val PPRO_UPSELL_URL = "https://duckduckgo.com/pro?origin=funnel_pro_android_apptp_banner" + } +} 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 new file mode 100644 index 000000000000..3e51d50a7a4f --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePlugin.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024 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 android.content.Context +import android.view.View +import androidx.core.view.doOnAttach +import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.app.tabs.BrowserNav +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.mobile.android.vpn.R +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 +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.SELF_STOP +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.Companion.PRIORITY_DISABLED +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.PproUpsellDisabledMessagePlugin.Companion.PRIORITY_PPRO_DISABLED +import com.duckduckgo.subscriptions.api.Subscriptions +import javax.inject.Inject +import kotlinx.coroutines.runBlocking + +@ContributesActivePlugin( + scope = AppScope::class, + boundType = AppTPStateMessagePlugin::class, + priority = PRIORITY_PPRO_DISABLED, +) +class PproUpsellDisabledMessagePlugin @Inject constructor( + private val subscriptions: Subscriptions, + private val vpnDetector: ExternalVpnDetector, + private val browserNav: BrowserNav, + private val deviceShieldPixels: DeviceShieldPixels, +) : AppTPStateMessagePlugin { + override fun getView( + context: Context, + vpnState: VpnState, + clickListener: (DefaultAppTPMessageAction) -> Unit, + ): View? { + val isEligible = runBlocking { vpnDetector.isExternalVpnDetected() && subscriptions.isUpsellEligible() } + return if (vpnState.state == DISABLED && vpnState.stopReason is SELF_STOP && isEligible) { + AppTpDisabledInfoPanel(context).apply { + setClickableLink( + PPRO_UPSELL_ANNOTATION, + context.getText(R.string.apptp_PproUpsellInfoDisabled), + ) { context.launchPPro() } + doOnAttach { + deviceShieldPixels.reportPproUpsellDisabledInfoShown() + } + } + } else { + null + } + } + + private fun Context.launchPPro() { + deviceShieldPixels.reportPproUpsellDisabledInfoLinkClicked() + startActivity(browserNav.openInNewTab(this, PPRO_UPSELL_URL)) + } + + private suspend fun Subscriptions.isUpsellEligible(): Boolean { + return getAccessToken() == null && isEligible() + } + + companion object { + internal const val PRIORITY_PPRO_DISABLED = PRIORITY_DISABLED - 1 + private const val PPRO_UPSELL_ANNOTATION = "ppro_upsell_link" + private const val PPRO_UPSELL_URL = "https://duckduckgo.com/pro?origin=funnel_pro_android_apptp_disabled" + } +} 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 new file mode 100644 index 000000000000..c5d197166f17 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePlugin.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 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 android.content.Context +import android.view.View +import androidx.core.view.doOnAttach +import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.app.tabs.BrowserNav +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.mobile.android.vpn.R +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.VpnState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.REVOKED +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.Companion.PRIORITY_REVOKED +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.PproUpsellRevokedMessagePlugin.Companion.PRIORITY_PPRO_REVOKED +import com.duckduckgo.subscriptions.api.Subscriptions +import javax.inject.Inject +import kotlinx.coroutines.runBlocking + +@ContributesActivePlugin( + scope = AppScope::class, + boundType = AppTPStateMessagePlugin::class, + priority = PRIORITY_PPRO_REVOKED, +) +class PproUpsellRevokedMessagePlugin @Inject constructor( + private val subscriptions: Subscriptions, + private val browserNav: BrowserNav, + private val deviceShieldPixels: DeviceShieldPixels, +) : AppTPStateMessagePlugin { + override fun getView( + context: Context, + vpnState: VpnState, + clickListener: (DefaultAppTPMessageAction) -> Unit, + ): View? { + val isEligible = runBlocking { subscriptions.isUpsellEligible() } + return if (vpnState.state == DISABLED && vpnState.stopReason == REVOKED && isEligible) { + AppTpDisabledInfoPanel(context).apply { + setClickableLink( + PPRO_UPSELL_ANNOTATION, + context.getText(R.string.apptp_PproUpsellInfoRevoked), + ) { context.launchPPro() } + doOnAttach { + deviceShieldPixels.reportPproUpsellRevokedInfoShown() + } + } + } else { + null + } + } + + private fun Context.launchPPro() { + deviceShieldPixels.reportPproUpsellRevokedInfoLinkClicked() + startActivity(browserNav.openInNewTab(this, PPRO_UPSELL_URL)) + } + + private suspend fun Subscriptions.isUpsellEligible(): Boolean { + return getAccessToken() == null && isEligible() + } + + companion object { + internal const val PRIORITY_PPRO_REVOKED = PRIORITY_REVOKED - 1 + private const val PPRO_UPSELL_ANNOTATION = "ppro_upsell_link" + private const val PPRO_UPSELL_URL = "https://duckduckgo.com/pro?origin=funnel_pro_android_apptp_revoked" + } +} diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/RevokedMessagePlugin.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/RevokedMessagePlugin.kt new file mode 100644 index 000000000000..a110e0501013 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/RevokedMessagePlugin.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 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 android.content.Context +import android.view.View +import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.common.ui.view.InfoPanel.Companion.REPORT_ISSUES_ANNOTATION +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.mobile.android.vpn.R +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.DISABLED +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.REVOKED +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.Companion.PRIORITY_REVOKED +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction +import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message.AppTPStateMessagePlugin.DefaultAppTPMessageAction.LaunchFeedback +import javax.inject.Inject + +@ContributesActivePlugin( + scope = AppScope::class, + boundType = AppTPStateMessagePlugin::class, + priority = PRIORITY_REVOKED, +) +class RevokedMessagePlugin @Inject constructor() : AppTPStateMessagePlugin { + override fun getView( + context: Context, + vpnState: VpnState, + clickListener: (DefaultAppTPMessageAction) -> Unit, + ): View? { + return if (vpnState.state == DISABLED && vpnState.stopReason == REVOKED) { + AppTpDisabledInfoPanel(context).apply { + setClickableLink( + REPORT_ISSUES_ANNOTATION, + context.getText(R.string.atp_ActivityRevokedLabel), + ) { clickListener.invoke(LaunchFeedback) } + } + } else { + null + } + } +} diff --git a/app-tracking-protection/vpn-impl/src/main/res/layout/activity_device_shield_activity.xml b/app-tracking-protection/vpn-impl/src/main/res/layout/activity_device_shield_activity.xml index d9ba85c928eb..745f23b617ac 100644 --- a/app-tracking-protection/vpn-impl/src/main/res/layout/activity_device_shield_activity.xml +++ b/app-tracking-protection/vpn-impl/src/main/res/layout/activity_device_shield_activity.xml @@ -50,23 +50,10 @@ app:secondaryText="@string/appTrackingProtectionNotifyMeSubtitle" app:sharedPrefsKeyForDismiss="key_component_dismissed_in_apptp" /> - - - + + + \ No newline at end of file diff --git a/app-tracking-protection/vpn-impl/src/main/res/layout/view_message_info_enabled.xml b/app-tracking-protection/vpn-impl/src/main/res/layout/view_message_info_enabled.xml new file mode 100644 index 000000000000..c1de7186f686 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/res/layout/view_message_info_enabled.xml @@ -0,0 +1,23 @@ + + + diff --git a/app-tracking-protection/vpn-impl/src/main/res/values/donottranslate.xml b/app-tracking-protection/vpn-impl/src/main/res/values/donottranslate.xml index 903b101ce06d..57a267b954cc 100644 --- a/app-tracking-protection/vpn-impl/src/main/res/values/donottranslate.xml +++ b/app-tracking-protection/vpn-impl/src/main/res/values/donottranslate.xml @@ -40,5 +40,11 @@ App Tracking Protection Status + + Unable to use App Tracking Protection because of your VPN? Switch to DuckDuckGo VPN — the only VPN that\’s compatible. Learn more. + App Tracking Protection Disabled\nDid your VPN disable App Tracking Protection? Use the toggle above to re-enable it.\n\nGet DuckDuckGo VPN — the only VPN that works with App Tracking Protection. Learn more. + Take Your Privacy to the Next Level + DuckDuckGo has the only VPN that\’s compatible with App Tracking Protection. Activate with a paid Privacy Pro subscription. + Learn More diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivityViewModelTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivityViewModelTest.kt index 7335d5eea355..8d4cf87e5a17 100644 --- a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivityViewModelTest.kt +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivityViewModelTest.kt @@ -28,7 +28,6 @@ import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor import com.duckduckgo.mobile.android.vpn.stats.AppTrackerBlockingStatsRepository import com.duckduckgo.mobile.android.vpn.ui.onboarding.VpnStore -import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.DeviceShieldTrackerActivityViewModel.BannerState import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.DeviceShieldTrackerActivityViewModel.ViewEvent import kotlin.time.ExperimentalTime import kotlinx.coroutines.delay @@ -320,22 +319,4 @@ class DeviceShieldTrackerActivityViewModelTest { cancelAndIgnoreRemainingEvents() } } - - @Test - fun whenBannerStateCalledOutsideOnboardingSessionThenReturnNextSessionBanner() { - whenever(vpnStore.getAndSetOnboardingSession()).thenReturn(false) - - val bannerState = viewModel.bannerState() - - assertEquals(BannerState.NextSessionBanner, bannerState) - } - - @Test - fun whenBannerStateCalledDuringOnboardingSessionThenReturnOnboardingBanner() { - whenever(vpnStore.getAndSetOnboardingSession()).thenReturn(true) - - val bannerState = viewModel.bannerState() - - assertEquals(BannerState.OnboardingBanner, bannerState) - } } diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/ActionRequiredDisabledMessagePluginTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/ActionRequiredDisabledMessagePluginTest.kt new file mode 100644 index 000000000000..7570ac43092e --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/ActionRequiredDisabledMessagePluginTest.kt @@ -0,0 +1,48 @@ +package com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.AlwaysOnState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ActionRequiredDisabledMessagePluginTest { + private lateinit var plugin: ActionRequiredDisabledMessagePlugin + private val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + plugin = ActionRequiredDisabledMessagePlugin() + } + + @Test + fun whenVPNIsDisabledThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, alwaysOnState = AlwaysOnState.ALWAYS_ON_LOCKED_DOWN)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnablingThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLING, alwaysOnState = AlwaysOnState.ALWAYS_ON_LOCKED_DOWN)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnabledAndAlwaysOnOnlyEnabledThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLED, alwaysOnState = AlwaysOnState.ALWAYS_ON_ENABLED)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnabledAndLockdownEnabledThenGetViewReturnsView() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLED, alwaysOnState = AlwaysOnState.ALWAYS_ON_LOCKED_DOWN)) {} + assertNotNull(result) + } +} diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledBySystemMessagePluginTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledBySystemMessagePluginTest.kt new file mode 100644 index 000000000000..427eca76acae --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledBySystemMessagePluginTest.kt @@ -0,0 +1,71 @@ +package com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DisabledBySystemMessagePluginTest { + private lateinit var plugin: DisabledBySystemMessagePlugin + private val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + plugin = DisabledBySystemMessagePlugin() + } + + @Test + fun whenVPNIsEnabledThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLED)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnablinghenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLING)) {} + assertNull(result) + } + + @Test + fun whenVPNIsInvalidThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.INVALID)) {} + assertNull(result) + } + + @Test + fun whenVPNIsDisabledWithRevokedReasonThenGetViewReturnsNotNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.REVOKED)) {} + assertNotNull(result) + } + + @Test + fun whenVPNIsEnabledWithSelfStopReasonThenGetViewReturnsNotNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.SELF_STOP())) {} + assertNotNull(result) + } + + @Test + fun whenVPNIsEnabledWithErrorReasonThenGetViewReturnsNotNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.ERROR)) {} + assertNotNull(result) + } + + @Test + fun whenVPNIsEnabledWithRestartReasonThenGetViewReturnsNotNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.RESTART)) {} + assertNotNull(result) + } + + @Test + fun whenVPNIsEnabledWithUnknownReasonThenGetViewReturnsNotNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.UNKNOWN)) {} + assertNotNull(result) + } +} diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledMessagePluginTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledMessagePluginTest.kt new file mode 100644 index 000000000000..f7df354fc622 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/DisabledMessagePluginTest.kt @@ -0,0 +1,71 @@ +package com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DisabledMessagePluginTest { + private lateinit var plugin: DisabledMessagePlugin + private val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + plugin = DisabledMessagePlugin() + } + + @Test + fun whenVPNIsEnabledThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLED)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnablinghenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLING)) {} + assertNull(result) + } + + @Test + fun whenVPNIsInvalidThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.INVALID)) {} + assertNull(result) + } + + @Test + fun whenVPNIsDisabledWithRevokedReasonThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.REVOKED)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnabledWithSelfStopReasonThenGetViewReturnsNotNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.SELF_STOP())) {} + assertNotNull(result) + } + + @Test + fun whenVPNIsEnabledWithErrorReasonThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.ERROR)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnabledWithRestartReasonThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.RESTART)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnabledWithUnknownReasonThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.UNKNOWN)) {} + assertNull(result) + } +} diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/FakeVPNStore.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/FakeVPNStore.kt new file mode 100644 index 000000000000..11c6940802ff --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/FakeVPNStore.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 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.mobile.android.vpn.ui.onboarding.VpnStore + +class FakeVPNStore( + onboardingShown: Boolean = false, + appTpEnabledCtaShown: Boolean = false, + onboardingSession: Boolean = false, + notifyMeInAppTpDismissed: Boolean = false, + pproUpsellBannerDismissed: Boolean = false, +) : VpnStore { + private var _onboardingShown: Boolean = onboardingShown + private var _appTpEnabledCtaShown: Boolean = appTpEnabledCtaShown + private var _onboardingSession: Boolean = onboardingSession + private var _notifyMeInAppTpDismissed: Boolean = notifyMeInAppTpDismissed + private var _pproUpsellBannerDismissed: Boolean = pproUpsellBannerDismissed + + override fun onboardingDidShow() { + _onboardingShown = true + } + + override fun onboardingDidNotShow() { + _onboardingShown = false + } + + override fun didShowOnboarding(): Boolean = _onboardingShown + + override fun didShowAppTpEnabledCta(): Boolean = _appTpEnabledCtaShown + + override fun appTpEnabledCtaDidShow() { + _appTpEnabledCtaShown = true + } + + override fun getAndSetOnboardingSession(): Boolean { + return _onboardingSession.also { + _onboardingSession = true + } + } + + override fun dismissNotifyMeInAppTp() { + _notifyMeInAppTpDismissed = true + } + + override fun isNotifyMeInAppTpDismissed(): Boolean = _notifyMeInAppTpDismissed + + override fun dismissPproUpsellBanner() { + _pproUpsellBannerDismissed = true + } + + override fun isPproUpsellBannerDismised(): Boolean = _pproUpsellBannerDismissed +} diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/NextSessionEnabledMessagePluginTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/NextSessionEnabledMessagePluginTest.kt new file mode 100644 index 000000000000..018952f009dc --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/NextSessionEnabledMessagePluginTest.kt @@ -0,0 +1,52 @@ +package com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +class NextSessionEnabledMessagePluginTest { + private lateinit var plugin: NextSessionEnabledMessagePlugin + private lateinit var vpnStore: FakeVPNStore + private val context: Context = ApplicationProvider.getApplicationContext() + private val deviceShieldPixels: DeviceShieldPixels = mock() + + @Before + fun setUp() { + vpnStore = FakeVPNStore() + plugin = NextSessionEnabledMessagePlugin(vpnStore, deviceShieldPixels) + } + + @Test + fun whenVPNIsDisabledThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnablingThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLING)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnabledAndIsNotOnboardingThenGetViewReturnsView() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLED)) {} + assertNotNull(result) + } + + @Test + fun whenVPNIsEnabledAndIsOnboardingThenGetViewReturnsNull() { + vpnStore.getAndSetOnboardingSession() + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLED)) {} + assertNull(result) + } +} diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/OnboardingEnabledMessagePluginTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/OnboardingEnabledMessagePluginTest.kt new file mode 100644 index 000000000000..972673a89d5d --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/OnboardingEnabledMessagePluginTest.kt @@ -0,0 +1,49 @@ +package com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class OnboardingEnabledMessagePluginTest { + private lateinit var plugin: OnboardingEnabledMessagePlugin + private lateinit var vpnStore: FakeVPNStore + private val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + vpnStore = FakeVPNStore() + plugin = OnboardingEnabledMessagePlugin(vpnStore) + } + + @Test + fun whenVPNIsDisabledThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnablingThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLING)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnabledAndIsNotOnboardingThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLED)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnabledAndIsOnboardingThenGetViewReturnsView() { + vpnStore.getAndSetOnboardingSession() + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLED)) {} + assertNotNull(result) + } +} 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 new file mode 100644 index 000000000000..eb735e223848 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PProUpsellBannerPluginTest.kt @@ -0,0 +1,64 @@ +package com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message + +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.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 +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.subscriptions.api.Subscriptions +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class PProUpsellBannerPluginTest { + private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext + private val browserNav: BrowserNav = mock() + private val subscriptions: Subscriptions = mock() + private val deviceShieldPixels: DeviceShieldPixels = mock() + private lateinit var vpnStore: FakeVPNStore + private lateinit var plugin: PProUpsellBannerPlugin + + @Before + fun setUp() { + vpnStore = FakeVPNStore(pproUpsellBannerDismissed = false) + plugin = PProUpsellBannerPlugin(subscriptions, browserNav, vpnStore, deviceShieldPixels) + } + + @Test + fun whenVPNIsDisabledAndUserNotEligibleToPProThenGetViewReturnsNull() = runTest { + whenever(subscriptions.isEligible()).thenReturn(false) + + val result = plugin.getView(context, VpnState(state = DISABLED)) {} + + assertNull(result) + } + + @Test + fun whenVPNIsEnabledAndUserIsSubscriberToPProThenGetViewReturnsNull() = runTest { + whenever(subscriptions.isEligible()).thenReturn(true) + whenever(subscriptions.getAccessToken()).thenReturn("123") + + val result = plugin.getView(context, VpnState(state = ENABLED)) {} + + assertNull(result) + } + + @Test + fun whenBannerDismissedThenGetViewReturnsNull() = runTest { + whenever(subscriptions.isEligible()).thenReturn(true) + whenever(subscriptions.getAccessToken()).thenReturn(null) + vpnStore.dismissPproUpsellBanner() + + val result = plugin.getView(context, VpnState(state = ENABLED)) {} + + assertNull(result) + } +} 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 new file mode 100644 index 000000000000..cafd73ddaaa2 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellDisabledMessagePluginTest.kt @@ -0,0 +1,102 @@ +package com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message + +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.mobile.android.vpn.network.ExternalVpnDetector +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 +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.ENABLING +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.REVOKED +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.SELF_STOP +import com.duckduckgo.subscriptions.api.Subscriptions +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class PproUpsellDisabledMessagePluginTest { + private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext + private val browserNav: BrowserNav = mock() + private val subscriptions: Subscriptions = mock() + private val deviceShieldPixels: DeviceShieldPixels = mock() + private val vpnDetector: ExternalVpnDetector = mock() + private lateinit var plugin: PproUpsellDisabledMessagePlugin + + @Before + fun setUp() { + plugin = PproUpsellDisabledMessagePlugin(subscriptions, vpnDetector, browserNav, deviceShieldPixels) + } + + @Test + fun whenVPNIsDisabledWith3rdPartyOnAndUserNotEligibleToPProThenGetViewReturnsNull() = runTest { + whenever(vpnDetector.isExternalVpnDetected()).thenReturn(true) + whenever(subscriptions.isEligible()).thenReturn(false) + + val result = plugin.getView(context, VpnState(state = DISABLED, stopReason = SELF_STOP())) {} + + assertNull(result) + } + + @Test + fun whenVPNIsDisabledWith3rdPartyOnAndUserIsSubscriberToPProThenGetViewReturnsNull() = runTest { + whenever(vpnDetector.isExternalVpnDetected()).thenReturn(true) + whenever(subscriptions.isEligible()).thenReturn(true) + whenever(subscriptions.getAccessToken()).thenReturn("123") + + val result = plugin.getView(context, VpnState(state = DISABLED, stopReason = SELF_STOP())) {} + + assertNull(result) + } + + @Test + fun whenVPNIsDisabledWith3rdPartyOnAndUserIsEligibleToPproThenGetViewReturnsNotNull() = runTest { + whenever(vpnDetector.isExternalVpnDetected()).thenReturn(true) + whenever(subscriptions.isEligible()).thenReturn(true) + whenever(subscriptions.getAccessToken()).thenReturn(null) + + val result = plugin.getView(context, VpnState(state = DISABLED, stopReason = SELF_STOP())) {} + + assertNotNull(result) + } + + @Test + fun whenVPNIsDisabledBy3rdPartyOnAndUserIsEligibleToPproThenGetViewReturnsNull() = runTest { + whenever(vpnDetector.isExternalVpnDetected()).thenReturn(true) + whenever(subscriptions.isEligible()).thenReturn(true) + whenever(subscriptions.getAccessToken()).thenReturn(null) + + val result = plugin.getView(context, VpnState(state = DISABLED, stopReason = REVOKED)) {} + + assertNull(result) + } + + @Test + fun whenVPNIsEnablingWith3rdPartyOnAndUserIsEligibleToPproThenGetViewReturnsNull() = runTest { + whenever(vpnDetector.isExternalVpnDetected()).thenReturn(true) + whenever(subscriptions.isEligible()).thenReturn(true) + whenever(subscriptions.getAccessToken()).thenReturn(null) + + val result = plugin.getView(context, VpnState(state = ENABLING)) {} + + assertNull(result) + } + + @Test + fun whenVPNIsEnabledAndUserIsEligibleToPproThenGetViewReturnsNull() = runTest { + whenever(vpnDetector.isExternalVpnDetected()).thenReturn(true) + whenever(subscriptions.isEligible()).thenReturn(true) + whenever(subscriptions.getAccessToken()).thenReturn(null) + + val result = plugin.getView(context, VpnState(state = ENABLED)) {} + + assertNull(result) + } +} 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 new file mode 100644 index 000000000000..4d452a29e99a --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/PproUpsellRevokedMessagePluginTest.kt @@ -0,0 +1,94 @@ +package com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message + +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.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 +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.ENABLING +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.REVOKED +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.SELF_STOP +import com.duckduckgo.subscriptions.api.Subscriptions +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class PproUpsellRevokedMessagePluginTest { + private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext + private val browserNav: BrowserNav = mock() + private val subscriptions: Subscriptions = mock() + private val deviceShieldPixels: DeviceShieldPixels = mock() + private lateinit var plugin: PproUpsellRevokedMessagePlugin + + @Before + fun setUp() { + plugin = PproUpsellRevokedMessagePlugin(subscriptions, browserNav, deviceShieldPixels) + } + + @Test + fun whenVPNIsDisabledBy3rdPartyAndUserNotEligibleToPProThenGetViewReturnsNull() = runTest { + whenever(subscriptions.isEligible()).thenReturn(false) + + val result = plugin.getView(context, VpnState(state = DISABLED, stopReason = REVOKED)) {} + + assertNull(result) + } + + @Test + fun whenVPNIsDisabledBy3rdPartyOnAndUserIsSubscriberToPProThenGetViewReturnsNull() = runTest { + whenever(subscriptions.isEligible()).thenReturn(true) + whenever(subscriptions.getAccessToken()).thenReturn("123") + + val result = plugin.getView(context, VpnState(state = DISABLED, stopReason = REVOKED)) {} + + assertNull(result) + } + + @Test + fun whenVPNIsDisabledBy3rdPartyOnAndUserIsEligibleToPproThenGetViewReturnsNotNull() = runTest { + whenever(subscriptions.isEligible()).thenReturn(true) + whenever(subscriptions.getAccessToken()).thenReturn(null) + + val result = plugin.getView(context, VpnState(state = DISABLED, stopReason = REVOKED)) {} + + assertNotNull(result) + } + + @Test + fun whenVPNIsDisabledByUserAndUserIsEligibleToPproThenGetViewReturnsNull() = runTest { + whenever(subscriptions.isEligible()).thenReturn(true) + whenever(subscriptions.getAccessToken()).thenReturn(null) + + val result = plugin.getView(context, VpnState(state = DISABLED, stopReason = SELF_STOP())) {} + + assertNull(result) + } + + @Test + fun whenVPNIsEnablingAndUserIsEligibleToPproThenGetViewReturnsNull() = runTest { + whenever(subscriptions.isEligible()).thenReturn(true) + whenever(subscriptions.getAccessToken()).thenReturn(null) + + val result = plugin.getView(context, VpnState(state = ENABLING)) {} + + assertNull(result) + } + + @Test + fun whenVPNIsEnabledAndUserIsEligibleToPproThenGetViewReturnsNull() = runTest { + whenever(subscriptions.isEligible()).thenReturn(true) + whenever(subscriptions.getAccessToken()).thenReturn(null) + + val result = plugin.getView(context, VpnState(state = ENABLED)) {} + + assertNull(result) + } +} diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/RevokedMessagePluginTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/RevokedMessagePluginTest.kt new file mode 100644 index 000000000000..5f2a62b12930 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/view/message/RevokedMessagePluginTest.kt @@ -0,0 +1,71 @@ +package com.duckduckgo.mobile.android.vpn.ui.tracker_activity.view.message + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnState +import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RevokedMessagePluginTest { + private lateinit var plugin: RevokedMessagePlugin + private val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + plugin = RevokedMessagePlugin() + } + + @Test + fun whenVPNIsEnabledThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLED)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnablinghenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.ENABLING)) {} + assertNull(result) + } + + @Test + fun whenVPNIsInvalidThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.INVALID)) {} + assertNull(result) + } + + @Test + fun whenVPNIsDisabledWithRevokedReasonThenGetViewReturnsNotNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.REVOKED)) {} + assertNotNull(result) + } + + @Test + fun whenVPNIsEnabledWithSelfStopReasonThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.SELF_STOP())) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnabledWithErrorReasonThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.ERROR)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnabledWithRestartReasonThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.RESTART)) {} + assertNull(result) + } + + @Test + fun whenVPNIsEnabledWithUnknownReasonThenGetViewReturnsNull() { + val result = plugin.getView(context, VpnState(state = VpnRunningState.DISABLED, stopReason = VpnStopReason.UNKNOWN)) {} + assertNull(result) + } +}