diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt
index ff2ca1b76e5f..3731f6a3dc61 100644
--- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt
+++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt
@@ -98,8 +98,6 @@ import com.duckduckgo.app.browser.commands.NavigationCommand
import com.duckduckgo.app.browser.commands.NavigationCommand.Navigate
import com.duckduckgo.app.browser.customtabs.CustomTabPixelNames
import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment
-import com.duckduckgo.app.browser.duckchat.DuckChatJSHelper
-import com.duckduckgo.app.browser.duckchat.RealDuckChatJSHelper.Companion.DUCK_CHAT_FEATURE_NAME
import com.duckduckgo.app.browser.duckplayer.DUCK_PLAYER_FEATURE_NAME
import com.duckduckgo.app.browser.duckplayer.DUCK_PLAYER_PAGE_FEATURE_NAME
import com.duckduckgo.app.browser.duckplayer.DuckPlayerJSHelper
@@ -220,7 +218,9 @@ import com.duckduckgo.downloads.api.DownloadStateListener
import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckchat.api.DuckChat
+import com.duckduckgo.duckchat.impl.DuckChatJSHelper
import com.duckduckgo.duckchat.impl.DuckChatPixelName
+import com.duckduckgo.duckchat.impl.RealDuckChatJSHelper.Companion.DUCK_CHAT_FEATURE_NAME
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerOrigin.AUTO
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerOrigin.OVERLAY
@@ -5870,8 +5870,8 @@ class BrowserTabViewModelTest {
@Test
fun whenProcessJsCallbackMessageForDuckChatThenSendCommand() = runTest {
whenever(mockEnabledToggle.isEnabled()).thenReturn(true)
- val sendResponseToJs = Command.SendResponseToJs(JsCallbackData(JSONObject(), "", "", ""))
- whenever(mockDuckChatJSHelper.processJsCallbackMessage(anyString(), anyString(), anyOrNull(), anyOrNull())).thenReturn(sendResponseToJs)
+ val jsCallbackData = JsCallbackData(JSONObject(), "", "", "")
+ whenever(mockDuckChatJSHelper.processJsCallbackMessage(anyString(), anyString(), anyOrNull(), anyOrNull())).thenReturn(jsCallbackData)
testee.processJsCallbackMessage(
DUCK_CHAT_FEATURE_NAME,
"method",
diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
index 065999175b15..b598cce293f9 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
@@ -169,8 +169,6 @@ import com.duckduckgo.app.browser.commands.Command.WebViewError
import com.duckduckgo.app.browser.commands.NavigationCommand
import com.duckduckgo.app.browser.customtabs.CustomTabPixelNames
import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment
-import com.duckduckgo.app.browser.duckchat.DuckChatJSHelper
-import com.duckduckgo.app.browser.duckchat.RealDuckChatJSHelper.Companion.DUCK_CHAT_FEATURE_NAME
import com.duckduckgo.app.browser.duckplayer.DUCK_PLAYER_FEATURE_NAME
import com.duckduckgo.app.browser.duckplayer.DUCK_PLAYER_PAGE_FEATURE_NAME
import com.duckduckgo.app.browser.duckplayer.DuckPlayerJSHelper
@@ -299,7 +297,9 @@ import com.duckduckgo.downloads.api.DownloadStateListener
import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckchat.api.DuckChat
+import com.duckduckgo.duckchat.impl.DuckChatJSHelper
import com.duckduckgo.duckchat.impl.DuckChatPixelName
+import com.duckduckgo.duckchat.impl.RealDuckChatJSHelper.Companion.DUCK_CHAT_FEATURE_NAME
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
import com.duckduckgo.history.api.NavigationHistory
@@ -3533,7 +3533,7 @@ class BrowserTabViewModel @Inject constructor(
val response = duckChatJSHelper.processJsCallbackMessage(featureName, method, id, data)
withContext(dispatchers.main()) {
response?.let {
- command.value = it
+ command.value = SendResponseToJs(it)
}
}
}
diff --git a/app/src/main/java/com/duckduckgo/app/browser/duckchat/DuckChatPreferencesStore.kt b/app/src/main/java/com/duckduckgo/app/browser/duckchat/DuckChatPreferencesStore.kt
deleted file mode 100644
index 486b55062685..000000000000
--- a/app/src/main/java/com/duckduckgo/app/browser/duckchat/DuckChatPreferencesStore.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (c) 2025 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.app.browser.duckchat
-
-import androidx.core.content.edit
-import com.duckduckgo.data.store.api.SharedPreferencesProvider
-import com.duckduckgo.di.scopes.AppScope
-import com.squareup.anvil.annotations.ContributesBinding
-import javax.inject.Inject
-
-interface DuckChatPreferencesStore {
- fun fetchAndClearUserPreferences(): String?
- fun updateUserPreferences(userPreferences: String?)
-}
-
-@ContributesBinding(AppScope::class)
-class RealDuckChatPreferencesStore @Inject constructor(
- private val sharedPreferencesProvider: SharedPreferencesProvider,
-) : DuckChatPreferencesStore {
-
- private val preferences by lazy {
- sharedPreferencesProvider.getSharedPreferences(FILENAME)
- }
-
- override fun fetchAndClearUserPreferences(): String? =
- preferences.getString(USER_PREFERENCES, null).also {
- preferences.edit { remove(USER_PREFERENCES) }
- }
-
- override fun updateUserPreferences(userPreferences: String?) {
- preferences.edit {
- putString(USER_PREFERENCES, userPreferences)
- }
- }
-
- companion object {
- const val FILENAME = "com.duckduckgo.app.duckchat"
- const val USER_PREFERENCES = "DUCK_CHAT_USER_PREFERENCES"
- }
-}
diff --git a/app/src/main/java/com/duckduckgo/app/browser/webview/WebViewActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/webview/WebViewActivity.kt
index 33fc37396473..a59e74eb7066 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/webview/WebViewActivity.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/webview/WebViewActivity.kt
@@ -27,28 +27,16 @@ import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.browser.BrowserActivity
import com.duckduckgo.app.browser.BrowserWebViewClient
-import com.duckduckgo.app.browser.commands.Command.SendResponseToJs
import com.duckduckgo.app.browser.databinding.ActivityWebviewBinding
-import com.duckduckgo.app.browser.duckchat.DuckChatJSHelper
-import com.duckduckgo.app.browser.duckchat.RealDuckChatJSHelper.Companion.DUCK_CHAT_FEATURE_NAME
-import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.viewbinding.viewBinding
-import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
-import com.duckduckgo.js.messaging.api.JsMessageCallback
-import com.duckduckgo.js.messaging.api.JsMessaging
import com.duckduckgo.navigation.api.getActivityParams
import com.duckduckgo.user.agent.api.UserAgentProvider
import javax.inject.Inject
-import javax.inject.Named
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.json.JSONObject
@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(WebViewActivityWithParams::class)
@@ -63,20 +51,6 @@ class WebViewActivity : DuckDuckGoActivity() {
@Inject
lateinit var pixel: Pixel
- @Inject
- @Named("ContentScopeScripts")
- lateinit var contentScopeScripts: JsMessaging
-
- @Inject
- lateinit var duckChatJSHelper: DuckChatJSHelper
-
- @Inject
- @AppCoroutineScope
- lateinit var appCoroutineScope: CoroutineScope
-
- @Inject
- lateinit var dispatcherProvider: DispatcherProvider
-
private val binding: ActivityWebviewBinding by viewBinding()
private val toolbar
@@ -132,33 +106,6 @@ class WebViewActivity : DuckDuckGoActivity() {
databaseEnabled = false
setSupportZoom(true)
}
-
- contentScopeScripts.register(
- it,
- object : JsMessageCallback() {
- override fun process(
- featureName: String,
- method: String,
- id: String?,
- data: JSONObject?,
- ) {
- when (featureName) {
- DUCK_CHAT_FEATURE_NAME -> {
- appCoroutineScope.launch(dispatcherProvider.io()) {
- val response = duckChatJSHelper.processJsCallbackMessage(featureName, method, id, data)
- if (response is SendResponseToJs) {
- withContext(dispatcherProvider.main()) {
- contentScopeScripts.onResponse(response.data)
- }
- }
- }
- }
-
- else -> {}
- }
- }
- },
- )
}
url?.let {
diff --git a/app/src/test/java/com/duckduckgo/app/browser/duckchat/RealDuckChatPreferencesStoreTest.kt b/app/src/test/java/com/duckduckgo/app/browser/duckchat/RealDuckChatPreferencesStoreTest.kt
deleted file mode 100644
index f08d3c01d6f1..000000000000
--- a/app/src/test/java/com/duckduckgo/app/browser/duckchat/RealDuckChatPreferencesStoreTest.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (c) 2025 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.app.browser.duckchat
-
-import android.content.SharedPreferences
-import com.duckduckgo.app.browser.duckchat.RealDuckChatPreferencesStore.Companion.USER_PREFERENCES
-import com.duckduckgo.common.test.CoroutineTestRule
-import com.duckduckgo.common.test.api.InMemorySharedPreferences
-import com.duckduckgo.data.store.api.SharedPreferencesProvider
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNull
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-class RealDuckChatPreferencesStoreTest {
-
- @get:Rule
- var coroutineRule = CoroutineTestRule()
-
- private lateinit var preferences: SharedPreferences
- private lateinit var testee: RealDuckChatPreferencesStore
-
- @Before
- fun setup() = runTest {
- preferences = InMemorySharedPreferences()
-
- testee = RealDuckChatPreferencesStore(
- object : SharedPreferencesProvider {
- override fun getSharedPreferences(name: String, multiprocess: Boolean, migrate: Boolean): SharedPreferences {
- return preferences
- }
-
- override fun getEncryptedSharedPreferences(name: String, multiprocess: Boolean): SharedPreferences {
- return preferences
- }
- },
- )
- }
-
- @Test
- fun whenFetchAndClearUserPreferencesAndPreferencesExistThenReturnAndClearPreferences() = runTest {
- val storedPreferences = "userPreferences"
- preferences.edit().putString(USER_PREFERENCES, storedPreferences).apply()
-
- val result = testee.fetchAndClearUserPreferences()
-
- assertEquals(storedPreferences, result)
- assertNull(preferences.getString(USER_PREFERENCES, null))
- assertNull(testee.fetchAndClearUserPreferences())
- }
-
- @Test
- fun whenFetchAndClearUserPreferencesAndNoPreferencesExistThenReturnNull() = runTest {
- val result = testee.fetchAndClearUserPreferences()
-
- assertNull(result)
- }
-
- @Test
- fun whenUpdateUserPreferencesThenStoreProvidedPreferences() = runTest {
- val newPreferences = "newUserPreferences"
-
- testee.updateUserPreferences(newPreferences)
-
- assertEquals(newPreferences, preferences.getString(USER_PREFERENCES, null))
- }
-
- @Test
- fun whenUpdateUserPreferencesWithNullThenStoreNullPreferences() = runTest {
- testee.updateUserPreferences(null)
-
- assertNull(preferences.getString(USER_PREFERENCES, null))
- }
-}
diff --git a/browser-api/src/main/java/com/duckduckgo/browser/api/ui/BrowserScreens.kt b/browser-api/src/main/java/com/duckduckgo/browser/api/ui/BrowserScreens.kt
index 74a1eb7e0a82..4142c3c73b2a 100644
--- a/browser-api/src/main/java/com/duckduckgo/browser/api/ui/BrowserScreens.kt
+++ b/browser-api/src/main/java/com/duckduckgo/browser/api/ui/BrowserScreens.kt
@@ -22,13 +22,21 @@ import com.duckduckgo.navigation.api.GlobalActivityStarter
* Model that represents the Browser Screens hosted inside :app module that can be launched from other modules.
*/
sealed class BrowserScreens {
- object FeedbackActivityWithEmptyParams : GlobalActivityStarter.ActivityParams
+
+ /**
+ * Use this model to launch the standalone WebView
+ */
data class WebViewActivityWithParams(
val url: String,
val screenTitle: String,
val supportNewWindows: Boolean = false,
) : GlobalActivityStarter.ActivityParams
+ /**
+ * Use this model to launch the Feedback screen
+ */
+ object FeedbackActivityWithEmptyParams : GlobalActivityStarter.ActivityParams
+
/**
* Use this model to launch the Bookmarks screen
*/
diff --git a/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChatSettingsScreens.kt b/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChatSettingsScreens.kt
index 06aa6e3f5148..05c9e9f0c74c 100644
--- a/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChatSettingsScreens.kt
+++ b/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChatSettingsScreens.kt
@@ -19,6 +19,6 @@ package com.duckduckgo.duckchat.api
import com.duckduckgo.navigation.api.GlobalActivityStarter
/**
- * Use this model to launch the Duck Player Settings screen
+ * Use this model to launch the DuckChat Settings screen
*/
object DuckChatSettingsNoParams : GlobalActivityStarter.ActivityParams
diff --git a/duckchat/duckchat-impl/build.gradle b/duckchat/duckchat-impl/build.gradle
index 039e5a4f26fc..a17dcb76a1b4 100644
--- a/duckchat/duckchat-impl/build.gradle
+++ b/duckchat/duckchat-impl/build.gradle
@@ -30,6 +30,7 @@ dependencies {
implementation project(':common-ui')
implementation project(':common-utils')
implementation project(':browser-api')
+ implementation project(':js-messaging-api')
anvil project(path: ':anvil-compiler')
implementation project(path: ':anvil-annotations')
diff --git a/duckchat/duckchat-impl/src/main/AndroidManifest.xml b/duckchat/duckchat-impl/src/main/AndroidManifest.xml
index 85562b4c3388..b90168e9029b 100644
--- a/duckchat/duckchat-impl/src/main/AndroidManifest.xml
+++ b/duckchat/duckchat-impl/src/main/AndroidManifest.xml
@@ -22,5 +22,9 @@
android:exported="false"
android:label="@string/duck_chat_title"
android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity" />
+
\ No newline at end of file
diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatDataStore.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatDataStore.kt
index 11dd64ba7fdd..a5194e05c4e9 100644
--- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatDataStore.kt
+++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatDataStore.kt
@@ -20,6 +20,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.duckchat.impl.SharedPreferencesDuckChatDataStore.Keys.DUCK_CHAT_OPENED
@@ -40,6 +41,8 @@ interface DuckChatDataStore {
suspend fun setShowInBrowserMenu(showDuckChat: Boolean)
fun observeShowInBrowserMenu(): Flow
fun getShowInBrowserMenu(): Boolean
+ suspend fun fetchAndClearUserPreferences(): String?
+ suspend fun updateUserPreferences(userPreferences: String?)
suspend fun registerOpened()
suspend fun wasOpenedBefore(): Boolean
}
@@ -54,6 +57,7 @@ class SharedPreferencesDuckChatDataStore @Inject constructor(
private object Keys {
val DUCK_CHAT_SHOW_IN_MENU = booleanPreferencesKey(name = "DUCK_CHAT_SHOW_IN_MENU")
val DUCK_CHAT_OPENED = booleanPreferencesKey(name = "DUCK_CHAT_OPENED")
+ val DUCK_CHAT_USER_PREFERENCES = stringPreferencesKey("DUCK_CHAT_USER_PREFERENCES")
}
private val duckChatShowInBrowserMenu: StateFlow = store.data
@@ -75,6 +79,22 @@ class SharedPreferencesDuckChatDataStore @Inject constructor(
return duckChatShowInBrowserMenu.value
}
+ override suspend fun fetchAndClearUserPreferences(): String? {
+ val userPreferences = store.data.map { it[Keys.DUCK_CHAT_USER_PREFERENCES] }.firstOrNull()
+ store.edit { prefs -> prefs.remove(Keys.DUCK_CHAT_USER_PREFERENCES) }
+ return userPreferences
+ }
+
+ override suspend fun updateUserPreferences(userPreferences: String?) {
+ store.edit { prefs ->
+ if (userPreferences == null) {
+ prefs.remove(Keys.DUCK_CHAT_USER_PREFERENCES)
+ } else {
+ prefs[Keys.DUCK_CHAT_USER_PREFERENCES] = userPreferences
+ }
+ }
+ }
+
override suspend fun registerOpened() {
store.edit { it[DUCK_CHAT_OPENED] = true }
}
diff --git a/app/src/main/java/com/duckduckgo/app/browser/duckchat/DuckChatJSHelper.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatJSHelper.kt
similarity index 84%
rename from app/src/main/java/com/duckduckgo/app/browser/duckchat/DuckChatJSHelper.kt
rename to duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatJSHelper.kt
index 45688117f07a..a73614472db3 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/duckchat/DuckChatJSHelper.kt
+++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatJSHelper.kt
@@ -14,15 +14,14 @@
* limitations under the License.
*/
-package com.duckduckgo.app.browser.duckchat
+package com.duckduckgo.duckchat.impl
-import com.duckduckgo.app.browser.commands.Command
-import com.duckduckgo.app.browser.commands.Command.SendResponseToJs
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.js.messaging.api.JsCallbackData
import com.squareup.anvil.annotations.ContributesBinding
import javax.inject.Inject
+import kotlinx.coroutines.runBlocking
import org.json.JSONObject
interface DuckChatJSHelper {
@@ -31,13 +30,13 @@ interface DuckChatJSHelper {
method: String,
id: String?,
data: JSONObject?,
- ): Command?
+ ): JsCallbackData?
}
@ContributesBinding(AppScope::class)
class RealDuckChatJSHelper @Inject constructor(
private val duckChat: DuckChat,
- private val preferencesStore: DuckChatPreferencesStore,
+ private val dataStore: DuckChatDataStore,
) : DuckChatJSHelper {
override suspend fun processJsCallbackMessage(
@@ -45,16 +44,16 @@ class RealDuckChatJSHelper @Inject constructor(
method: String,
id: String?,
data: JSONObject?,
- ): Command? = when (method) {
+ ): JsCallbackData? = when (method) {
METHOD_GET_AI_CHAT_NATIVE_HANDOFF_DATA -> id?.let {
- SendResponseToJs(getAIChatNativeHandoffData(featureName, method, it))
+ getAIChatNativeHandoffData(featureName, method, it)
}
METHOD_GET_AI_CHAT_NATIVE_CONFIG_VALUES -> id?.let {
- SendResponseToJs(getAIChatNativeConfigValues(featureName, method, it))
+ getAIChatNativeConfigValues(featureName, method, it)
}
METHOD_OPEN_AI_CHAT -> {
val payload = extractPayload(data)
- preferencesStore.updateUserPreferences(payload)
+ dataStore.updateUserPreferences(payload)
duckChat.openDuckChat()
null
}
@@ -65,7 +64,7 @@ class RealDuckChatJSHelper @Inject constructor(
val jsonPayload = JSONObject().apply {
put(PLATFORM, ANDROID)
put(IS_HANDOFF_ENABLED, duckChat.isEnabled())
- put(PAYLOAD, preferencesStore.fetchAndClearUserPreferences())
+ put(PAYLOAD, runBlocking { dataStore.fetchAndClearUserPreferences() })
}
return JsCallbackData(jsonPayload, featureName, method, id)
}
diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatWebViewActivity.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatWebViewActivity.kt
new file mode 100644
index 000000000000..3b91a29e5e11
--- /dev/null
+++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatWebViewActivity.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2025 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.duckchat.impl
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.os.Message
+import android.view.MenuItem
+import android.webkit.WebChromeClient
+import android.webkit.WebSettings
+import android.webkit.WebView
+import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
+import com.duckduckgo.anvil.annotations.InjectWith
+import com.duckduckgo.app.di.AppCoroutineScope
+import com.duckduckgo.app.tabs.BrowserNav
+import com.duckduckgo.common.ui.DuckDuckGoActivity
+import com.duckduckgo.common.ui.viewbinding.viewBinding
+import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.di.scopes.ActivityScope
+import com.duckduckgo.duckchat.impl.RealDuckChatJSHelper.Companion.DUCK_CHAT_FEATURE_NAME
+import com.duckduckgo.duckchat.impl.databinding.ActivityDuckChatWebviewBinding
+import com.duckduckgo.js.messaging.api.JsMessageCallback
+import com.duckduckgo.js.messaging.api.JsMessaging
+import com.duckduckgo.navigation.api.GlobalActivityStarter
+import com.duckduckgo.navigation.api.getActivityParams
+import javax.inject.Inject
+import javax.inject.Named
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.json.JSONObject
+
+internal data class DuckChatWebViewActivityWithParams(
+ val url: String,
+) : GlobalActivityStarter.ActivityParams
+
+@InjectWith(ActivityScope::class)
+@ContributeToActivityStarter(DuckChatWebViewActivityWithParams::class)
+class DuckChatWebViewActivity : DuckDuckGoActivity() {
+
+ @Inject
+ lateinit var webViewClient: DuckChatWebViewClient
+
+ @Inject
+ @Named("ContentScopeScripts")
+ lateinit var contentScopeScripts: JsMessaging
+
+ @Inject
+ lateinit var duckChatJSHelper: DuckChatJSHelper
+
+ @Inject
+ @AppCoroutineScope
+ lateinit var appCoroutineScope: CoroutineScope
+
+ @Inject
+ lateinit var dispatcherProvider: DispatcherProvider
+
+ @Inject
+ lateinit var browserNav: BrowserNav
+
+ private val binding: ActivityDuckChatWebviewBinding by viewBinding()
+
+ private val toolbar
+ get() = binding.includeToolbar.toolbar
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(binding.root)
+ setupToolbar(toolbar)
+
+ val params = intent.getActivityParams(DuckChatWebViewActivityWithParams::class.java)
+ val url = params?.url
+
+ binding.simpleWebview.let {
+ it.webViewClient = webViewClient
+ it.webChromeClient = object : WebChromeClient() {
+ override fun onCreateWindow(
+ view: WebView?,
+ isDialog: Boolean,
+ isUserGesture: Boolean,
+ resultMsg: Message?,
+ ): Boolean {
+ view?.requestFocusNodeHref(resultMsg)
+ val newWindowUrl = resultMsg?.data?.getString("url")
+ if (newWindowUrl != null) {
+ startActivity(browserNav.openInNewTab(this@DuckChatWebViewActivity, newWindowUrl))
+ return true
+ }
+ return false
+ }
+ }
+
+ it.settings.apply {
+ userAgentString = CUSTOM_UA
+ javaScriptEnabled = true
+ domStorageEnabled = true
+ loadWithOverviewMode = true
+ useWideViewPort = true
+ builtInZoomControls = true
+ displayZoomControls = false
+ mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
+ setSupportMultipleWindows(true)
+ databaseEnabled = false
+ setSupportZoom(true)
+ }
+
+ contentScopeScripts.register(
+ it,
+ object : JsMessageCallback() {
+ override fun process(
+ featureName: String,
+ method: String,
+ id: String?,
+ data: JSONObject?,
+ ) {
+ when (featureName) {
+ DUCK_CHAT_FEATURE_NAME -> {
+ appCoroutineScope.launch(dispatcherProvider.io()) {
+ duckChatJSHelper.processJsCallbackMessage(featureName, method, id, data)?.let { response ->
+ withContext(dispatcherProvider.main()) {
+ contentScopeScripts.onResponse(response)
+ }
+ }
+ }
+ }
+ else -> {}
+ }
+ }
+ },
+ )
+ }
+
+ url?.let {
+ binding.simpleWebview.loadUrl(it)
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ android.R.id.home -> {
+ super.onBackPressed()
+ return true
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ override fun onBackPressed() {
+ if (binding.simpleWebview.canGoBack()) {
+ binding.simpleWebview.goBack()
+ } else {
+ super.onBackPressed()
+ }
+ }
+
+ companion object {
+ private const val CUSTOM_UA =
+ "Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/124.0.0.0 Mobile DuckDuckGo/5 Safari/537.36"
+ }
+}
diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatWebViewClient.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatWebViewClient.kt
new file mode 100644
index 000000000000..c8f285545e23
--- /dev/null
+++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/DuckChatWebViewClient.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2025 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.duckchat.impl
+
+import android.graphics.Bitmap
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.annotation.UiThread
+import com.duckduckgo.browser.api.JsInjectorPlugin
+import com.duckduckgo.common.utils.plugins.PluginPoint
+import javax.inject.Inject
+
+class DuckChatWebViewClient @Inject constructor(
+ private val jsPlugins: PluginPoint,
+) : WebViewClient() {
+
+ @UiThread
+ override fun onPageStarted(
+ webView: WebView,
+ url: String?,
+ favicon: Bitmap?,
+ ) {
+ jsPlugins.getPlugins().forEach {
+ it.onPageStarted(webView, url, null)
+ }
+ }
+}
diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt
index d88487bca1ec..acf86779ac1d 100644
--- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt
+++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt
@@ -23,7 +23,6 @@ import androidx.core.net.toUri
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.di.IsMainProcess
import com.duckduckgo.app.statistics.pixels.Pixel
-import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams
import com.duckduckgo.common.utils.AppUrl.ParamKey.QUERY
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
@@ -149,13 +148,10 @@ class RealDuckChat @Inject constructor(
private fun startDuckChatActivity(url: String) {
val intent = globalActivityStarter.startIntent(
context,
- WebViewActivityWithParams(
+ DuckChatWebViewActivityWithParams(
url = url,
- screenTitle = context.getString(R.string.duck_chat_title),
- supportNewWindows = true,
),
)
-
intent?.let {
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(it)
diff --git a/duckchat/duckchat-impl/src/main/res/layout/activity_duck_chat_webview.xml b/duckchat/duckchat-impl/src/main/res/layout/activity_duck_chat_webview.xml
new file mode 100644
index 000000000000..c64aa696b679
--- /dev/null
+++ b/duckchat/duckchat-impl/src/main/res/layout/activity_duck_chat_webview.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/DuckChatWebViewClientTest.kt b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/DuckChatWebViewClientTest.kt
new file mode 100644
index 000000000000..5109188a6026
--- /dev/null
+++ b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/DuckChatWebViewClientTest.kt
@@ -0,0 +1,27 @@
+package com.duckduckgo.duckchat.impl
+
+import android.webkit.WebView
+import com.duckduckgo.browser.api.JsInjectorPlugin
+import com.duckduckgo.common.utils.plugins.PluginPoint
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class DuckChatWebViewClientTest {
+
+ @Test
+ fun whenOnPageStartedCalledThenJsPluginOnPageStartedInvoked() {
+ val mockPlugin: JsInjectorPlugin = mock()
+ val pluginPoint: PluginPoint = mock()
+ whenever(pluginPoint.getPlugins()).thenReturn(listOf(mockPlugin))
+
+ val duckChatWebViewClient = DuckChatWebViewClient(pluginPoint)
+ val webView: WebView = mock()
+ val url = "https://example.com"
+
+ duckChatWebViewClient.onPageStarted(webView, url, null)
+
+ verify(mockPlugin).onPageStarted(webView, url, null)
+ }
+}
diff --git a/app/src/test/java/com/duckduckgo/app/browser/duckchat/RealDuckChatJSHelperTest.kt b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatJSHelperTest.kt
similarity index 62%
rename from app/src/test/java/com/duckduckgo/app/browser/duckchat/RealDuckChatJSHelperTest.kt
rename to duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatJSHelperTest.kt
index ecd1f20d0b2f..8bda5dff912c 100644
--- a/app/src/test/java/com/duckduckgo/app/browser/duckchat/RealDuckChatJSHelperTest.kt
+++ b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatJSHelperTest.kt
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-package com.duckduckgo.app.browser.duckchat
+package com.duckduckgo.duckchat.impl
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.duckduckgo.app.browser.commands.Command.SendResponseToJs
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.js.messaging.api.JsCallbackData
@@ -39,11 +38,11 @@ class RealDuckChatJSHelperTest {
var coroutineRule = CoroutineTestRule()
private val mockDuckChat: DuckChat = mock()
- private val mockPreferencesStore: DuckChatPreferencesStore = mock()
+ private val mockDataStore: DuckChatDataStore = mock()
private val testee = RealDuckChatJSHelper(
duckChat = mockDuckChat,
- preferencesStore = mockPreferencesStore,
+ dataStore = mockDataStore,
)
@Test
@@ -68,15 +67,15 @@ class RealDuckChatJSHelperTest {
}
@Test
- fun whenGetAIChatNativeHandoffDataAndDuckChatEnabledThenReturnSendResponseToJsWithDuckChatEnabled() = runTest {
+ fun whenGetAIChatNativeHandoffDataAndDuckChatEnabledThenReturnJsCallbackDataWithDuckChatEnabled() = runTest {
val featureName = "aiChat"
val method = "getAIChatNativeHandoffData"
val id = "123"
whenever(mockDuckChat.isEnabled()).thenReturn(true)
- whenever(mockPreferencesStore.fetchAndClearUserPreferences()).thenReturn("preferences")
+ whenever(mockDataStore.fetchAndClearUserPreferences()).thenReturn("preferences")
- val result = testee.processJsCallbackMessage(featureName, method, id, null) as SendResponseToJs
+ val result = testee.processJsCallbackMessage(featureName, method, id, null)
val jsonPayload = JSONObject().apply {
put("platform", "android")
@@ -84,24 +83,24 @@ class RealDuckChatJSHelperTest {
put("aiChatPayload", "preferences")
}
- val expected = SendResponseToJs(JsCallbackData(jsonPayload, featureName, method, id))
+ val expected = JsCallbackData(jsonPayload, featureName, method, id)
- assertEquals(expected.data.id, result.data.id)
- assertEquals(expected.data.method, result.data.method)
- assertEquals(expected.data.featureName, result.data.featureName)
- assertEquals(expected.data.params.toString(), result.data.params.toString())
+ assertEquals(expected.id, result!!.id)
+ assertEquals(expected.method, result.method)
+ assertEquals(expected.featureName, result.featureName)
+ assertEquals(expected.params.toString(), result.params.toString())
}
@Test
- fun whenGetAIChatNativeHandoffDataAndDuckChatDisabledThenReturnSendResponseToJsWithDuckChatDisabled() = runTest {
+ fun whenGetAIChatNativeHandoffDataAndDuckChatDisabledThenReturnJsCallbackDataWithDuckChatDisabled() = runTest {
val featureName = "aiChat"
val method = "getAIChatNativeHandoffData"
val id = "123"
whenever(mockDuckChat.isEnabled()).thenReturn(false)
- whenever(mockPreferencesStore.fetchAndClearUserPreferences()).thenReturn("preferences")
+ whenever(mockDataStore.fetchAndClearUserPreferences()).thenReturn("preferences")
- val result = testee.processJsCallbackMessage(featureName, method, id, null) as SendResponseToJs
+ val result = testee.processJsCallbackMessage(featureName, method, id, null)
val jsonPayload = JSONObject().apply {
put("platform", "android")
@@ -109,24 +108,24 @@ class RealDuckChatJSHelperTest {
put("aiChatPayload", "preferences")
}
- val expected = SendResponseToJs(JsCallbackData(jsonPayload, featureName, method, id))
+ val expected = JsCallbackData(jsonPayload, featureName, method, id)
- assertEquals(expected.data.id, result.data.id)
- assertEquals(expected.data.method, result.data.method)
- assertEquals(expected.data.featureName, result.data.featureName)
- assertEquals(expected.data.params.toString(), result.data.params.toString())
+ assertEquals(expected.id, result!!.id)
+ assertEquals(expected.method, result.method)
+ assertEquals(expected.featureName, result.featureName)
+ assertEquals(expected.params.toString(), result.params.toString())
}
@Test
- fun whenGetAIChatNativeHandoffDataAndPreferencesNullThenReturnSendResponseToJsWithPreferencesNull() = runTest {
+ fun whenGetAIChatNativeHandoffDataAndPreferencesNullThenReturnJsCallbackDataWithPreferencesNull() = runTest {
val featureName = "aiChat"
val method = "getAIChatNativeHandoffData"
val id = "123"
whenever(mockDuckChat.isEnabled()).thenReturn(true)
- whenever(mockPreferencesStore.fetchAndClearUserPreferences()).thenReturn(null)
+ whenever(mockDataStore.fetchAndClearUserPreferences()).thenReturn(null)
- val result = testee.processJsCallbackMessage(featureName, method, id, null) as SendResponseToJs
+ val result = testee.processJsCallbackMessage(featureName, method, id, null)
val jsonPayload = JSONObject().apply {
put("platform", "android")
@@ -134,12 +133,12 @@ class RealDuckChatJSHelperTest {
put("aiChatPayload", null)
}
- val expected = SendResponseToJs(JsCallbackData(jsonPayload, featureName, method, id))
+ val expected = JsCallbackData(jsonPayload, featureName, method, id)
- assertEquals(expected.data.id, result.data.id)
- assertEquals(expected.data.method, result.data.method)
- assertEquals(expected.data.featureName, result.data.featureName)
- assertEquals(expected.data.params.toString(), result.data.params.toString())
+ assertEquals(expected.id, result!!.id)
+ assertEquals(expected.method, result.method)
+ assertEquals(expected.featureName, result.featureName)
+ assertEquals(expected.params.toString(), result.params.toString())
}
@Test
@@ -153,63 +152,63 @@ class RealDuckChatJSHelperTest {
}
@Test
- fun whenGetAIChatNativeConfigValuesAndDuckChatEnabledThenReturnSendResponseToJsWithDuckChatEnabled() = runTest {
+ fun whenGetAIChatNativeConfigValuesAndDuckChatEnabledThenReturnJsCallbackDataWithDuckChatEnabled() = runTest {
val featureName = "aiChat"
val method = "getAIChatNativeConfigValues"
val id = "123"
whenever(mockDuckChat.isEnabled()).thenReturn(true)
- whenever(mockPreferencesStore.fetchAndClearUserPreferences()).thenReturn("preferences")
+ whenever(mockDataStore.fetchAndClearUserPreferences()).thenReturn("preferences")
- val result = testee.processJsCallbackMessage(featureName, method, id, null) as SendResponseToJs
+ val result = testee.processJsCallbackMessage(featureName, method, id, null)
val jsonPayload = JSONObject().apply {
put("platform", "android")
put("isAIChatHandoffEnabled", true)
}
- val expected = SendResponseToJs(JsCallbackData(jsonPayload, featureName, method, id))
+ val expected = JsCallbackData(jsonPayload, featureName, method, id)
- assertEquals(expected.data.id, result.data.id)
- assertEquals(expected.data.method, result.data.method)
- assertEquals(expected.data.featureName, result.data.featureName)
- assertEquals(expected.data.params.toString(), result.data.params.toString())
+ assertEquals(expected.id, result!!.id)
+ assertEquals(expected.method, result.method)
+ assertEquals(expected.featureName, result.featureName)
+ assertEquals(expected.params.toString(), result.params.toString())
}
@Test
- fun whenGetAIChatNativeConfigValuesAndDuckChatDisabledThenReturnSendResponseToJsWithDuckChatDisabled() = runTest {
+ fun whenGetAIChatNativeConfigValuesAndDuckChatDisabledThenReturnJsCallbackDataWithDuckChatDisabled() = runTest {
val featureName = "aiChat"
val method = "getAIChatNativeConfigValues"
val id = "123"
whenever(mockDuckChat.isEnabled()).thenReturn(false)
- whenever(mockPreferencesStore.fetchAndClearUserPreferences()).thenReturn("preferences")
+ whenever(mockDataStore.fetchAndClearUserPreferences()).thenReturn("preferences")
- val result = testee.processJsCallbackMessage(featureName, method, id, null) as SendResponseToJs
+ val result = testee.processJsCallbackMessage(featureName, method, id, null)
val jsonPayload = JSONObject().apply {
put("platform", "android")
put("isAIChatHandoffEnabled", false)
}
- val expected = SendResponseToJs(JsCallbackData(jsonPayload, featureName, method, id))
+ val expected = JsCallbackData(jsonPayload, featureName, method, id)
- assertEquals(expected.data.id, result.data.id)
- assertEquals(expected.data.method, result.data.method)
- assertEquals(expected.data.featureName, result.data.featureName)
- assertEquals(expected.data.params.toString(), result.data.params.toString())
+ assertEquals(expected.id, result!!.id)
+ assertEquals(expected.method, result.method)
+ assertEquals(expected.featureName, result.featureName)
+ assertEquals(expected.params.toString(), result.params.toString())
}
@Test
- fun whenGetAIChatNativeConfigValuesAndPreferencesNullThenReturnSendResponseToJsWithPreferencesNull() = runTest {
+ fun whenGetAIChatNativeConfigValuesAndPreferencesNullThenReturnJsCallbackDataWithPreferencesNull() = runTest {
val featureName = "aiChat"
val method = "getAIChatNativeConfigValues"
val id = "123"
whenever(mockDuckChat.isEnabled()).thenReturn(true)
- whenever(mockPreferencesStore.fetchAndClearUserPreferences()).thenReturn(null)
+ whenever(mockDataStore.fetchAndClearUserPreferences()).thenReturn(null)
- val result = testee.processJsCallbackMessage(featureName, method, id, null) as SendResponseToJs
+ val result = testee.processJsCallbackMessage(featureName, method, id, null)
val jsonPayload = JSONObject().apply {
put("platform", "android")
@@ -217,12 +216,12 @@ class RealDuckChatJSHelperTest {
put("aiChatPayload", null)
}
- val expected = SendResponseToJs(JsCallbackData(jsonPayload, featureName, method, id))
+ val expected = JsCallbackData(jsonPayload, featureName, method, id)
- assertEquals(expected.data.id, result.data.id)
- assertEquals(expected.data.method, result.data.method)
- assertEquals(expected.data.featureName, result.data.featureName)
- assertEquals(expected.data.params.toString(), result.data.params.toString())
+ assertEquals(expected.id, result!!.id)
+ assertEquals(expected.method, result.method)
+ assertEquals(expected.featureName, result.featureName)
+ assertEquals(expected.params.toString(), result.params.toString())
}
@Test
@@ -236,7 +235,7 @@ class RealDuckChatJSHelperTest {
assertNull(testee.processJsCallbackMessage(featureName, method, id, data))
- verify(mockPreferencesStore).updateUserPreferences(payloadString)
+ verify(mockDataStore).updateUserPreferences(payloadString)
verify(mockDuckChat).openDuckChat()
}
@@ -247,7 +246,7 @@ class RealDuckChatJSHelperTest {
val id = "123"
assertNull(testee.processJsCallbackMessage(featureName, method, id, null))
- verify(mockPreferencesStore).updateUserPreferences(null)
+ verify(mockDataStore).updateUserPreferences(null)
verify(mockDuckChat).openDuckChat()
}
@@ -259,7 +258,7 @@ class RealDuckChatJSHelperTest {
val data = JSONObject(mapOf("aiChatPayload" to JSONObject.NULL))
assertNull(testee.processJsCallbackMessage(featureName, method, id, data))
- verify(mockPreferencesStore).updateUserPreferences(null)
+ verify(mockDataStore).updateUserPreferences(null)
verify(mockDuckChat).openDuckChat()
}
}
diff --git a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt
index ce70cb14ea5f..76a2f58c608b 100644
--- a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt
+++ b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt
@@ -21,7 +21,6 @@ import android.content.Intent
import androidx.core.net.toUri
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.duckduckgo.app.statistics.pixels.Pixel
-import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
import com.duckduckgo.feature.toggles.api.Toggle.State
import com.duckduckgo.navigation.api.GlobalActivityStarter
@@ -73,7 +72,7 @@ class RealDuckChatTest {
whenever(mockDuckPlayerFeatureRepository.shouldShowInBrowserMenu()).thenReturn(true)
whenever(mockContext.getString(any())).thenReturn("Duck.ai")
setFeatureToggle(true)
- whenever(mockGlobalActivityStarter.startIntent(any(), any())).thenReturn(mockIntent)
+ whenever(mockGlobalActivityStarter.startIntent(any(), any())).thenReturn(mockIntent)
}
@Test
@@ -130,10 +129,8 @@ class RealDuckChatTest {
testee.openDuckChat()
verify(mockGlobalActivityStarter).startIntent(
mockContext,
- WebViewActivityWithParams(
+ DuckChatWebViewActivityWithParams(
url = "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5",
- screenTitle = "Duck.ai",
- supportNewWindows = true,
),
)
verify(mockContext).startActivity(any())
@@ -145,10 +142,8 @@ class RealDuckChatTest {
testee.openDuckChat(query = "example")
verify(mockGlobalActivityStarter).startIntent(
mockContext,
- WebViewActivityWithParams(
+ DuckChatWebViewActivityWithParams(
url = "https://duckduckgo.com/?q=example&ia=chat&duckai=5",
- screenTitle = "Duck.ai",
- supportNewWindows = true,
),
)
verify(mockContext).startActivity(any())
@@ -160,10 +155,8 @@ class RealDuckChatTest {
testee.openDuckChatWithAutoPrompt(query = "example")
verify(mockGlobalActivityStarter).startIntent(
mockContext,
- WebViewActivityWithParams(
+ DuckChatWebViewActivityWithParams(
url = "https://duckduckgo.com/?q=example&prompt=1&ia=chat&duckai=5",
- screenTitle = "Duck.ai",
- supportNewWindows = true,
),
)
verify(mockContext).startActivity(any())
diff --git a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/SharedPreferencesDuckChatDataStoreTest.kt b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/SharedPreferencesDuckChatDataStoreTest.kt
new file mode 100644
index 000000000000..58b415253aad
--- /dev/null
+++ b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/SharedPreferencesDuckChatDataStoreTest.kt
@@ -0,0 +1,80 @@
+package com.duckduckgo.duckchat.impl
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStoreFile
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.duckduckgo.common.test.CoroutineTestRule
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SharedPreferencesDuckChatDataStoreTest {
+
+ @get:Rule
+ val coroutineRule = CoroutineTestRule()
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private val testDataStore: DataStore =
+ PreferenceDataStoreFactory.create(
+ scope = coroutineRule.testScope,
+ produceFile = { context.preferencesDataStoreFile("duck_chat_store") },
+ )
+
+ private val testee: DuckChatDataStore =
+ SharedPreferencesDuckChatDataStore(testDataStore, coroutineRule.testScope)
+
+ companion object {
+ val DUCK_CHAT_USER_PREFERENCES = stringPreferencesKey("DUCK_CHAT_USER_PREFERENCES")
+ }
+
+ @Test
+ fun whenFetchAndClearUserPreferencesAndPreferencesExistThenReturnAndClearPreferences() = runTest {
+ val storedPreferences = "userPreferences"
+ testDataStore.updateData { current ->
+ current.toMutablePreferences().apply {
+ this[DUCK_CHAT_USER_PREFERENCES] = storedPreferences
+ }
+ }
+
+ val result = testee.fetchAndClearUserPreferences()
+
+ assertEquals(storedPreferences, result)
+ assertFalse(testDataStore.data.first().contains(DUCK_CHAT_USER_PREFERENCES))
+ assertNull(testee.fetchAndClearUserPreferences())
+ }
+
+ @Test
+ fun whenFetchAndClearUserPreferencesAndNoPreferencesExistThenReturnNull() = runTest {
+ val result = testee.fetchAndClearUserPreferences()
+
+ assertNull(result)
+ }
+
+ @Test
+ fun whenUpdateUserPreferencesThenStoreProvidedPreferences() = runTest {
+ val newPreferences = "newUserPreferences"
+
+ testee.updateUserPreferences(newPreferences)
+
+ assertEquals(newPreferences, testDataStore.data.first()[DUCK_CHAT_USER_PREFERENCES])
+ }
+
+ @Test
+ fun whenUpdateUserPreferencesWithNullThenStoreNullPreferences() = runTest {
+ testee.updateUserPreferences(null)
+
+ assertFalse(testDataStore.data.first().contains(DUCK_CHAT_USER_PREFERENCES))
+ }
+}