Skip to content

Commit

Permalink
Add privacy pro upsell in AppTP (#4953)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
karlenDimla authored Sep 4, 2024
1 parent 97e1b2c commit 360893a
Show file tree
Hide file tree
Showing 33 changed files with 1,624 additions and 114 deletions.
1 change: 1 addition & 0 deletions app-tracking-protection/vpn-impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<String, String> = emptyMap(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ 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(),
)
}

companion object {
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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ interface VpnStore {

fun dismissNotifyMeInAppTp()
fun isNotifyMeInAppTpDismissed(): Boolean

fun dismissPproUpsellBanner()
fun isPproUpsellBannerDismised(): Boolean
}

@ContributesBinding(AppScope::class)
Expand Down Expand Up @@ -96,13 +99,22 @@ 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"

private const val KEY_DEVICE_SHIELD_ONBOARDING_LAUNCHED = "KEY_DEVICE_SHIELD_ONBOARDING_LAUNCHED"
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -111,6 +113,9 @@ class DeviceShieldTrackerActivity :
@Inject
lateinit var globalActivityStarter: GlobalActivityStarter

@Inject
lateinit var appTPStateMessagePluginPoint: ActivePluginPoint<AppTPStateMessagePlugin>

private val binding: ActivityDeviceShieldActivityBinding by viewBinding()

private lateinit var deviceShieldSwitch: DaxSwitch
Expand All @@ -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)

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) }
Expand Down Expand Up @@ -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)
Expand All @@ -515,7 +545,7 @@ class DeviceShieldTrackerActivity :
}

updateCounts(state.trackerCountInfo)
updateRunningState(state.runningState, state.bannerState)
updateRunningState(state.runningState)
}

private fun updateCounts(trackerCountInfo: DeviceShieldTrackerActivityViewModel.TrackerCountInfo) {
Expand All @@ -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()
}
}
}

Expand Down Expand Up @@ -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"
Expand Down
Loading

0 comments on commit 360893a

Please sign in to comment.