Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -35,7 +35,6 @@ class DefaultBrowserObserver(
appInstallStore.defaultBrowser = isDefaultBrowser
when {
isDefaultBrowser -> {
appInstallStore.wasEverDefaultBrowser = true
val params = mapOf(
PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to false.toString(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class AdditionalDefaultBrowserPromptsImpl @Inject constructor(
moshi: Moshi,
) : AdditionalDefaultBrowserPrompts, ModalEvaluator, PrivacyConfigCallbackPlugin {

override val priority: Int = 2
override val priority: Int = 3
override val evaluatorId: String = "additional_default_browser_prompts"

private val evaluationMutex = Mutex()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ interface AppInstallStore : MainProcessLifecycleObserver {

var widgetInstalled: Boolean

/** Setting to `true` also flips [wasEverDefaultBrowser] to `true` permanently. */
var defaultBrowser: Boolean
var wasEverDefaultBrowser: Boolean
val wasEverDefaultBrowser: Boolean

var defaultBrowserChangedSurveyDone: Boolean

Expand Down Expand Up @@ -66,11 +67,15 @@ class AppInstallSharedPreferences @Inject constructor(

override var defaultBrowser: Boolean
get() = preferences.getBoolean(KEY_DEFAULT_BROWSER, false)
set(defaultBrowser) = preferences.edit { putBoolean(KEY_DEFAULT_BROWSER, defaultBrowser) }
set(defaultBrowser) = preferences.edit {
putBoolean(KEY_DEFAULT_BROWSER, defaultBrowser)
if (defaultBrowser) {
putBoolean(KEY_WAS_EVER_DEFAULT_BROWSER, true)
}
}

override var wasEverDefaultBrowser: Boolean
override val wasEverDefaultBrowser: Boolean
get() = preferences.getBoolean(KEY_WAS_EVER_DEFAULT_BROWSER, false)
set(value) = preferences.edit { putBoolean(KEY_WAS_EVER_DEFAULT_BROWSER, value) }

override var defaultBrowserChangedSurveyDone: Boolean
get() = preferences.getBoolean(KEY_DEFAULT_BROWSER_CHANGED_SURVEY_DONE, false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ class AndroidUserBrowserProperties(
return appInstallStore.defaultBrowser
}

override suspend fun wasEverDefaultBrowser(): Boolean {
return appInstallStore.wasEverDefaultBrowser
}

override fun emailEnabled(): Boolean {
return emailManager.isSignedIn()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,6 @@ class BrandDesignUpdatePageViewModel @Inject constructor(
fun onDefaultBrowserSet() {
defaultRoleBrowserDialog.dialogShown()
appInstallStore.defaultBrowser = true
appInstallStore.wasEverDefaultBrowser = true
pixel.fire(AppPixelName.DEFAULT_BROWSER_SET, mapOf(PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString()))
viewModelScope.launch {
_viewState.update { it.copy(showSplitOption = isSplitOmnibarEnabled()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ class DefaultBrowserPageViewModel @Inject constructor(
timesPressedJustOnce = 0
if (defaultBrowserDetector.isDefaultBrowser()) {
installStore.defaultBrowser = true
installStore.wasEverDefaultBrowser = true
val params = mapOf(
Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString(),
Pixel.PixelParameter.DEFAULT_BROWSER_SET_ORIGIN to originValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,6 @@ class WelcomePageViewModel @Inject constructor(
fun onDefaultBrowserSet() {
defaultRoleBrowserDialog.dialogShown()
appInstallStore.defaultBrowser = true
appInstallStore.wasEverDefaultBrowser = true
pixel.fire(AppPixelName.DEFAULT_BROWSER_SET, mapOf(PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString()))

viewModelScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,36 +65,6 @@ class DefaultBrowserObserverTest {
verify(mockPixel).fire(AppPixelName.DEFAULT_BROWSER_SET, params)
}

@Test
fun whenDDGBecomesDefaultBrowserThenWasEverDefaultSetToTrue() {
whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(true)
whenever(mockAppInstallStore.defaultBrowser).thenReturn(false)

testee.onResume(mockOwner)

verify(mockAppInstallStore).wasEverDefaultBrowser = true
}

@Test
fun whenDDGIsNotDefaultBrowserThenWasEverDefaultNotChanged() {
whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(false)
whenever(mockAppInstallStore.defaultBrowser).thenReturn(false)

testee.onResume(mockOwner)

verify(mockAppInstallStore, never()).wasEverDefaultBrowser = any()
}

@Test
fun whenDDGRemainsDefaultBrowserThenWasEverDefaultNotChanged() {
whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(true)
whenever(mockAppInstallStore.defaultBrowser).thenReturn(true)

testee.onResume(mockOwner)

verify(mockAppInstallStore, never()).wasEverDefaultBrowser = any()
}

@Test
fun whenDDGIsNotDefaultBrowserIfItWasNotBeforeThenDoNotFireSetPixel() {
whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,35 @@ class AppInstallSharedPreferencesTest {

assertEquals(existingTimestamp, testee.installTimestamp)
}

@Test
fun whenInitializedThenDefaultBrowserAndWasEverDefaultBrowserAreFalse() = runTest {
assertFalse(testee.defaultBrowser)
assertFalse(testee.wasEverDefaultBrowser)
}

@Test
fun whenDefaultBrowserSetToTrueThenWasEverDefaultBrowserIsTrue() = runTest {
testee.defaultBrowser = true

assertTrue(testee.defaultBrowser)
assertTrue(testee.wasEverDefaultBrowser)
}

@Test
fun whenDefaultBrowserSetToFalseInitiallyThenWasEverDefaultBrowserRemainsFalse() = runTest {
testee.defaultBrowser = false

assertFalse(testee.defaultBrowser)
assertFalse(testee.wasEverDefaultBrowser)
}

@Test
fun whenDefaultBrowserSetToFalseAfterBeingTrueThenWasEverDefaultBrowserRemainsTrue() = runTest {
testee.defaultBrowser = true
testee.defaultBrowser = false

assertFalse(testee.defaultBrowser)
assertTrue(testee.wasEverDefaultBrowser)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2018,6 +2018,10 @@ class TabSwitcherViewModelTest {
TODO("Not yet implemented")
}

override suspend fun wasEverDefaultBrowser(): Boolean {
TODO("Not yet implemented")
}

override fun emailEnabled(): Boolean {
TODO("Not yet implemented")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ interface UserBrowserProperties {
fun daysSinceInstalled(): Long
suspend fun daysUsedSince(since: Date): Long
fun defaultBrowser(): Boolean

/**
* Returns true if this app has ever been set as the user's default browser.
* Once true, this value is permanent — it does not revert if the user later
* changes their default browser to something else.
*/
suspend fun wasEverDefaultBrowser(): Boolean
fun emailEnabled(): Boolean
fun searchCount(): Long
fun widgetAdded(): Boolean
Expand Down
1 change: 1 addition & 0 deletions dax-prompts/dax-prompts-impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ apply from: "$rootProject.projectDir/gradle/android-library.gradle"

dependencies {
implementation project(':dax-prompts-api')
implementation project(':modal-coordinator-api')
implementation project(':browser-api')
implementation project(':common-utils')
implementation project(':design-system')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright (c) 2026 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.daxprompts.impl

import android.content.Context
import android.content.Intent
import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.onboarding.OnboardingFlowChecker
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.daxprompts.api.DaxPromptBrowserComparisonNoParams
import com.duckduckgo.daxprompts.impl.repository.DaxPromptsRepository
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.modalcoordinator.api.ModalEvaluator
import com.duckduckgo.navigation.api.GlobalActivityStarter
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.anvil.annotations.ContributesMultibinding
import dagger.SingleInstanceIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject

interface WinBackPromptEvaluator

@ContributesMultibinding(
scope = AppScope::class,
boundType = ModalEvaluator::class,
)
@ContributesBinding(
scope = AppScope::class,
boundType = WinBackPromptEvaluator::class,
)
@SingleInstanceIn(scope = AppScope::class)
class WinBackPromptEvaluatorImpl @Inject constructor(
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val applicationContext: Context,
private val userBrowserProperties: UserBrowserProperties,
private val defaultBrowserDetector: DefaultBrowserDetector,
private val daxPromptsRepository: DaxPromptsRepository,
private val globalActivityStarter: GlobalActivityStarter,
private val dispatchers: DispatcherProvider,
private val reactivateUsersToggles: ReactivateUsersToggles,
private val onboardingFlowChecker: OnboardingFlowChecker,
) : ModalEvaluator, WinBackPromptEvaluator {
Comment thread
cursor[bot] marked this conversation as resolved.

override val priority: Int = 1

override val evaluatorId: String = "win_back_prompt"

override suspend fun evaluate(): ModalEvaluator.EvaluationResult = withContext(dispatchers.io()) {
if (isEnabled() && isEligible()) {
val intent = globalActivityStarter
.startIntent(applicationContext, DaxPromptBrowserComparisonNoParams) ?: return@withContext ModalEvaluator.EvaluationResult.Skipped

delay(MODAL_DISPLAY_DELAY)
appCoroutineScope.launch(dispatchers.main()) {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
applicationContext.startActivity(intent)
}

return@withContext ModalEvaluator.EvaluationResult.ModalShown
} else {
return@withContext ModalEvaluator.EvaluationResult.Skipped
}
}
Comment thread
cursor[bot] marked this conversation as resolved.

private fun isEnabled() = reactivateUsersToggles.self().isEnabled() && reactivateUsersToggles.defaultBrowserWinBackPrompt().isEnabled()

private suspend fun isEligible() = onboardingFlowChecker.isOnboardingComplete() &&
userBrowserProperties.wasEverDefaultBrowser() &&
!daxPromptsRepository.getDaxPromptsBrowserComparisonShown() &&
!defaultBrowserDetector.isDefaultBrowser()

companion object {
private const val MODAL_DISPLAY_DELAY = 1500L
}
}
Loading
Loading