Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fd4c55a
Set up experiment to reactivate users. WIP.
anikiki May 8, 2025
d1f5191
Updated to use Toggle.DefaultFeatureValue.FALSE instead of false.
anikiki May 8, 2025
2dc4fdb
Removed condition.
anikiki May 8, 2025
a46e15b
Renamed variants.
anikiki May 8, 2025
36e3474
Simplified ReactivateUsersPixelsPlugin class.
anikiki May 8, 2025
4ecd434
Removed unneeded conditions. Renamed functions. Fixed lint.
anikiki May 8, 2025
8f9693e
Reverted the privacy config url.
anikiki May 8, 2025
5ea6266
Fixed existing tests. Added more tests.
anikiki May 8, 2025
8f14e9b
Renamed enum values.
anikiki May 8, 2025
91480a2
Updated copy.
anikiki May 8, 2025
190ee8f
Fixed tests.
anikiki May 8, 2025
d9fe58b
Moved DefaultBrowserDetector to browser-api module. Added a check if …
anikiki May 12, 2025
59b0d81
Merge branch 'develop' into feature/ana/experiment_setup_android_expe…
anikiki May 12, 2025
e00a20d
Added more checks for the default browser prompt.
anikiki May 12, 2025
0e83fd0
Updated the isEligible function.
anikiki May 15, 2025
0736b62
Updated video url.
anikiki May 15, 2025
dc579f0
Merge branch 'develop' into feature/ana/experiment_setup_android_expe…
anikiki May 15, 2025
1d057b1
Updated string.
anikiki May 16, 2025
71f351b
Updated the eligible condition.
anikiki May 16, 2025
5ae5690
Removed changing the settings when DuckPlayer prompt is dismissed.
anikiki May 16, 2025
c55c04a
Updated Duck Player prompt to match the latest design changes.
anikiki May 19, 2025
0a506e8
Updated to match the latest design changes.
anikiki May 19, 2025
cb1b371
Updated UI for the browser comparison prompt.
anikiki May 22, 2025
9d9822c
Changed copy as requested in Copy Review.
anikiki May 22, 2025
8b25380
Added pixel when cliking on even more protections link.
anikiki May 22, 2025
410a729
Updated UI for the Duck Player prompt.
anikiki May 22, 2025
4bdb203
Fixed dark mode.
anikiki May 23, 2025
875b743
Renamed colors.
anikiki May 23, 2025
2eb6561
Merge branch 'develop' into feature/ana/experiment_setup_android_expe…
anikiki May 23, 2025
1de7d8d
Replaced pixel by metric.
anikiki May 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.device.DeviceInfo
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.common.utils.plugins.headers.CustomHeadersProvider
import com.duckduckgo.daxprompts.impl.ReactivateUsersExperiment
import com.duckduckgo.downloads.api.DownloadStateListener
import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
Expand Down Expand Up @@ -553,6 +554,7 @@ class BrowserTabViewModelTest {
private val mockSiteErrorHandler: StringSiteErrorHandler = mock()
private val mockSiteHttpErrorHandler: HttpCodeSiteErrorHandler = mock()
private val mockSubscriptionsJSHelper: SubscriptionsJSHelper = mock()
private val mockReactivateUsersExperiment: ReactivateUsersExperiment = mock()

private val selectedTab = TabEntity("TAB_ID", "https://example.com", position = 0, sourceTabId = "TAB_ID_SOURCE")

Expand Down Expand Up @@ -728,7 +730,14 @@ class BrowserTabViewModelTest {
httpErrorPixels = { mockHttpErrorPixels },
duckPlayer = mockDuckPlayer,
duckChat = mockDuckChat,
duckPlayerJSHelper = DuckPlayerJSHelper(mockDuckPlayer, mockAppBuildConfig, mockPixel, mockDuckDuckGoUrlDetector, mockPagesSettingPlugin),
duckPlayerJSHelper = DuckPlayerJSHelper(
mockDuckPlayer,
mockAppBuildConfig,
mockPixel,
mockDuckDuckGoUrlDetector,
mockPagesSettingPlugin,
mockReactivateUsersExperiment,
),
duckChatJSHelper = mockDuckChatJSHelper,
refreshPixelSender = refreshPixelSender,
changeOmnibarPositionFeature = changeOmnibarPositionFeature,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,6 @@ import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject
import timber.log.Timber

interface DefaultBrowserDetector {
fun deviceSupportsDefaultBrowserConfiguration(): Boolean
fun isDefaultBrowser(): Boolean
fun hasDefaultBrowser(): Boolean
}

@ContributesMultibinding(scope = AppScope::class, boundType = BrowserFeatureStateReporterPlugin::class)
@ContributesBinding(scope = AppScope::class, boundType = DefaultBrowserDetector::class)
class AndroidDefaultBrowserDetector @Inject constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.daxprompts.impl.ReactivateUsersExperiment
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerOrigin.AUTO
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerOrigin.OVERLAY
Expand Down Expand Up @@ -74,6 +75,7 @@ class DuckPlayerJSHelper @Inject constructor(
private val pixel: Pixel,
private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector,
private val pagesSettingPlugin: PluginPoint<DuckPlayerPageSettingsPlugin>,
private val reactivateUsersExperiment: ReactivateUsersExperiment,
) {
private suspend fun getUserPreferences(featureName: String, method: String, id: String): JsCallbackData {
val userValues = duckPlayer.getUserPreferences()
Expand Down Expand Up @@ -175,6 +177,9 @@ class DuckPlayerJSHelper @Inject constructor(
data.getJSONObject("params").getString(it)
}
duckPlayer.sendDuckPlayerPixel(pixelName, paramsMap)
if (pixelName == "play.use") {
reactivateUsersExperiment.fireDuckPlayerUseIfInExperiment()
}
}

suspend fun processJsCallbackMessage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,57 @@
package com.duckduckgo.app.launch

import android.os.Bundle
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.browser.BrowserActivity
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.onboarding.ui.OnboardingActivity
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.daxprompts.api.DaxPromptBrowserComparisonNoParams
import com.duckduckgo.daxprompts.api.DaxPromptDuckPlayerNoParams
import com.duckduckgo.daxprompts.impl.ui.DaxPromptBrowserComparisonActivity.Companion.DAX_PROMPT_BROWSER_COMPARISON_SET_DEFAULT_EXTRA
import com.duckduckgo.daxprompts.impl.ui.DaxPromptDuckPlayerActivity.Companion.DAX_PROMPT_DUCK_PLAYER_ACTIVITY_URL_EXTRA
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.navigation.api.GlobalActivityStarter
import javax.inject.Inject
import kotlinx.coroutines.launch
import timber.log.Timber

@InjectWith(ActivityScope::class)
class LaunchBridgeActivity : DuckDuckGoActivity() {

private val viewModel: LaunchViewModel by bindViewModel()

private val startDaxPromptDuckPlayerActivityForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == RESULT_OK) {
val url = result.data?.getStringExtra(DAX_PROMPT_DUCK_PLAYER_ACTIVITY_URL_EXTRA)
Timber.d("Received RESULT_OK from DaxPromptDuckPlayerActivity with extra: $url.")
viewModel.onDaxPromptDuckPlayerActivityResult(url)
} else {
Timber.d("Received non-OK result from DaxPromptDuckPlayerActivity.")
viewModel.onDaxPromptDuckPlayerActivityResult()
}
}

private val startDaxPromptBrowserComparisonActivityForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == RESULT_OK) {
val show = result.data?.getBooleanExtra(DAX_PROMPT_BROWSER_COMPARISON_SET_DEFAULT_EXTRA, false)
Timber.d("Received RESULT_OK from DaxPromptBrowserComparisonActivity with extra: $show")
viewModel.onDaxPromptBrowserComparisonActivityResult(show)
} else {
Timber.d("Received non-OK result from DaxPromptBrowserComparisonActivity")
viewModel.onDaxPromptBrowserComparisonActivityResult()
}
}

