Skip to content
Merged
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 @@ -161,11 +161,16 @@ class AppThirdPartyCookieManagerTest {

@Test
fun whenClearAllDataIfDomainIsInExclusionListThenDomainNotDeletedFromDatabase() = runTest {
givenDomainIsInTheThirdPartyCookieList(EXCLUDED_DOMAIN_URI.host!!)
val excludedDomains = AppThirdPartyCookieManager.hostsThatAlwaysRequireThirdPartyCookies
excludedDomains.forEach { domain ->
givenDomainIsInTheThirdPartyCookieList(domain)
}

testee.clearAllData()

assertNotNull(authCookiesAllowedDomainsRepository.getDomain(EXCLUDED_DOMAIN_URI.host!!))
excludedDomains.forEach { domain ->
assertNotNull(authCookiesAllowedDomainsRepository.getDomain(domain))
}
}

private suspend fun givenDomainIsInTheThirdPartyCookieList(domain: String) = runTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ class AppThirdPartyCookieManager(
val host = uri.host ?: return
val domain = authCookiesAllowedDomainsRepository.getDomain(host)
withContext(dispatchers.main()) {
if (domain != null && hasExcludedCookieName()) {
// Allow third-party cookies for domains that need cross-domain functionality
if (hostsThatAlwaysRequireThirdPartyCookies.contains(host) || (domain != null && hasExcludedCookieName())) {
logcat { "Cookies enabled for $uri" }
cookieManagerProvider.get()?.setAcceptThirdPartyCookies(webView, true)
} else {
Expand Down Expand Up @@ -107,6 +108,8 @@ class AppThirdPartyCookieManager(
private const val CODE = "code"
const val GOOGLE_ACCOUNTS_URL = "https://accounts.google.com"
const val GOOGLE_ACCOUNTS_HOST = "accounts.google.com"
val hostsThatAlwaysRequireThirdPartyCookies = listOf("home.nest.com")

// duck.ai needs third-party cookies for cross-domain migration between duckduckgo.com and duck.ai
val hostsThatAlwaysRequireThirdPartyCookies = listOf("home.nest.com", "duck.ai", "duckduckgo.com")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ interface AuthCookiesAllowedDomainsDao {
fun getDomain(host: String): AuthCookieAllowedDomainEntity?

@Query("DELETE FROM auth_cookies_allowed_domains WHERE domain NOT IN (:exceptionList)")
fun deleteAll(exceptionList: String)
fun deleteAll(exceptionList: List<String>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class AuthCookiesAllowedDomainsRepository @Inject constructor(

suspend fun deleteAll(exceptionList: List<String> = emptyList()) {
withContext(dispatcherProvider.io()) {
authCookiesAllowedDomainsDao.deleteAll(exceptionList.joinToString(","))
authCookiesAllowedDomainsDao.deleteAll(exceptionList)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,6 @@ interface DuckChat {
* @return true if the onboarding was completed, false otherwise.
*/
suspend fun isContextualOnboardingCompleted(): Boolean

suspend fun isStandaloneMigrationCompleted(): Boolean
}
1 change: 1 addition & 0 deletions duckchat/duckchat-impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ dependencies {
implementation project(':navigation-api')
implementation project(':sync-api')
implementation project(':data-store-api')
implementation project(':cookies-api')

anvil project(path: ':anvil-compiler')
implementation project(path: ':anvil-annotations')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.di.IsMainProcess
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.tabs.BrowserNav
import com.duckduckgo.common.utils.AppUrl
import com.duckduckgo.common.utils.AppUrl.ParamKey.QUERY
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.cookies.api.CookieManagerProvider
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.duckchat.api.DuckAiFeatureState
import com.duckduckgo.duckchat.api.DuckChat
Expand Down Expand Up @@ -68,6 +70,7 @@ import kotlinx.coroutines.withContext
import logcat.logcat
import okhttp3.HttpUrl.Companion.toHttpUrl
import javax.inject.Inject
import kotlin.text.forEach

interface DuckChatInternal : DuckChat {
/**
Expand Down Expand Up @@ -288,6 +291,7 @@ class RealDuckChat @Inject constructor(
private val newAddressBarOptionBottomSheetDialogFactory: NewAddressBarOptionBottomSheetDialogFactory,
private val duckAiContextualOnboardingBottomSheetDialogFactory: DuckAiContextualOnboardingBottomSheetDialogFactory,
private val deviceSyncState: DeviceSyncState,
private val cookiesManager: CookieManagerProvider,
) : DuckChatInternal,
DuckAiFeatureState,
PrivacyConfigCallbackPlugin {
Expand Down Expand Up @@ -684,6 +688,13 @@ class RealDuckChat @Inject constructor(
return duckChatFeatureRepository.isContextualOnboardingCompleted()
}

override suspend fun isStandaloneMigrationCompleted(): Boolean {
val cookieManager = cookiesManager.get()
val ddgCookies = cookieManager?.getCookie(AppUrl.Url.COOKIES)?.split(";").orEmpty()
val isMigrationCompleted = ddgCookies.contains("migration_status_dev_01=migrated_dev_01")
return isMigrationCompleted
}

private suspend fun hasActiveSession(): Boolean {
val now = System.currentTimeMillis()
val lastSession = duckChatFeatureRepository.lastSessionTimestamp()
Expand Down Expand Up @@ -720,6 +731,8 @@ class RealDuckChat @Inject constructor(
}

duckChatLink = settingsJson?.aiChatURL ?: DUCK_CHAT_WEB_LINK
logcat { "Duck.ai: duckChatLink $duckChatLink" }

settingsJson
?.aiChatBangs
?.takeIf { it.isNotEmpty() }
Expand Down Expand Up @@ -781,11 +794,16 @@ class RealDuckChat @Inject constructor(
isFullscreenModeEnabled = showFullScreenMode
_showFullScreenMode.emit(showFullScreenMode)

val showContextualMode = isDuckChatFeatureEnabled && isDuckChatUserEnabled && duckChatFeature.contextualMode().isEnabled()
isContextualModeEnabled = showContextualMode
_showContextualMode.emit(showContextualMode)
val isContextualModeKillSwitch = duckChatFeature.contextualModeKillSwitch().isEnabled()

val showContextualMode = (isDuckChatFeatureEnabled && isDuckChatUserEnabled && isStandaloneMigrationCompleted()) || (
isDuckChatFeatureEnabled && isDuckChatUserEnabled && duckChatFeature.contextualMode().isEnabled()
)

isContextualModeEnabled = showContextualMode && isContextualModeKillSwitch
_showContextualMode.emit(isContextualModeEnabled)

isAutomaticContextAttachmentEnabled = showContextualMode &&
isAutomaticContextAttachmentEnabled = isContextualModeEnabled &&
duckChatFeature.automaticContextAttachment()
.isEnabled() && duckChatFeatureRepository.isAutomaticPageContextAttachmentUserSettingEnabled()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ class DuckChatContextualFragment :
) {
super.onViewCreated(view, savedInstanceState)

// Explicitly enable cookies for this WebView
cookieManager.setAcceptCookie(true)
cookieManager.setAcceptThirdPartyCookies(simpleWebview, true)

simpleWebview.let {
it.webViewClient = webViewClient
webViewClient.onPageFinishedListener = { url ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ interface DuckChatFeature {
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
fun sendInputScreenOnboardingWideEvent(): Toggle

/**
* @return `true` when the contextual mode killswitch is enabled
* This overrules contextualMode and standaloneMigration
*/
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
fun contextualModeKillSwitch(): Toggle

/**
* @return `true` when the contextual mode is enabled
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ open class DuckChatWebViewFragment : DuckDuckGoFragment(R.layout.activity_duck_c
private val cookieManager: CookieManager by lazy { CookieManager.getInstance() }

private var pendingFileDownload: PendingFileDownload? = null

private val downloadMessagesJob = ConflatedJob()

private val binding: ActivityDuckChatWebviewBinding by viewBinding()
Expand All @@ -196,6 +197,10 @@ open class DuckChatWebViewFragment : DuckDuckGoFragment(R.layout.activity_duck_c

val url = arguments?.getString(KEY_DUCK_AI_URL) ?: "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5"

// Explicitly enable cookies for this WebView
cookieManager.setAcceptCookie(true)
cookieManager.setAcceptThirdPartyCookies(simpleWebview, true)

simpleWebview.let {
it.webViewClient = webViewClient
it.webChromeClient = object : WebChromeClient() {
Expand Down Expand Up @@ -717,9 +722,6 @@ open class DuckChatWebViewFragment : DuckDuckGoFragment(R.layout.activity_duck_c

override fun onDestroyView() {
super.onDestroyView()
appCoroutineScope.launch(dispatcherProvider.io()) {
cookieManager.flush()
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.duckduckgo.duckchat.impl
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.webkit.CookieManager
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Lifecycle.State.CREATED
Expand All @@ -27,6 +28,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.tabs.BrowserNav
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.common.utils.AppUrl
import com.duckduckgo.cookies.api.CookieManagerProvider
import com.duckduckgo.duckchat.api.DuckChatSettingsNoParams
import com.duckduckgo.duckchat.impl.feature.AIChatImageUploadFeature
import com.duckduckgo.duckchat.impl.feature.DuckChatFeature
Expand Down Expand Up @@ -95,6 +98,7 @@ class RealDuckChatTest {
private val mockNewAddressBarOptionBottomSheetDialog: NewAddressBarOptionBottomSheetDialog = mock()
private val mockDuckAiContextualOnboardingBottomSheetDialogFactory: DuckAiContextualOnboardingBottomSheetDialogFactory = mock()
private val mockDeviceSyncState: DeviceSyncState = mock()
private val cookiesManager: CookieManagerProvider = mock()

private lateinit var testee: RealDuckChat

Expand Down Expand Up @@ -131,6 +135,7 @@ class RealDuckChatTest {
mockNewAddressBarOptionBottomSheetDialogFactory,
mockDuckAiContextualOnboardingBottomSheetDialogFactory,
mockDeviceSyncState,
cookiesManager,
),
)
coroutineRule.testScope.advanceUntilIdle()
Expand Down Expand Up @@ -1234,6 +1239,7 @@ class RealDuckChatTest {
@Test
fun `when contextual mode enabled, isDuckChatContextualModeEnabled returns true`() = runTest {
duckChatFeature.contextualMode().setRawStoredState(State(enable = true))
duckChatFeature.contextualModeKillSwitch().setRawStoredState(State(enable = true))
testee.onPrivacyConfigDownloaded()

assertTrue(testee.isDuckChatContextualModeEnabled())
Expand All @@ -1242,11 +1248,50 @@ class RealDuckChatTest {
@Test
fun `when contextual mode disabled, isDuckChatContextualModeEnabled returns false`() = runTest {
duckChatFeature.contextualMode().setRawStoredState(State(enable = false))
duckChatFeature.contextualModeKillSwitch().setRawStoredState(State(enable = true))
testee.onPrivacyConfigDownloaded()

assertFalse(testee.isDuckChatContextualModeEnabled())
}

@Test
fun `when migration cookie present and kill switch enabled then isDuckChatContextualModeEnabled returns true`() = runTest {
val cookieManager = mock<CookieManager>()
whenever(cookiesManager.get()).thenReturn(cookieManager)
whenever(cookieManager.getCookie(AppUrl.Url.COOKIES)).thenReturn("migration_status_dev_01=migrated_dev_01")
duckChatFeature.contextualMode().setRawStoredState(State(enable = false))
duckChatFeature.contextualModeKillSwitch().setRawStoredState(State(enable = true))

testee.onPrivacyConfigDownloaded()

assertTrue(testee.isDuckChatContextualModeEnabled())
}

@Test
fun `when migration cookie present then isStandaloneMigrationCompleted returns true`() = runTest {
val cookieManager = mock<CookieManager>()
whenever(cookiesManager.get()).thenReturn(cookieManager)
whenever(cookieManager.getCookie(AppUrl.Url.COOKIES)).thenReturn("a=b;migration_status_dev_01=migrated_dev_01;c=d")

assertTrue(testee.isStandaloneMigrationCompleted())
}

@Test
fun `when migration cookie missing then isStandaloneMigrationCompleted returns false`() = runTest {
val cookieManager = mock<CookieManager>()
whenever(cookiesManager.get()).thenReturn(cookieManager)
whenever(cookieManager.getCookie(AppUrl.Url.COOKIES)).thenReturn("a=b; c=d")

assertFalse(testee.isStandaloneMigrationCompleted())
}

@Test
fun `when cookie manager is null then isStandaloneMigrationCompleted returns false`() = runTest {
whenever(cookiesManager.get()).thenReturn(null)

assertFalse(testee.isStandaloneMigrationCompleted())
}

companion object {
val SETTINGS_JSON = """
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,7 @@ class DuckChatContextualViewModelTest {
) = Unit

override suspend fun isContextualOnboardingCompleted(): Boolean = true
override suspend fun isStandaloneMigrationCompleted(): Boolean = true
}

private class FakeDuckChatContextualDataStore : DuckChatContextualDataStore {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class FakeDuckChat(
private val cosmeticInputScreenUserSettingEnabled = MutableStateFlow<Boolean?>(null)
private val automaticContextAttachmentUserSettingEnabled = MutableStateFlow<Boolean>(false)
var contextualOnboardingCompleted: Boolean = false
var standaloneMigrationCompleted: Boolean = false

override fun isEnabled(): Boolean = enabled

Expand Down Expand Up @@ -100,6 +101,10 @@ class FakeDuckChat(
return contextualOnboardingCompleted
}

override suspend fun isStandaloneMigrationCompleted(): Boolean {
return standaloneMigrationCompleted
}

fun setEnabled(enabled: Boolean) {
this.enabled = enabled
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class FakeDuckChatInternal(
private val inputScreenUserSettingEnabled = MutableStateFlow(false)
private val cosmeticInputScreenUserSettingEnabled = MutableStateFlow<Boolean?>(null)
private val automaticContextAttachmentUserSettingEnabled = MutableStateFlow<Boolean>(false)
private val standaloneMigrationCompleted = MutableStateFlow<Boolean>(false)
var contextualOnboardingCompleted: Boolean = false

// DuckChat interface methods
Expand Down Expand Up @@ -81,6 +82,7 @@ class FakeDuckChatInternal(
}

override suspend fun isContextualOnboardingCompleted(): Boolean = contextualOnboardingCompleted
override suspend fun isStandaloneMigrationCompleted(): Boolean = standaloneMigrationCompleted.value

override fun isAutomaticContextAttachmentEnabled(): Boolean = automaticContextAttachmentUserSettingEnabled.value

Expand Down
Loading