@Inject
lateinit var globalActivityStarter: GlobalActivityStarter

override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
Expand Down Expand Up @@ -59,6 +95,24 @@ class LaunchBridgeActivity : DuckDuckGoActivity() {
is LaunchViewModel.Command.Home -> {
showHome()
}

is LaunchViewModel.Command.DaxPromptDuckPlayer -> {
showDaxPromptDuckPlayer()
}

is LaunchViewModel.Command.CloseDaxPrompt -> {
lifecycleScope.launch { viewModel.showOnboardingOrHome() }
}

is LaunchViewModel.Command.PlayVideoInDuckPlayer -> {
startActivity(BrowserActivity.intent(this, queryExtra = it.url))
overridePendingTransition(0, 0)
finish()
}

is LaunchViewModel.Command.DaxPromptBrowserComparison -> {
showDaxPromptBrowserComparison()
}
}
}

Expand All @@ -72,4 +126,18 @@ class LaunchBridgeActivity : DuckDuckGoActivity() {
overridePendingTransition(0, 0)
finish()
}

private fun showDaxPromptDuckPlayer() {
val intentDaxPromptDuckPlayer =
globalActivityStarter.startIntent(this, DaxPromptDuckPlayerNoParams)
intentDaxPromptDuckPlayer?.let { startDaxPromptDuckPlayerActivityForResult.launch(it) }
}

private fun showDaxPromptBrowserComparison() {
val intentDaxPromptComparisonChart =
globalActivityStarter.startIntent(this, DaxPromptBrowserComparisonNoParams)
intentDaxPromptComparisonChart?.let {
startDaxPromptBrowserComparisonActivityForResult.launch(it)
}
}
}
46 changes: 46 additions & 0 deletions app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ package com.duckduckgo.app.launch

import androidx.lifecycle.ViewModel
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.app.global.install.AppInstallStore
import com.duckduckgo.app.onboarding.store.UserStageStore
import com.duckduckgo.app.onboarding.store.isNewUser
import com.duckduckgo.app.referral.AppInstallationReferrerStateListener
import com.duckduckgo.app.referral.AppInstallationReferrerStateListener.Companion.MAX_REFERRER_WAIT_TIME_MS
import com.duckduckgo.common.utils.SingleLiveEvent
import com.duckduckgo.daxprompts.api.DaxPrompts
import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType.NONE
import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType.SHOW_CONTROL
import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType.SHOW_VARIANT_BROWSER_COMPARISON
import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType.SHOW_VARIANT_DUCKPLAYER
import com.duckduckgo.di.scopes.ActivityScope
import javax.inject.Inject
import kotlinx.coroutines.withTimeoutOrNull
Expand All @@ -32,6 +38,8 @@ import timber.log.Timber
class LaunchViewModel @Inject constructor(
private val userStageStore: UserStageStore,
private val appReferrerStateListener: AppInstallationReferrerStateListener,
private val daxPrompts: DaxPrompts,
private val appInstallStore: AppInstallStore,
) :
ViewModel() {

Expand All @@ -40,18 +48,56 @@ class LaunchViewModel @Inject constructor(
sealed class Command {
data object Onboarding : Command()
data class Home(val replaceExistingSearch: Boolean = false) : Command()
data object DaxPromptDuckPlayer : Command()
data class PlayVideoInDuckPlayer(val url: String) : Command()
data object DaxPromptBrowserComparison : Command()
data object CloseDaxPrompt : Command()
}

suspend fun determineViewToShow() {
waitForReferrerData()

when (daxPrompts.evaluate()) {
SHOW_CONTROL, NONE -> {
Timber.d("Control / None action")
showOnboardingOrHome()
}

SHOW_VARIANT_DUCKPLAYER -> {
Timber.d("Variant Duck Player action")
command.value = Command.DaxPromptDuckPlayer
}

SHOW_VARIANT_BROWSER_COMPARISON -> {
Timber.d("Variant Browser Comparison action")
command.value = Command.DaxPromptBrowserComparison
}
}
}

suspend fun showOnboardingOrHome() {
if (userStageStore.isNewUser()) {
command.value = Command.Onboarding
} else {
command.value = Command.Home()
}
}

fun onDaxPromptDuckPlayerActivityResult(url: String? = null) {
if (url != null) {
command.value = Command.PlayVideoInDuckPlayer(url)
} else {
command.value = Command.CloseDaxPrompt
}
}

fun onDaxPromptBrowserComparisonActivityResult(showComparisonChart: Boolean? = false) {
if (showComparisonChart != null) {
appInstallStore.defaultBrowser = showComparisonChart
}
command.value = Command.CloseDaxPrompt
}

private suspend fun waitForReferrerData() {
val startTime = System.currentTimeMillis()

Expand Down
55 changes: 55 additions & 0 deletions app/src/test/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ package com.duckduckgo.app.launch

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import com.duckduckgo.app.global.install.AppInstallStore
import com.duckduckgo.app.launch.LaunchViewModel.Command.DaxPromptBrowserComparison
import com.duckduckgo.app.launch.LaunchViewModel.Command.DaxPromptDuckPlayer
import com.duckduckgo.app.launch.LaunchViewModel.Command.Home
import com.duckduckgo.app.launch.LaunchViewModel.Command.Onboarding
import com.duckduckgo.app.onboarding.store.AppStage
import com.duckduckgo.app.onboarding.store.UserStageStore
import com.duckduckgo.app.referral.StubAppReferrerFoundStateListener
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.daxprompts.api.DaxPrompts
import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Rule
Expand All @@ -44,6 +49,8 @@ class LaunchViewModelTest {

private val userStageStore = mock<UserStageStore>()
private val mockCommandObserver: Observer<LaunchViewModel.Command> = mock()
private val mockDaxPrompts: DaxPrompts = mock()
private val mockAppInstallStore: AppInstallStore = mock()

private lateinit var testee: LaunchViewModel

Expand All @@ -57,7 +64,10 @@ class LaunchViewModelTest {
testee = LaunchViewModel(
userStageStore,
StubAppReferrerFoundStateListener("xx"),
mockDaxPrompts,
mockAppInstallStore,
)
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)
testee.command.observeForever(mockCommandObserver)

Expand All @@ -71,7 +81,10 @@ class LaunchViewModelTest {
testee = LaunchViewModel(
userStageStore,
StubAppReferrerFoundStateListener("xx", mockDelayMs = 1_000),
mockDaxPrompts,
mockAppInstallStore,
)
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)
testee.command.observeForever(mockCommandObserver)

Expand All @@ -85,7 +98,10 @@ class LaunchViewModelTest {
testee = LaunchViewModel(
userStageStore,
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
mockDaxPrompts,
mockAppInstallStore,
)
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)
testee.command.observeForever(mockCommandObserver)

Expand All @@ -99,7 +115,10 @@ class LaunchViewModelTest {
testee = LaunchViewModel(
userStageStore,
StubAppReferrerFoundStateListener("xx"),
mockDaxPrompts,
mockAppInstallStore,
)
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
testee.command.observeForever(mockCommandObserver)
testee.determineViewToShow()
Expand All @@ -111,7 +130,10 @@ class LaunchViewModelTest {
testee = LaunchViewModel(
userStageStore,
StubAppReferrerFoundStateListener("xx", mockDelayMs = 1_000),
mockDaxPrompts,
mockAppInstallStore,
)
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
testee.command.observeForever(mockCommandObserver)
testee.determineViewToShow()
Expand All @@ -123,10 +145,43 @@ class LaunchViewModelTest {
testee = LaunchViewModel(
userStageStore,
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
mockDaxPrompts,
mockAppInstallStore,
)
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
testee.command.observeForever(mockCommandObserver)
testee.determineViewToShow()
verify(mockCommandObserver).onChanged(any<Home>())
}

@Test
fun whenEvaluateReturnsDuckPlayerVariantThenCommandIsDaxPromptDuckPlayer() = runTest {
testee = LaunchViewModel(
userStageStore,
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
mockDaxPrompts,
mockAppInstallStore,
)
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.SHOW_VARIANT_DUCKPLAYER)
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
testee.command.observeForever(mockCommandObserver)
testee.determineViewToShow()
verify(mockCommandObserver).onChanged(any<DaxPromptDuckPlayer>())
}

@Test
fun whenEvaluateReturnsBrowserComparisonVariantThenCommandIsDaxPromptBrowserComparison() = runTest {
testee = LaunchViewModel(
userStageStore,
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
mockDaxPrompts,
mockAppInstallStore,
)
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.SHOW_VARIANT_BROWSER_COMPARISON)
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
testee.command.observeForever(mockCommandObserver)
testee.determineViewToShow()
verify(mockCommandObserver).onChanged(any<DaxPromptBrowserComparison>())
}
}
Loading
Loading