diff --git a/app/build.gradle b/app/build.gradle
index 9ab0cb1860aa..e6913c178c32 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -511,6 +511,9 @@ dependencies {
implementation project(':settings-api')
implementation project(':settings-impl')
+ implementation project(':desktop-app-promotion-api')
+ implementation project(':desktop-app-promotion-impl')
+
implementation project(':broken-site-api')
implementation project(':broken-site-impl')
implementation project(':broken-site-store')
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 98d6e2c81d51..af739b0b46c5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -408,11 +408,6 @@
android:exported="false"
android:label="@string/aboutActivityTitle"
android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity" />
-
-
-
{
- finish()
- }
-
- is GetDesktopBrowserViewModel.Command.Dismissed -> {
- finish()
- }
-
- is GetDesktopBrowserViewModel.Command.ShareDownloadLink -> {
- launchShareSheet(command.url)
- }
-
- GetDesktopBrowserViewModel.Command.ShowCopiedNotification -> {
- showCopiedNotification()
- }
- }
- }
-
- private fun launchShareSheet(shareLink: String) {
- val shareIntent = Intent(Intent.ACTION_SEND).apply {
- type = "text/plain"
- putExtra(Intent.EXTRA_TEXT, shareLink)
- putExtra(Intent.EXTRA_TITLE, getString(R.string.getDesktopBrowserShareDownloadLink))
- }
-
- val pendingIntent = PendingIntent.getBroadcast(
- this,
- 0,
- Intent(this, GetDesktopBrowserShareBroadcastReceiver::class.java),
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
- )
-
- try {
- startActivity(
- Intent.createChooser(
- shareIntent,
- getString(R.string.getDesktopBrowserShareDownloadLink),
- pendingIntent.intentSender,
- ),
- )
- } catch (e: ActivityNotFoundException) {
- logcat(LogPriority.WARN) { "Activity not found for share: $e" }
- }
- }
-
- private fun setupClickListeners() {
- binding.shareDownloadLinkButton.setOnClickListener {
- viewModel.onShareDownloadLinkClicked()
- }
- binding.noThanksButton.setOnClickListener {
- viewModel.onNoThanksClicked()
- }
- binding.browserUrl.setOnClickListener {
- viewModel.onLinkClicked()
- }
- }
-
- private fun setupBackNavigationHandler() {
- onBackPressedDispatcher.addCallback(this) {
- viewModel.onBackPressed()
- }
- }
-
- private fun showCopiedNotification() {
- Snackbar.make(binding.root, R.string.getDesktopBrowserUrlLinkCopied, Snackbar.LENGTH_SHORT).show()
- }
-
- private fun getDesktopBrowserViewModel(params: GetDesktopBrowserActivityParams): GetDesktopBrowserViewModel = ViewModelProvider.create(
- store = viewModelStore,
- factory = object : ViewModelProvider.Factory {
- @Suppress("UNCHECKED_CAST")
- override fun create(modelClass: Class) = getDesktopBrowserViewModelFactory.create(params) as T
- },
- extras = this.defaultViewModelCreationExtras,
- )[GetDesktopBrowserViewModel::class.java]
-}
-
-data class GetDesktopBrowserActivityParams(
- val source: Source,
-) : ActivityParams {
- enum class Source {
- COMPLETE_SETUP,
- OTHER,
- }
-}
diff --git a/app/src/main/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserViewModel.kt
deleted file mode 100644
index f22886f1134e..000000000000
--- a/app/src/main/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserViewModel.kt
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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.app.desktopbrowser
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.duckduckgo.app.clipboard.ClipboardInteractor
-import com.duckduckgo.app.desktopbrowser.GetDesktopBrowserActivityParams.Source
-import com.duckduckgo.app.pixels.AppPixelName
-import com.duckduckgo.app.settings.GetDesktopBrowserCompleteSetupSettings.Companion.GET_DESKTOP_BROWSER_SOURCE_PIXEL_PARAM
-import com.duckduckgo.app.settings.db.SettingsDataStore
-import com.duckduckgo.app.statistics.pixels.Pixel
-import com.duckduckgo.common.utils.DispatcherProvider
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-class GetDesktopBrowserViewModel @AssistedInject constructor(
- @Assisted private val params: GetDesktopBrowserActivityParams,
- private val settingsDataStore: SettingsDataStore,
- private val dispatchers: DispatcherProvider,
- private val clipboardInteractor: ClipboardInteractor,
- private val pixel: Pixel,
-) : ViewModel() {
-
- private val _viewState = MutableStateFlow(
- ViewState(showNoThanksButton = params.source == Source.COMPLETE_SETUP),
- )
- private val _command = Channel(1, BufferOverflow.DROP_OLDEST)
-
- val commands: Flow = _command.receiveAsFlow()
- val viewState: Flow = _viewState.asStateFlow()
-
- fun onShareDownloadLinkClicked() {
- viewModelScope.launch {
- pixel.fire(AppPixelName.GET_DESKTOP_BROWSER_SHARE_DOWNLOAD_LINK_CLICK)
- _command.send(
- Command.ShareDownloadLink(
- url = DESKTOP_BROWSER_URL,
- ),
- )
- }
- }
-
- fun onNoThanksClicked() {
- viewModelScope.launch {
- withContext(dispatchers.io()) {
- pixel.fire(
- AppPixelName.GET_DESKTOP_BROWSER_DISMISSED,
- mapOf(GET_DESKTOP_BROWSER_SOURCE_PIXEL_PARAM to GET_DESKTOP_BROWSER_SOURCE_NO_THANKS),
- )
- settingsDataStore.getDesktopBrowserSettingDismissed = true
- }
- _command.send(Command.Dismissed)
- }
- }
-
- fun onBackPressed() {
- viewModelScope.launch {
- _command.send(Command.Close)
- }
- }
-
- fun onLinkClicked() {
- viewModelScope.launch(dispatchers.io()) {
- pixel.fire(AppPixelName.GET_DESKTOP_BROWSER_LINK_CLICK)
- settingsDataStore.getDesktopBrowserSettingDismissed = true
- if (!clipboardInteractor.copyToClipboard(DESKTOP_BROWSER_URL, isSensitive = false)) {
- _command.send(Command.ShowCopiedNotification)
- }
- }
- }
-
- data class ViewState(
- val showNoThanksButton: Boolean = false,
- )
-
- sealed class Command {
- object Close : Command()
- object Dismissed : Command()
-
- object ShowCopiedNotification : Command()
- data class ShareDownloadLink(val url: String) : Command()
- }
-
- @AssistedFactory
- interface Factory {
- fun create(params: GetDesktopBrowserActivityParams): GetDesktopBrowserViewModel
- }
-
- companion object {
- private const val DESKTOP_BROWSER_URL = "https://duckduckgo.com/browser?origin=funnel_appsettings_android"
-
- private const val GET_DESKTOP_BROWSER_SOURCE_NO_THANKS = "no_thanks"
- }
-}
diff --git a/app/src/main/java/com/duckduckgo/app/settings/GetDesktopBrowserCompleteSetupSettings.kt b/app/src/main/java/com/duckduckgo/app/settings/GetDesktopBrowserCompleteSetupSettings.kt
index 19d3198cd76a..a021d92ca801 100644
--- a/app/src/main/java/com/duckduckgo/app/settings/GetDesktopBrowserCompleteSetupSettings.kt
+++ b/app/src/main/java/com/duckduckgo/app/settings/GetDesktopBrowserCompleteSetupSettings.kt
@@ -27,7 +27,6 @@ import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.duckduckgo.anvil.annotations.PriorityKey
import com.duckduckgo.app.browser.R
-import com.duckduckgo.app.desktopbrowser.GetDesktopBrowserActivityParams
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.statistics.pixels.Pixel
@@ -99,9 +98,7 @@ class GetDesktopBrowserCompleteSetupSettings @Inject constructor(
val intent = globalActivityStarter.startIntent(
activity,
- GetDesktopBrowserActivityParams(
- source = GetDesktopBrowserActivityParams.Source.COMPLETE_SETUP,
- ),
+ SettingsDesktopBrowserPromotionParams.forCompleteSetupCard(),
) ?: return@launch
setOnClickListener {
diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt
index c4dcd61120ed..f8d4192878e3 100644
--- a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt
+++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt
@@ -37,7 +37,6 @@ import com.duckduckgo.app.browser.BrowserActivity
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.databinding.ActivitySettingsNewBinding
import com.duckduckgo.app.browser.mode.InAppNavigation
-import com.duckduckgo.app.desktopbrowser.GetDesktopBrowserActivityParams
import com.duckduckgo.app.email.ui.EmailProtectionUnsupportedScreenNoParams
import com.duckduckgo.app.firebutton.DataClearingSettingsScreenNoParams
import com.duckduckgo.app.generalsettings.GeneralSettingsScreenNoParams
@@ -554,7 +553,7 @@ class SettingsActivity : DuckDuckGoActivity() {
is LaunchOtherPlatforms -> launchActivityAndFinish(
BrowserActivity.intent(context = this, launchSource = InAppNavigation, queryExtra = OTHER_PLATFORMS_URL),
)
- is Command.LaunchGetDesktopBrowser -> launchScreen(GetDesktopBrowserActivityParams(source = GetDesktopBrowserActivityParams.Source.OTHER))
+ is Command.LaunchGetDesktopBrowser -> launchScreen(SettingsDesktopBrowserPromotionParams.forSettingsListItem())
is Command.LaunchWhatsNew -> launchScreen(ModalSurfaceActivityFromMessageId(it.messageId, it.messageType, launchedFromSettings = true))
}
}
diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsDesktopBrowserPromotionHandler.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsDesktopBrowserPromotionHandler.kt
new file mode 100644
index 000000000000..0c909ada5fd9
--- /dev/null
+++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsDesktopBrowserPromotionHandler.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.app.settings
+
+import com.duckduckgo.app.pixels.AppPixelName
+import com.duckduckgo.app.settings.GetDesktopBrowserCompleteSetupSettings.Companion.GET_DESKTOP_BROWSER_SOURCE_PIXEL_PARAM
+import com.duckduckgo.app.settings.db.SettingsDataStore
+import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionInteractionHandler
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionInteractionHandler.Interaction
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionParams
+import com.duckduckgo.desktopapppromotion.api.PixelConfig
+import com.duckduckgo.desktopapppromotion.api.PixelFireSpec
+import com.duckduckgo.di.scopes.AppScope
+import com.squareup.anvil.annotations.ContributesMultibinding
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+/**
+ * Once the user has engaged with the promo screen at all — dismissed it, copied the link or shared it
+ * — the "Complete your setup" card has done its job and stops being offered. Every interaction sets
+ * the same flag, which is how the three separate paths behaved before this screen was shared.
+ */
+@ContributesMultibinding(AppScope::class)
+class SettingsDesktopBrowserPromotionHandler @Inject constructor(
+ private val settingsDataStore: SettingsDataStore,
+ private val dispatchers: DispatcherProvider,
+) : DesktopAppPromotionInteractionHandler {
+
+ override val handlerId: String = HANDLER_ID
+
+ override suspend fun onInteraction(interaction: Interaction) {
+ withContext(dispatchers.io()) {
+ settingsDataStore.getDesktopBrowserSettingDismissed = true
+ }
+ }
+
+ companion object {
+ const val HANDLER_ID = "settings_desktop_browser"
+ }
+}
+
+/**
+ * The promo screen's default copy is this screen's copy, so Settings supplies only the parts that are
+ * genuinely its own: the attributed URL, whether the dismiss button is offered, its pixels, and the
+ * handler that persists the dismissal.
+ */
+object SettingsDesktopBrowserPromotionParams {
+
+ fun forCompleteSetupCard(): DesktopAppPromotionParams = DesktopAppPromotionParams(
+ downloadUrl = DOWNLOAD_URL,
+ showDismissButton = true,
+ pixels = PixelConfig(
+ shareClicked = PixelFireSpec(AppPixelName.GET_DESKTOP_BROWSER_SHARE_DOWNLOAD_LINK_CLICK.pixelName),
+ linkClicked = PixelFireSpec(AppPixelName.GET_DESKTOP_BROWSER_LINK_CLICK.pixelName),
+ dismissed = PixelFireSpec(
+ AppPixelName.GET_DESKTOP_BROWSER_DISMISSED.pixelName,
+ hashMapOf(GET_DESKTOP_BROWSER_SOURCE_PIXEL_PARAM to SOURCE_NO_THANKS),
+ ),
+ ),
+ handlerId = SettingsDesktopBrowserPromotionHandler.HANDLER_ID,
+ )
+
+ fun forSettingsListItem(): DesktopAppPromotionParams = DesktopAppPromotionParams(
+ downloadUrl = DOWNLOAD_URL,
+ showDismissButton = false,
+ pixels = PixelConfig(
+ shareClicked = PixelFireSpec(AppPixelName.GET_DESKTOP_BROWSER_SHARE_DOWNLOAD_LINK_CLICK.pixelName),
+ linkClicked = PixelFireSpec(AppPixelName.GET_DESKTOP_BROWSER_LINK_CLICK.pixelName),
+ ),
+ handlerId = SettingsDesktopBrowserPromotionHandler.HANDLER_ID,
+ )
+
+ private const val DOWNLOAD_URL = "https://duckduckgo.com/browser?origin=funnel_appsettings_android"
+ private const val SOURCE_NO_THANKS = "no_thanks"
+}
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index bbf89b52b41e..ba3eab25dce1 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -213,13 +213,7 @@
Вземете браузъра за работен плот
DuckDuckGo за Mac и Windows
- Защитете личната си информация и на Mac, и на Windows!
- За да изтеглите DuckDuckGo за Mac или Windows, посетете:
- duckduckgo.com/browser
- Споделяне на връзка за изтегляне
- Не, благодаря
Скрий
- Връзката е копирана
Защита от изскачащи прозорци за бисквитки
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index e2bbc6b658af..686396efa1e6 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -215,13 +215,7 @@
Nainstalovat prohlížeč pro počítač
DuckDuckGo pro Mac a Windows
- Chraň své osobní údaje i na Macu a Windows!
- DuckDuckGo pro Mac nebo Windows si můžeš stáhnout na:
- duckduckgo.com/browser
- Sdílet odkaz ke stažení
- Ne, děkuji
Skrýt
- Odkaz se zkopíroval
Ochrana před vyskakovacími okny ohledně cookies
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index aa1eacb53c5f..1c6da669d89b 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -213,13 +213,7 @@
Hent browser til computeren
DuckDuckGo til Mac og Windows
- Beskyt dine personlige oplysninger, også på Mac og Windows!
- For at downloade DuckDuckGo på Mac eller Windows, skal du besøge:
- duckduckgo.com/browser
- Del downloadlink
- Nej tak
Skjul
- Link kopieret
Beskyttelse mod pop op-beskeder om cookies
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index b338322d09da..3a994f779709 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -213,13 +213,7 @@
Desktop-Browser herunterladen
DuckDuckGo für Mac und Windows
- Schütze deine persönlichen Daten auch auf Mac und Windows!
- Um DuckDuckGo auf Mac oder Windows herunterzuladen, besuche:
- duckduckgo.com/browser
- Download-Link teilen
- Nein, danke
Ausblenden
- Link kopiert
Cookie-Pop-up-Schutz
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index f63c047594c2..7a31999a1100 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -213,13 +213,7 @@
Αποκτήστε το πρόγραμμα περιήγησης για υπολογιστές
DuckDuckGo για Mac και Windows
- Προστατέψτε τα προσωπικά στοιχεία σας τόσο σε Mac όσο και σε Windows!
- Για να κάνετ ελήψη του DuckDuckGo σε Mac ή Windows, επισκεφθείτε τη διεύθυνση:
- duckduckgo.com/browser
- Κοινή χρήση συνδέσμου λήψης
- Όχι, ευχαριστώ
Αποκρυψη
- Ο σύνδεσμος αντιγράφηκε
Προστασία αναδυόμενων παραθύρων για cookies
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 8be61f3878d1..12d88179a527 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -213,13 +213,7 @@
Obtén el navegador de escritorio
DuckDuckGo para Mac y Windows
- ¡Protege tu información personal en Mac y Windows también!
- Para descargar DuckDuckGo en Mac o Windows, visita:
- duckduckgo.com/browser
- Compartir enlace de descarga
- No, gracias
Ocultar
- Enlace copiado
Protección contra ventanas emergentes de cookies
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index 9b6cb0bec115..e889f092c889 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -213,13 +213,7 @@
Hangi töölaua brauser
DuckDuckGo Maci ja Windowsi jaoks
- Kaitse oma isikuandmeid nii Macis kui ka Windowsis!
- DuckDuckGo allalaadimiseks Macile või Windowsile külasta:
- duckduckgo.com/browser
- Jaga allalaadimise linki
- Ei, aitäh
Peida
- Link kopeeritud
Küpsiste hüpikakna kaitse
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index ac8d0da2055b..07bc899262b4 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -213,13 +213,7 @@
Hanki pöytäkoneselain
DuckDuckGo Mac- ja Windows-laitteeseen
- Suojaa henkilökohtaiset tietosi myös Mac- ja Windows-laitteessa!
- Lataa DuckDuckGo Mac- ja Windows-laitteeseen osoitteesta:
- duckduckgo.com/browser
- Jaa latauslinkki
- Ei kiitos
Piilota
- Linkki kopioitu
Evästeiden ponnahdusikkuna -suojaus
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index b1003c3e14a7..4f06ef7f0b35 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -213,13 +213,7 @@
Télécharger le navigateur de bureau
DuckDuckGo pour Mac et Windows
- Protégez aussi vos informations personnelles sur Mac et Windows !
- Pour télécharger DuckDuckGo sur Mac ou Windows, visitez :
- duckduckgo.com/browser
- Partager le lien de téléchargement
- Non merci
Masquer
- Lien copié
Protection contre les fenêtres contextuelles des cookies
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index ea4d45df7db8..f45147a691fc 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -215,13 +215,7 @@
Nabavi preglednik za PC
DuckDuckGo za Mac i Windows
- Zaštiti svoje osobne podatke na Macu i Windowsima!
- Za preuzimanje DuckDuckGo na Mac ili Windows, posjeti:
- duckduckgo.com/browser
- Podijeli poveznicu za preuzimanje
- Ne, hvala
Sakrij
- Poveznica je kopirana
Zaštita od skočnih prozora kolačića
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 621e789c2d54..2dac289be690 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -213,13 +213,7 @@
Asztali böngésző letöltése
DuckDuckGo Mac és Windows rendszerre
- Védd a személyes adataidat Mac és Windows rendszeren is!
- A DuckDuckGo Mac vagy Windows rendszerre történő letöltéséhez látogass el ide:
- duckduckgo.com/browser
- Letöltési link megosztása
- Nem, köszönöm
Elrejtés
- Link másolva
Felugró sütiablak elleni védelem
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index dc2f9b98654b..b957683a7ed1 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -213,13 +213,7 @@
Scarica il browser per desktop
DuckDuckGo per Mac e Windows
- Proteggi i tuoi dati personali anche su Mac e Windows.
- Per scaricare DuckDuckGo su Mac o Windows, visita:
- duckduckgo.com/browser
- Condividi link per il download
- No, grazie
Nascondi
- Link copiato
Protezione pop-up dei cookie
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index 40c6a8d6f913..6b82b1c94282 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -215,13 +215,7 @@
Gaukite kompiuterio naršyklę
„DuckDuckGo“, skirta „Mac“ ir „Windows“
- Apsaugok asmeninę informaciją ir „Mac“ bei „Windows“ kompiuteriuose!
- Kad atsisiųstum „DuckDuckGo“ į „Mac“ arba „Windows“, apsilankyk:
- duckduckgo.com/browser
- Bendrinti atsisiuntimo nuorodą
- Ne, dėkoju
Slėpti
- Nuoroda nukopijuota
Apsauga nuo slapukų iškylančiųjų langų
diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml
index a42ddb13ceed..23df74db35e2 100644
--- a/app/src/main/res/values-lv/strings.xml
+++ b/app/src/main/res/values-lv/strings.xml
@@ -214,13 +214,7 @@
Iegūsti galddatora pārlūku
DuckDuckGo operētājsistēmām Mac un Windows
- Aizsargā savu personisko informāciju arī Mac un Windows!
- Lai lejupielādētu DuckDuckGo operētājsistēmā Mac vai Windows, apmeklē:
- duckduckgo.com/browser
- Kopīgot lejupielādes saiti
- Nē, paldies
Paslēpt
- Saite nokopēta
Sīkfailu uznirstošo logu aizsardzība
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml
index d8ea7bd98690..11c0efbc9f6d 100644
--- a/app/src/main/res/values-nb/strings.xml
+++ b/app/src/main/res/values-nb/strings.xml
@@ -213,13 +213,7 @@
Skaff deg nettleseren for datamaskin
DuckDuckGo for Mac og Windows
- Beskytt personopplysningene dine på Mac og Windows også!
- For å laste ned DuckDuckGo på Mac eller Windows kan du gå til:
- duckduckgo.com/browser
- Del nedlastingslenke
- Nei takk
Skjul
- Lenken er kopiert
Beskyttelse mot popup-vinduer om informasjonskapsler
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 8e03dd9bcbb1..823ccc9a7b72 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -213,13 +213,7 @@
Download de desktopbrowser
DuckDuckGo voor Mac en Windows
- Bescherm je persoonlijke informatie ook op Mac en Windows!
- Om DuckDuckGo te downloaden op Mac of Windows, bezoek:
- duckduckgo.com/browser
- Downloadlink delen
- Nee, bedankt
Verbergen
- Link gekopieerd
Bescherming tegen cookiepop-ups
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 936867c5f0a4..a3e7cfe45329 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -215,13 +215,7 @@
Pobierz przeglądarkę komputerową
DuckDuckGo dla komputerów Mac i Windows
- Chroń swoje dane osobowe także na Macu i Windowsie!
- Aby pobrać DuckDuckGo na komputer Mac lub Windows, odwiedź stronę:
- duckduckgo.com/browser
- Udostępnij link pobierania
- Nie, dziękuję
Ukryj
- Skopiowano łącze
Ochrona przed wyskakującymi okienkami dotyczącymi plików cookie
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 8c0ca87bbf99..ce7e248f9691 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -213,13 +213,7 @@
Obter navegador para computador
DuckDuckGo para Mac e Windows
- Protege a tua informação pessoal também no Mac e Windows!
- Para transferir o DuckDuckGo no Mac ou Windows, visita:
- duckduckgo.com/browser
- Partilhar link de transferência
- Não, obrigado
Ocultar
- Link copiado
Proteção contra pop-ups de cookies
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 6c040157d04a..523cfba77370 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -214,13 +214,7 @@
Obține browserul pentru desktop
DuckDuckGo pentru Mac și Windows
- Protejează-ți informațiile personale și pe Mac și Windows!
- Pentru a descărca DuckDuckGo pe Mac sau Windows, vizitează:
- duckduckgo.com/browser
- Trimite linkul de descărcare
- Nu, mulțumesc
Ascunde
- Link copiat
Protecție pentru ferestre pop-up aferente modulelor cookie
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 7cc84be06a1f..c716361699f0 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -215,13 +215,7 @@
Скачать настольный браузер
DuckDuckGo для Mac и Windows
- Защитите личную информацию на Mac и Windows!
- Скачивайте DuckDuckGo:
- duckduckgo.com/browser
- Поделиться ссылкой для загрузки
- Нет, спасибо
Скрыть
- Ссылка скопирована
Защита от всплывающих окон куки
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 52811ca67690..69a2f83b0419 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -215,13 +215,7 @@
Získajte prehliadač pre desktop PC
DuckDuckGo pre Mac a Windows
- Chráň si svoje osobné údaje na Macu aj vo Windows!
- Ak si chceš stiahnuť aplikáciu DuckDuckGo v systéme Mac alebo Windows, navštív túto stránku:
- duckduckgo.com/browser
- Zdieľať odkaz na stiahnutie
- Nie, ďakujem
Skryť
- Odkaz bol skopírovaný
Ochrana proti automatickému otváraniu okien o súboroch cookie
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index ae594b3c9a34..57d9cf10c446 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -215,13 +215,7 @@
Namesti namizni brskalnik
DuckDuckGo za računalnike Mac in Windows
- Zaščitite svoje osebne podatke tudi v računalnikih Mac in Windows!
- Če želite prenesti DuckDuckGo v računalniku Mac ali Windows, obiščite:
- duckduckgo.com/browser
- Deli povezavo za prenos
- Ne, hvala
Skrij se
- Povezava je kopirana
Zaščita pred pojavnimi okni piškotkov
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 9773664a15f6..6d4421a1807c 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -213,13 +213,7 @@
Hämta webbläsare för dator
DuckDuckGo för Mac och Windows
- Skydda din personliga information på Mac och Windows också!
- För att ladda ner DuckDuckGo på Mac eller Windows, besök:
- duckduckgo.com/browser
- Dela nedladdningslänk
- Nej tack
Dölj
- Länken har kopierats
Skydd mot popup-fönster för cookies
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 9d632a8c4434..f99372ab3ff6 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -213,13 +213,7 @@
Masaüstü Tarayıcısını Edinin
Mac ve Windows için DuckDuckGo
- Mac ve Windows\'ta da kişisel bilgilerinizi koruyun!
- DuckDuckGo\'yu Mac veya Windows\'ta indirmek için şu adresi ziyaret edin:
- duckduckgo.com/browser
- İndirme Bağlantısını Paylaş
- Hayır Teşekkürler
Gizle
- Bağlantı kopyalandı
Çerez Açılır Pencere Engelleyici
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e307d3a367a4..bbcdf49b9fa3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -212,13 +212,7 @@
Get Desktop Browser
DuckDuckGo for Mac and Windows
- Protect your personal info on Mac and Windows too!
- To download DuckDuckGo on Mac or Windows, visit:
- duckduckgo.com/browser
- Share Download Link
- No Thanks
Hide
- Link copied
Cookie Pop-Up Protection
diff --git a/app/src/test/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserViewModelTest.kt
deleted file mode 100644
index 8e934be9b53f..000000000000
--- a/app/src/test/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserViewModelTest.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * 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.app.desktopbrowser
-
-import app.cash.turbine.test
-import com.duckduckgo.app.clipboard.ClipboardInteractor
-import com.duckduckgo.app.desktopbrowser.GetDesktopBrowserActivityParams.Source
-import com.duckduckgo.app.desktopbrowser.GetDesktopBrowserViewModel.Command
-import com.duckduckgo.app.pixels.AppPixelName
-import com.duckduckgo.app.settings.db.SettingsDataStore
-import com.duckduckgo.app.statistics.pixels.Pixel
-import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Count
-import com.duckduckgo.common.test.CoroutineTestRule
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Rule
-import org.junit.Test
-import org.mockito.kotlin.any
-import org.mockito.kotlin.eq
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyNoInteractions
-import org.mockito.kotlin.whenever
-
-class GetDesktopBrowserViewModelTest {
-
- @get:Rule
- val coroutineTestRule = CoroutineTestRule()
-
- private val settingsDataStoreMock: SettingsDataStore = mock()
- private val clipboardInteractorMock: ClipboardInteractor = mock()
- private val pixelMock: Pixel = mock()
-
- private var testee: GetDesktopBrowserViewModel = GetDesktopBrowserViewModel(
- params = GetDesktopBrowserActivityParams(Source.COMPLETE_SETUP),
- settingsDataStore = settingsDataStoreMock,
- dispatchers = coroutineTestRule.testDispatcherProvider,
- clipboardInteractor = clipboardInteractorMock,
- pixel = pixelMock,
- )
-
- @Test
- fun whenSourceIsCompleteSetupThenShowNoThanksButtonIsTrue() = runTest {
- testee.viewState.test {
- val viewState = awaitItem()
- assertTrue(viewState.showNoThanksButton)
- }
- }
-
- @Test
- fun whenSourceIsOtherThenShowNoThanksButtonIsFalse() = runTest {
- testee = GetDesktopBrowserViewModel(
- params = GetDesktopBrowserActivityParams(Source.OTHER),
- settingsDataStore = settingsDataStoreMock,
- dispatchers = coroutineTestRule.testDispatcherProvider,
- clipboardInteractor = clipboardInteractorMock,
- pixel = pixelMock,
- )
-
- testee.viewState.test {
- val viewState = awaitItem()
- assertFalse(viewState.showNoThanksButton)
- }
- }
-
- @Test
- fun whenOnShareDownloadLinkClickedThenEmitShareDownloadLinkCommand() = runTest {
- testee.commands.test {
- testee.onShareDownloadLinkClicked()
-
- val command = awaitItem() as Command.ShareDownloadLink
- assertEquals("https://duckduckgo.com/browser?origin=funnel_appsettings_android", command.url)
- }
- }
-
- @Test
- fun whenOnNoThanksClickedThenSetDataStoreFlagAndEmitDismissedCommand() = runTest {
- testee.commands.test {
- testee.onNoThanksClicked()
-
- verify(settingsDataStoreMock).getDesktopBrowserSettingDismissed = true
- val command = awaitItem()
- assertEquals(Command.Dismissed, command)
- }
- }
-
- @Test
- fun whenOnBackPressedThenEmitCloseCommand() = runTest {
- testee.commands.test {
- testee.onBackPressed()
-
- val command = awaitItem()
-
- verifyNoInteractions(settingsDataStoreMock)
- assertEquals(Command.Close, command)
- }
- }
-
- @Test
- fun whenOnLinkClickedAndSystemShowsNotificationThenDoNotEmitShowCopiedNotificationCommand() = runTest {
- whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(true)
-
- testee.commands.test {
- testee.onLinkClicked()
- expectNoEvents()
- }
- }
-
- @Test
- fun whenOnLinkClickedAndSystemDoesNotShowNotificationThenEmitShowCopiedNotificationCommand() = runTest {
- whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(false)
-
- testee.commands.test {
- testee.onLinkClicked()
-
- val command = awaitItem()
- assertEquals(Command.ShowCopiedNotification, command)
- }
- }
-
- @Test
- fun whenOnShareDownloadLinkClickedThenPixelIsFired() = runTest {
- testee.commands.test {
- testee.onShareDownloadLinkClicked()
- awaitItem()
- }
-
- verify(pixelMock).fire(AppPixelName.GET_DESKTOP_BROWSER_SHARE_DOWNLOAD_LINK_CLICK)
- }
-
- @Test
- fun whenOnNoThanksClickedThenPixelIsFired() = runTest {
- testee.commands.test {
- testee.onNoThanksClicked()
- awaitItem()
- }
-
- verify(pixelMock).fire(
- eq(AppPixelName.GET_DESKTOP_BROWSER_DISMISSED),
- eq(mapOf("source" to "no_thanks")),
- eq(emptyMap()),
- eq(Count),
- )
- }
-
- @Test
- fun whenOnLinkClickedThenSettingIsDismissed() = runTest {
- whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(true)
-
- testee.onLinkClicked()
-
- verify(settingsDataStoreMock).getDesktopBrowserSettingDismissed = true
- }
-
- @Test
- fun whenOnLinkClickedThenPixelIsFired() = runTest {
- whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(true)
-
- testee.onLinkClicked()
-
- verify(pixelMock).fire(AppPixelName.GET_DESKTOP_BROWSER_LINK_CLICK)
- }
-}
diff --git a/app/src/test/java/com/duckduckgo/app/settings/SettingsDesktopBrowserPromotionHandlerTest.kt b/app/src/test/java/com/duckduckgo/app/settings/SettingsDesktopBrowserPromotionHandlerTest.kt
new file mode 100644
index 000000000000..905feab12cbb
--- /dev/null
+++ b/app/src/test/java/com/duckduckgo/app/settings/SettingsDesktopBrowserPromotionHandlerTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.app.settings
+
+import com.duckduckgo.app.settings.db.SettingsDataStore
+import com.duckduckgo.common.test.CoroutineTestRule
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionInteractionHandler.Interaction
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+class SettingsDesktopBrowserPromotionHandlerTest {
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ private val settingsDataStoreMock: SettingsDataStore = mock()
+
+ private val testee = SettingsDesktopBrowserPromotionHandler(
+ settingsDataStore = settingsDataStoreMock,
+ dispatchers = coroutineTestRule.testDispatcherProvider,
+ )
+
+ @Test
+ fun whenLinkCopiedThenSettingIsDismissed() = runTest {
+ testee.onInteraction(Interaction.LINK_COPIED)
+
+ verify(settingsDataStoreMock).getDesktopBrowserSettingDismissed = true
+ }
+
+ @Test
+ fun whenShareCompletedThenSettingIsDismissed() = runTest {
+ testee.onInteraction(Interaction.SHARE_COMPLETED)
+
+ verify(settingsDataStoreMock).getDesktopBrowserSettingDismissed = true
+ }
+
+ @Test
+ fun whenDismissedThenSettingIsDismissed() = runTest {
+ testee.onInteraction(Interaction.DISMISSED)
+
+ verify(settingsDataStoreMock).getDesktopBrowserSettingDismissed = true
+ }
+}
diff --git a/app/src/test/java/com/duckduckgo/app/settings/SettingsDesktopBrowserPromotionParamsTest.kt b/app/src/test/java/com/duckduckgo/app/settings/SettingsDesktopBrowserPromotionParamsTest.kt
new file mode 100644
index 000000000000..35bd7d1639ad
--- /dev/null
+++ b/app/src/test/java/com/duckduckgo/app/settings/SettingsDesktopBrowserPromotionParamsTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.app.settings
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+/**
+ * Pixel names reach the shared promo screen as plain strings, so these assert the exact wire names
+ * and parameters this entry point fired before the screens were consolidated.
+ */
+class SettingsDesktopBrowserPromotionParamsTest {
+
+ @Test
+ fun whenLaunchedFromCompleteSetupCardThenDismissButtonIsOfferedAndDismissPixelIsConfigured() {
+ val params = SettingsDesktopBrowserPromotionParams.forCompleteSetupCard()
+
+ assertTrue(params.showDismissButton)
+ assertEquals("m_get_desktop_browser_dismissed", params.pixels.dismissed?.pixelName)
+ assertEquals(mapOf("source" to "no_thanks"), params.pixels.dismissed?.parameters)
+ }
+
+ @Test
+ fun whenLaunchedFromSettingsListItemThenNoDismissButtonAndNoDismissPixel() {
+ val params = SettingsDesktopBrowserPromotionParams.forSettingsListItem()
+
+ assertFalse(params.showDismissButton)
+ assertNull(params.pixels.dismissed)
+ }
+
+ @Test
+ fun whenLaunchedFromEitherEntryPointThenShareAndLinkPixelsAreUnchanged() {
+ listOf(
+ SettingsDesktopBrowserPromotionParams.forCompleteSetupCard(),
+ SettingsDesktopBrowserPromotionParams.forSettingsListItem(),
+ ).forEach { params ->
+ assertEquals("m_get_desktop_browser_share_download_link_click", params.pixels.shareClicked?.pixelName)
+ assertEquals("m_get_desktop_browser_link_click", params.pixels.linkClicked?.pixelName)
+ }
+ }
+
+ @Test
+ fun whenLaunchedFromEitherEntryPointThenNoImpressionPixelIsConfigured() {
+ // The impression pixel belongs to the Settings card, which fires it before this screen opens.
+ assertNull(SettingsDesktopBrowserPromotionParams.forCompleteSetupCard().pixels.impression)
+ assertNull(SettingsDesktopBrowserPromotionParams.forSettingsListItem().pixels.impression)
+ }
+
+ @Test
+ fun whenLaunchedFromEitherEntryPointThenAttributedUrlIsUnchanged() {
+ listOf(
+ SettingsDesktopBrowserPromotionParams.forCompleteSetupCard(),
+ SettingsDesktopBrowserPromotionParams.forSettingsListItem(),
+ ).forEach { params ->
+ assertEquals("https://duckduckgo.com/browser?origin=funnel_appsettings_android", params.downloadUrl)
+ }
+ }
+
+ @Test
+ fun whenLaunchedFromEitherEntryPointThenTheSettingsHandlerPersistsTheDismissal() {
+ listOf(
+ SettingsDesktopBrowserPromotionParams.forCompleteSetupCard(),
+ SettingsDesktopBrowserPromotionParams.forSettingsListItem(),
+ ).forEach { params ->
+ assertEquals(SettingsDesktopBrowserPromotionHandler.HANDLER_ID, params.handlerId)
+ }
+ }
+
+ @Test
+ fun whenLaunchedFromSettingsThenCopyIsLeftToThePromoScreenDefaults() {
+ val params = SettingsDesktopBrowserPromotionParams.forCompleteSetupCard()
+
+ assertNull(params.toolbarTitle)
+ assertNull(params.title)
+ assertNull(params.body)
+ assertEquals(0, params.illustration)
+ }
+}
diff --git a/autofill/autofill-impl/build.gradle b/autofill/autofill-impl/build.gradle
index 3c6459f328c5..15726a8a4817 100644
--- a/autofill/autofill-impl/build.gradle
+++ b/autofill/autofill-impl/build.gradle
@@ -44,6 +44,7 @@ dependencies {
testImplementation project(path: ':autofill-test')
implementation project(path: ':sync-api')
implementation project(path: ':navigation-api')
+ implementation project(path: ':desktop-app-promotion-api')
implementation project(':user-agent-api')
implementation project(':new-tab-page-api')
implementation project(':data-store-api')
diff --git a/autofill/autofill-impl/src/main/AndroidManifest.xml b/autofill/autofill-impl/src/main/AndroidManifest.xml
index ae63c6af166f..c41fc087f28e 100644
--- a/autofill/autofill-impl/src/main/AndroidManifest.xml
+++ b/autofill/autofill-impl/src/main/AndroidManifest.xml
@@ -9,10 +9,6 @@
android:name=".ui.credential.management.importpassword.ImportPasswordsActivity"
android:label="@string/autofillManagementHowToSyncDesktopPasswords"
android:exported="false" />
-
launchSharePageChooser(command.link)
- is ShowCopiedNotification -> showCopiedNotification()
- }
- }
-
- private fun showCopiedNotification() {
- Snackbar.make(binding.root, R.string.autofillManagementImportPasswordsGetDesktopBrowserLinkCopied, Toast.LENGTH_SHORT).show()
- }
-
- @SuppressLint("UnspecifiedImmutableFlag")
- private fun launchSharePageChooser(link: String) {
- val share = Intent(Intent.ACTION_SEND).apply {
- type = "text/plain"
- val message = getString(R.string.autofillManagementImportPasswordsGetDesktopBrowserIntentMessage, link)
- putExtra(Intent.EXTRA_TEXT, message)
- putExtra(Intent.EXTRA_TITLE, getString(R.string.autofillManagementImportPasswordsGetDesktopBrowserIntentTitle))
- }
-
- try {
- startActivity(Intent.createChooser(share, null))
- } catch (e: ActivityNotFoundException) {
- logcat(WARN) { "Activity not found: ${e.asLog()}" }
- }
- }
-}
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/desktopapp/ImportPasswordsGetDesktopAppViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/desktopapp/ImportPasswordsGetDesktopAppViewModel.kt
deleted file mode 100644
index c3978b7cd16f..000000000000
--- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/desktopapp/ImportPasswordsGetDesktopAppViewModel.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (c) 2023 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.autofill.impl.ui.credential.management.importpassword.desktopapp
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.duckduckgo.anvil.annotations.ContributesViewModel
-import com.duckduckgo.app.statistics.pixels.Pixel
-import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_COPIED_DESKTOP_LINK
-import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_SHARED_DESKTOP_LINK
-import com.duckduckgo.autofill.impl.ui.credential.management.AutofillClipboardInteractor
-import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.desktopapp.ImportPasswordsGetDesktopAppViewModel.Command.ShareLink
-import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.desktopapp.ImportPasswordsGetDesktopAppViewModel.Command.ShowCopiedNotification
-import com.duckduckgo.common.utils.DispatcherProvider
-import com.duckduckgo.di.scopes.AppScope
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@ContributesViewModel(AppScope::class)
-class ImportPasswordsGetDesktopAppViewModel @Inject constructor(
- private val pixel: Pixel,
- private val dispatchers: DispatcherProvider,
- private val autofillClipboardInteractor: AutofillClipboardInteractor,
-) : ViewModel() {
-
- private val commandChannel = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
- val commands = commandChannel.receiveAsFlow()
-
- sealed class Command {
- data class ShareLink(val link: String) : Command()
- data object ShowCopiedNotification : Command()
- }
-
- data class ViewState(val windowsFeatureEnabled: Boolean)
-
- fun onShareClicked() {
- viewModelScope.launch {
- commandChannel.send(ShareLink(buildLink()))
-
- pixel.fire(AUTOFILL_IMPORT_PASSWORDS_SHARED_DESKTOP_LINK)
- }
- }
-
- fun onLinkClicked() {
- viewModelScope.launch(dispatchers.io()) {
- autofillClipboardInteractor.copyToClipboard(buildLink(), isSensitive = false)
-
- if (autofillClipboardInteractor.shouldShowCopyNotification()) {
- commandChannel.send(ShowCopiedNotification)
- }
-
- pixel.fire(AUTOFILL_IMPORT_PASSWORDS_COPIED_DESKTOP_LINK)
- }
- }
-
- private fun buildLink(): String {
- return "$BASE_LINK?$ATTRIBUTION"
- }
-
- companion object {
- private const val BASE_LINK = "https://duckduckgo.com/browser"
- private const val ATTRIBUTION = "origin=funnel_browser_android_sync"
- }
-}
diff --git a/autofill/autofill-impl/src/main/res/layout/activity_get_desktop_app.xml b/autofill/autofill-impl/src/main/res/layout/activity_get_desktop_app.xml
deleted file mode 100644
index c8a0e56d14ca..000000000000
--- a/autofill/autofill-impl/src/main/res/layout/activity_get_desktop_app.xml
+++ /dev/null
@@ -1,116 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/autofill/autofill-impl/src/main/res/values-bg/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-bg/strings-autofill-impl.xml
index d5142971cb4a..1cd251729c36 100644
--- a/autofill/autofill-impl/src/main/res/values-bg/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-bg/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Как да синхронизирате паролите от работния плот
Импортирайте пароли в десктоп версията на браузъра DuckDuckGo, след което ги синхронизирайте с различни устройства.
Вземете браузъра за работен плот
- Връзката е копирана
Вземете браузъра DuckDuckGo за Mac или Windows
Търсете поверително и блокирайте тракерите с браузъра за настолен компютър DuckDuckGo. Посетете тази връзка на своя компютър, за да го изтеглите още днес.\n\n%1$s
Синхронизиране с браузъра на работния плот
@@ -215,8 +214,6 @@
Изтеглете приложението за настолен компютър
Вземете DuckDuckGo за Mac или Windows
На компютъра си отидете на:
- duckduckgo.com/browser
- Споделяне на връзка за изтегляне
Докладване на проблем с автоматичното попълване
Анонимно докладвайте, че автоматичното попълване не работи на този сайт. Паролите никога няма да бъдат споделяни.
diff --git a/autofill/autofill-impl/src/main/res/values-cs/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-cs/strings-autofill-impl.xml
index ee03a5603516..813326e6c7eb 100644
--- a/autofill/autofill-impl/src/main/res/values-cs/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-cs/strings-autofill-impl.xml
@@ -214,7 +214,6 @@
Jak synchronizovat hesla na počítači
Importuj hesla do desktopové verze prohlížeče DuckDuckGo a potom je synchronizuj mezi zařízeními.
Nainstalovat prohlížeč pro počítač
- Odkaz se zkopíroval
Nainstaluj si prohlížeč DuckDuckGo pro Mac nebo Windows
Vyhledávej soukromě a blokuj trackery pomocí prohlížeče DuckDuckGo pro počítače. Stáhneš si ho z tohohle odkazu.\n\n%1$s
Synchronizovat s počítačem
@@ -227,8 +226,6 @@
Získat aplikaci pro počítač
Nainstaluj si DuckDuckGo pro Mac nebo Windows
V počítači přejdi na:
- duckduckgo.com/browser
- Sdílet odkaz ke stažení
Nahlásit problém s automatickým vyplňováním
Anonymně můžeš nahlásit, že automatické vyplňování na tomhle webu nefunguje. Hesla nikdy nesdílíme.
diff --git a/autofill/autofill-impl/src/main/res/values-da/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-da/strings-autofill-impl.xml
index 06af76f11b70..01319cfb3ed5 100644
--- a/autofill/autofill-impl/src/main/res/values-da/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-da/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Sådan synkroniseres adgangskoder fra pc
Importer adgangskoder i computerversionen af DuckDuckGo-browseren, og synkroniser derefter på tværs af enheder.
Hent browser til computeren
- Link kopieret
Hent DuckDuckGo-browseren til Mac eller Windows
Søg privat og bloker trackere med DuckDuckGo-browseren til computeren. Besøg dette link på din computer for at downloade i dag.\n\n%1$s
Synkroniser med computer
@@ -215,8 +214,6 @@
Hent skrivebordsapp
Hent DuckDuckGo til Mac eller Windows
På din computer skal du gå til:
- duckduckgo.com/browser
- Del downloadlink
Rapporter et problem med automatisk udfyldning
Rapportér anonymt, at automatisk udfyldning ikke fungerer på dette websted. Adgangskoder deles aldrig.
diff --git a/autofill/autofill-impl/src/main/res/values-de/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-de/strings-autofill-impl.xml
index b2355a22cbbc..489e69bae0ae 100644
--- a/autofill/autofill-impl/src/main/res/values-de/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-de/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
So synchronisierst du Desktop-Passwörter
Importiere Passwörter in der Desktop-Version des DuckDuckGo-Browsers und synchronisiere sie dann auf allen Geräten.
Desktop-Browser herunterladen
- Link kopiert
Hole dir den DuckDuckGo-Browser für Mac oder Windows
Suche privat und blockiere Tracker mit dem DuckDuckGo Desktop-Browser. Besuche zum Herunterladen diesen Link auf deinem Computer.\n\n%1$s
Mit Desktop synchronisieren
@@ -215,8 +214,6 @@
Lade dir die Desktop-App herunter
Hole dir DuckDuckGo für Mac oder Windows
Gehe auf deinem Computer zu:
- duckduckgo.com/browser
- Download-Link teilen
Problem mit Autofill melden
Melde anonym, dass das automatische Ausfüllen auf dieser Seite nicht funktioniert. Passwörter werden niemals geteilt.
diff --git a/autofill/autofill-impl/src/main/res/values-el/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-el/strings-autofill-impl.xml
index f2b83a5614a9..f8dd74d581b0 100644
--- a/autofill/autofill-impl/src/main/res/values-el/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-el/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Πώς να συγχρονίσετε τους κωδικούς πρόσβασης υπολογιστών
Εισαγάγετε κωδικούς πρόσβασης στην έκδοση του προγράμματος περιήγησης DuckDuckGo για υπολογιστές και έπειτα συγχρονίστε τους σε όλες τις συσκευές.
Αποκτήστε το πρόγραμμα περιήγησης για υπολογιστές
- Ο σύνδεσμος αντιγράφηκε
Αποκτήστε το πρόγραμμα περιήγησης DuckDuckGo για Mac ή Windows
Αναζητήστε ιδιωτικά και αποκλείστε εφαρμογές παρακολούθησης με το πρόγραμμα περιήγησης DuckDuckGo για υπολογιστές. Επισκεφτείτε αυτόν τον σύνδεσμο στον υπολογιστή σας για να πραγματοποιήσετε λήψη σήμερα.\n\n%1$s
Συγχρονισμός με υπολογιστή
@@ -215,8 +214,6 @@
Αποκτήστε την εφαρμογή για υπολογιστές
Αποκτήστε το DuckDuckGo για Mac ή Windows
Στον υπολογιστή σας, μεταβείτε στη διεύθυνση:
- duckduckgo.com/browser
- Κοινή χρήση συνδέσμου λήψης
Αναφέρετε ένα πρόβλημα με την Αυτόματη συμπλήρωση
Αναφέρετε ανώνυμα ότι η αυτόματη συμπλήρωση δεν λειτουργεί σε αυτόν τον ιστότοπο. Οι κωδικοί πρόσβασης δεν κοινοποιούνται ποτέ.
diff --git a/autofill/autofill-impl/src/main/res/values-es/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-es/strings-autofill-impl.xml
index 512fbe1f1329..882e96a7ae7d 100644
--- a/autofill/autofill-impl/src/main/res/values-es/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-es/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Cómo sincronizar las contraseñas de escritorio
Importa contraseñas en la versión de escritorio del navegador DuckDuckGo y, a continuación, sincronízalas entre dispositivos.
Obtén el navegador de escritorio
- Enlace copiado
Consigue el navegador DuckDuckGo para Mac o Windows
Busca de forma privada y bloquea los rastreadores con el navegador de escritorio DuckDuckGo. Visita este enlace en tu ordenador para descargarlo hoy mismo.\n\n%1$s
Sincronizar con el escritorio
@@ -215,8 +214,6 @@
Obtener la aplicación de escritorio
Consigue DuckDuckGo para Mac o Windows
En tu ordenador, ve a:
- duckduckgo.com/browser
- Compartir enlace de descarga
Informar de un problema con la función Autocompletar
Informar de forma anónima de que la función Autocompletar no funciona en esta página. Las contraseñas nunca se comparten.
diff --git a/autofill/autofill-impl/src/main/res/values-et/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-et/strings-autofill-impl.xml
index 105f822de1e3..f1c7e82bf4d1 100644
--- a/autofill/autofill-impl/src/main/res/values-et/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-et/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Kuidas sünkroonida töölaua paroole
Impordi paroolid DuckDuckGo brauseri töölauaversiooni, seejärel sünkrooni need kõigis seadmetes.
Hangi töölaua brauser
- Link kopeeritud
Hangi DuckDuckGo brauser Maci või Windowsi jaoks
Otsi privaatselt ja blokeeri jälgurid DuckDuckGo töölaua brauseriga. Külasta oma arvutis seda linki, et laadida see alla juba täna.\n\n%1$s
Sünkrooni töölauaga
@@ -215,8 +214,6 @@
Hangi töölaua rakendus
Hangi DuckDuckGo Maci või Windowsi jaoks
Ava oma arvutis:
- duckduckgo.com/browser
- Jaga allalaadimise linki
Teata automaatse täitmise probleemist
Teata anonüümselt, et automaatne täitmine sellel saidil ei tööta. Paroole ei jagata kunagi.
diff --git a/autofill/autofill-impl/src/main/res/values-fi/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-fi/strings-autofill-impl.xml
index 762c0b227243..41636860d395 100644
--- a/autofill/autofill-impl/src/main/res/values-fi/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-fi/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Tietokoneen salasanojen synkronointi
Tuo DuckDuckGo-selaimen pöytäkoneversiossa olevat salasanat ja synkronoi eri laitteiden kanssa.
Hanki pöytäkoneselain
- Linkki kopioitu
Hanki DuckDuckGo-selain Macille tai Windowsille
Tee yksityisiä hakuja ja estä seuranta DuckDuckGo-selaimen tietokoneversiolla. Mene tietokoneella tähän linkkiin ja lataa selain jo tänään.\n\n%1$s
Synkronointi pöytäkoneen kanssa
@@ -215,8 +214,6 @@
Hanki sovelluksen tietokoneversio
Hanki DuckDuckGo Macille tai Windowsille
Siirry tietokoneellasi kohtaan:
- duckduckgo.com/browser
- Jaa latauslinkki
Ilmoita automaattisen täytön ongelmasta
Tee nimetön ilmoitus automaattisen täytön ongelmista tällä sivustolla. Salasanoja ei koskaan jaeta.
diff --git a/autofill/autofill-impl/src/main/res/values-fr/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-fr/strings-autofill-impl.xml
index b1d095654300..382a47d1ce72 100644
--- a/autofill/autofill-impl/src/main/res/values-fr/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-fr/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Comment synchroniser les mots de passe de bureau
Importez les mots de passe dans la version de bureau du navigateur DuckDuckGo, puis synchronisez-les sur tous les appareils.
Télécharger le navigateur de bureau
- Lien copié
Procurez-vous le navigateur DuckDuckGo pour Mac ou Windows
Faites une recherche privée et bloquez les traqueurs avec le navigateur de bureau DuckDuckGo. Cliquez sur ce lien sur votre ordinateur pour le télécharger dès aujourd\'hui.\n\n%1$s
Synchronisation avec le navigateur de bureau
@@ -215,8 +214,6 @@
Obtenir l\'application de bureau
Procurez-vous DuckDuckGo pour Mac ou Windows
Sur votre ordinateur, accédez à :
- duckduckgo.com/browser
- Partager le lien de téléchargement
Signaler un problème de saisie automatique
La saisie automatique du signalement ne fonctionne pas sur ce site. Les mots de passe ne sont jamais partagés.
diff --git a/autofill/autofill-impl/src/main/res/values-hr/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-hr/strings-autofill-impl.xml
index 93b1907890c3..488ca2d41e48 100644
--- a/autofill/autofill-impl/src/main/res/values-hr/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-hr/strings-autofill-impl.xml
@@ -214,7 +214,6 @@
Kako sinkronizirati lozinke za računalo
Uvezi lozinke u stolnu verziju preglednika DuckDuckGo, a zatim sinkroniziraj preko uređaja.
Nabavi preglednik za PC
- Poveznica je kopirana
Nabavi DuckDuckGo preglednik za Mac ili Windows
Privatno pretražuj i blokiraj alate za praćenje pomoću stolne verzije preglednika DuckDuckGo. Za preuzimanje, posjeti ovu poveznicu na svom računalu.\n\n%1$s
Sinkronizacija s preglednikom na PC-u
@@ -227,8 +226,6 @@
Preuzmi aplikaciju za stolna računala
Nabavi DuckDuckGo za Mac ili Windows
Na računalu idi na:
- duckduckgo.com/browser
- Podijeli poveznicu za preuzimanje
Prijavi problem s automatskim popunjavanjem
Anonimno prijavi da automatsko popunjavanje ne radi na ovom web-mjestu. Lozinke se nikada ne dijele.
diff --git a/autofill/autofill-impl/src/main/res/values-hu/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-hu/strings-autofill-impl.xml
index e98834b07a74..e6df419a6460 100644
--- a/autofill/autofill-impl/src/main/res/values-hu/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-hu/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Asztali jelszavak szinkronizálásának módja
Importálj jelszavakat a DuckDuckGo böngésző asztali verziójába, majd szinkronizáld őket az eszközök között.
Asztali böngésző letöltése
- Link másolva
Mac vagy Windows verziójú DuckDuckGo böngésző letöltése
Keress privát módon, és blokkold a nyomkövetőket a DuckDuckGo asztali böngészőjével. A letöltéshez nyisd meg ezt a linket a számítógépen.\n\n%1$s
Szinkronizálás asztali böngészővel
@@ -215,8 +214,6 @@
Asztali alkalmazás letöltése
Mac vagy Windows verziójú DuckDuckGo letöltése
A számítógépeden lépj a következő lehetőségre:
- duckduckgo.com/browser
- Letöltési link megosztása
Automatikus kitöltéssel kapcsolatos probléma jelentése
Jelentsd névtelenül, hogy az automatikus kitöltés nem működik ezen a webhelyen. Jelszavak megosztására soha nem kerül sor.
diff --git a/autofill/autofill-impl/src/main/res/values-it/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-it/strings-autofill-impl.xml
index 60d75923ae85..a8d4e3ecae14 100644
--- a/autofill/autofill-impl/src/main/res/values-it/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-it/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Come sincronizzare le password del desktop
Importa le password nella versione desktop del browser DuckDuckGo, quindi sincronizza su tutti i dispositivi.
Scarica il browser per desktop
- Link copiato
Scarica il browser DuckDuckGo per Mac o Windows
Cerca privatamente e blocca i sistemi di tracciamento con il browser per desktop DuckDuckGo. Visita questo link dal computer per scaricarlo oggi stesso.\n\n%1$s
Sincronizzazione con il desktop
@@ -215,8 +214,6 @@
Scarica l\'app per desktop
Scarica DuckDuckGo per Mac o Windows
Sul tuo computer, visita:
- duckduckgo.com/browser
- Condividi link per il download
Segnala un problema con la compilazione automatica
Segnala in forma anonima la compilazione automatica non funzionante su questo sito. Le password non vengono mai condivise.
diff --git a/autofill/autofill-impl/src/main/res/values-lt/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-lt/strings-autofill-impl.xml
index eb535fcac7ce..fb81aed405ba 100644
--- a/autofill/autofill-impl/src/main/res/values-lt/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-lt/strings-autofill-impl.xml
@@ -214,7 +214,6 @@
Kaip sinchronizuoti darbalaukio slaptažodžius
Importuokite slaptažodžius „DuckDuckGo“ naršyklės versijoje kompiuteriui, tada sinchronizuokite juos visuose įrenginiuose.
Gaukite kompiuterio naršyklę
- Nuoroda nukopijuota
Gaukite „DuckDuckGo“ naršyklę, skirtą „Mac“ arba „Windows“
Ieškokite privačiai ir blokuokite sekimo priemones naudodami „DuckDuckGo“ naršyklę kompiuteriui. Spustelėkite šią nuorodą kompiuteryje, kad atsisiųstumėte šiandien.\n\n%1$s
Sinchronizavimas su kompiuteriu
@@ -227,8 +226,6 @@
Gauti kompiuterio programą
Gaukite „DuckDuckGo“, skirtą „Mac“ arba „Windows“
Kompiuteryje eikite į:
- duckduckgo.com/browser
- Bendrinti atsisiuntimo nuorodą
Praneškite apie funkcijos „Automatinis užpildymas“ problemą
Anonimiškai praneškite apie automatinį užpildymą, kuris neveikia šioje svetainėje. Slaptažodžiai niekada nėra bendrinami.
diff --git a/autofill/autofill-impl/src/main/res/values-lv/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-lv/strings-autofill-impl.xml
index 3d42fe307765..67c71e2c8e2b 100644
--- a/autofill/autofill-impl/src/main/res/values-lv/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-lv/strings-autofill-impl.xml
@@ -208,7 +208,6 @@
Kā sinhronizēt darbvirsmas paroles
Importē paroles DuckDuckGo pārlūka galddatora versijā un pēc tam sinhronizē tās visās ierīcēs.
Iegūsti galddatora pārlūku
- Saite nokopēta
Iegūsti DuckDuckGo pārlūku Mac vai Windows datoram
Meklē privāti un bloķē izsekotājus, izmantojot DuckDuckGo pārlūku galddatoram. Apmeklē šo saiti savā datorā, lai lejupielādētu jau šodien.\n\n%1$s
Sinhronizēt ar galddatoru
@@ -221,8 +220,6 @@
Iegūt datora programmu
Iegūsti DuckDuckGo Mac vai Windows datoram
Savā datorā dodies uz:
- duckduckgo.com/browser
- Kopīgot lejupielādes saiti
Ziņot par problēmu ar automātisko aizpildīšanu
Anonīmi ziņot, ka šajā vietnē nedarbojas automātiska aizpildīšana. Paroles nekad netiek izpaustas.
diff --git a/autofill/autofill-impl/src/main/res/values-nb/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-nb/strings-autofill-impl.xml
index 28be87aaba1c..a15f0f6a3b58 100644
--- a/autofill/autofill-impl/src/main/res/values-nb/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-nb/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Slik synkroniserer du passord på datamaskinen
Importer passord i datamaskinversjonen av DuckDuckGo-nettleseren, og så kan du synkronisere mellom enheter.
Skaff deg nettleseren for datamaskin
- Lenken er kopiert
Skaff deg DuckDuckGo-nettleseren for Mac eller Windows
Søk privat og blokker sporere med DuckDuckGo-nettleseren for datamaskin. Gå til denne lenken på datamaskinen for å laste ned i dag.\n\n%1$s
Synkroniser med datamaskinen
@@ -215,8 +214,6 @@
Hent skrivebordsappen
Skaff deg DuckDuckGo for Mac eller Windows
På datamaskinen går du til:
- duckduckgo.com/browser
- Del nedlastingslenke
Rapporter et problem med Autofyll
Rapporter anonymt at autofyll ikke fungerer på dette nettstedet. Passord deles aldri.
diff --git a/autofill/autofill-impl/src/main/res/values-nl/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-nl/strings-autofill-impl.xml
index 249173fc0e25..ee017c111de6 100644
--- a/autofill/autofill-impl/src/main/res/values-nl/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-nl/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Synchroniseren van desktopwachtwoorden
Importeer wachtwoorden in de desktopversie van de DuckDuckGo-browser en synchroniseer ze vervolgens tussen apparaten.
Download de desktopbrowser
- Link gekopieerd
DuckDuckGo-browser downloaden voor Mac of Windows
Zoek privé en blokkeer trackers met de DuckDuckGo-desktopbrowser. Bezoek deze link op je computer en download de browser vandaag nog.\n\n%1$s
Synchroniseren met de desktop
@@ -215,8 +214,6 @@
Desktop-app downloaden
DuckDuckGo downloaden voor Mac of Windows
Ga op je computer naar:
- duckduckgo.com/browser
- Downloadlink delen
Meld een probleem met automatisch invullen
Anoniem melden dat automatisch invullen niet werkt op deze site. Wachtwoorden worden nooit gedeeld.
diff --git a/autofill/autofill-impl/src/main/res/values-pl/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-pl/strings-autofill-impl.xml
index 327366d0fd95..0ec4cd5c80ee 100644
--- a/autofill/autofill-impl/src/main/res/values-pl/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-pl/strings-autofill-impl.xml
@@ -214,7 +214,6 @@
Jak synchronizować hasła na komputerze
Zaimportuj hasła w komputerowej wersji przeglądarki DuckDuckGo, a następnie zsynchronizuj je na różnych urządzeniach.
Pobierz przeglądarkę komputerową
- Skopiowano łącze
Pobierz przeglądarkę DuckDuckGo dla komputerów Mac lub systemu Windows
Wyszukuj prywatnie i blokuj skrypty śledzące za pomocą przeglądarki komputerowej DuckDuckGo. Odwiedź ten link na komputerze, aby pobrać ją już dziś.\n\n%1$s
Synchronizuj z komputerem
@@ -227,8 +226,6 @@
Pobierz aplikację komputerową
Pobierz DuckDuckGo dla komputerów Mac lub systemu Windows
Na komputerze przejdź do:
- duckduckgo.com/browser
- Udostępnij link pobierania
Zgłoś problem z autouzupełnianiem
Zgłoś anonimowo, że autouzupełnianie nie działa w tej witrynie. Hasła nigdy nie są udostępniane.
diff --git a/autofill/autofill-impl/src/main/res/values-pt/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-pt/strings-autofill-impl.xml
index bd3e9a700ff5..e30784b2a82b 100644
--- a/autofill/autofill-impl/src/main/res/values-pt/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-pt/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Como sincronizar palavras-passe com o computador
Importa palavras-passe na versão para computadores do navegador DuckDuckGo e sincroniza-as entre dispositivos.
Obter navegador para computador
- Link copiado
Obtém o navegador DuckDuckGo para Mac ou Windows
Pesquisa em privado e bloqueia rastreadores com o navegador para computadores DuckDuckGo. Visita este link no teu computador para transferires hoje.\n\n%1$s
Sincronizar com computador
@@ -215,8 +214,6 @@
Obter aplicação para computador
Obtém o DuckDuckGo para Mac ou Windows
No teu computador, acede a:
- duckduckgo.com/browser
- Partilhar link de transferência
Comunicar um problema com o preenchimento automático
Denuncia anonimamente que o preenchimento automático não está a funcionar neste site. As palavras-passe nunca são partilhadas.
diff --git a/autofill/autofill-impl/src/main/res/values-ro/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-ro/strings-autofill-impl.xml
index 0a6476663b02..5dd313062186 100644
--- a/autofill/autofill-impl/src/main/res/values-ro/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-ro/strings-autofill-impl.xml
@@ -208,7 +208,6 @@
Cum se sincronizează parolele de pe desktop
Importă parolele în versiunea desktop a browserului DuckDuckGo, apoi sincronizează-le între dispozitive.
Obține browserul pentru desktop
- Link copiat
Obține browserul DuckDuckGo pentru Mac sau Windows
Caută în mod privat și blochează tehnologiile de urmărire cu browserul pentru desktop DuckDuckGo. Accesează acest link pe computerul tău pentru a descărca astăzi.\n\n%1$s
Sincronizare cu computerul desktop
@@ -221,8 +220,6 @@
Obține aplicația pentru desktop
Obține DuckDuckGo pentru Mac sau Windows
De pe computerul tău, accesează:
- duckduckgo.com/browser
- Trimite linkul de descărcare
Raportează o problemă cu completarea automată
Raportează anonim că completarea automată nu funcționează pe acest site. Parolele nu sunt niciodată comunicate.
diff --git a/autofill/autofill-impl/src/main/res/values-ru/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-ru/strings-autofill-impl.xml
index 0e867d9db52e..bb6fcba273ac 100644
--- a/autofill/autofill-impl/src/main/res/values-ru/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-ru/strings-autofill-impl.xml
@@ -214,7 +214,6 @@
Как синхронизировать пароли из настольного браузера
Импортируйте пароли в настольную версию браузера DuckDuckGo, а затем синхронизируйте их между устройствами.
Скачать настольный браузер
- Ссылка скопирована
Браузер DuckDuckGo для Mac и Windows
Браузер DuckDuckGo для настольных компьютеров — это конфиденциальный поиск плюс блокировка трекеров. Скачивайте по ссылке.\n\n%1$s
Синхронизировать с компьютером
@@ -227,8 +226,6 @@
Наше настольное приложение
DuckDuckGo для Mac и Windows
На компьютере откройте ссылку:
- duckduckgo.com/browser
- Поделиться ссылкой для загрузки
Сообщить о сбое автозаполнения
Анонимный отчет о сбое в работе функции автозаполнения на этом сайте. Ни в коем случае не включает пароли.
diff --git a/autofill/autofill-impl/src/main/res/values-sk/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-sk/strings-autofill-impl.xml
index 7c8f438cad55..1e20d4050b24 100644
--- a/autofill/autofill-impl/src/main/res/values-sk/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-sk/strings-autofill-impl.xml
@@ -214,7 +214,6 @@
Ako synchronizovať heslá pracovnej plochy
Heslá môžete importovať do počítačovej verzie prehliadača DuckDuckGo a potom ich synchronizovať medzi zariadeniami.
Získajte prehliadač pre desktop PC
- Odkaz bol skopírovaný
Získajte prehliadač DuckDuckGo pre Mac alebo Windows
Vyhľadávajte súkromne a blokujte sledovacie zariadenia pomocou prehliadača DuckDuckGo pre počítače. Navštívte tento odkaz vo svojom počítači a stiahnite si ho ešte dnes.\n\n%1$s
Synchronizácia s počítačom
@@ -227,8 +226,6 @@
Získajte aplikáciu pre počítače
Získajte DuckDuckGo pre Mac alebo Windows
V počítači prejdite na:
- duckduckgo.com/browser
- Zdieľať odkaz na stiahnutie
Nahlásenie problému s funkciou Automatické dopĺňanie
Anonymne nahláste, že automatické dopĺňanie na tejto stránke nefunguje. Heslá sa nikdy nezdieľajú.
diff --git a/autofill/autofill-impl/src/main/res/values-sl/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-sl/strings-autofill-impl.xml
index 88d3a79f728d..9c4c67ad2582 100644
--- a/autofill/autofill-impl/src/main/res/values-sl/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-sl/strings-autofill-impl.xml
@@ -214,7 +214,6 @@
Kako sinhronizirati gesla za namizje
V namizni različici brskalnika DuckDuckGo uvozite gesla in jih nato sinhronizirajte med napravami.
Namesti namizni brskalnik
- Povezava je kopirana
Prenesite brskalnik DuckDuckGo za računalnike Mac ali Windows
Brskajte zasebno in blokirajte sledilnike z namiznim brskalnikom DuckDuckGo. Za prenos obiščite to povezavo v računalniku.\n\n%1$s
Sinhronizacija z namizjem
@@ -227,8 +226,6 @@
Pridobite namizno aplikacijo
Prenesite DuckDuckGo za računalnike Mac ali Windows
V računalniku odprite:
- duckduckgo.com/browser
- Deli povezavo za prenos
Prijavite težavo s samodejnim izpolnjevanjem
Anonimno prijavite, da samodejno izpolnjevanje ne deluje na tem spletnem mestu. Gesla niso nikoli deljena.
diff --git a/autofill/autofill-impl/src/main/res/values-sv/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-sv/strings-autofill-impl.xml
index 23618efd46af..e47523ecb15f 100644
--- a/autofill/autofill-impl/src/main/res/values-sv/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-sv/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Så här synkroniserar du lösenord för dator
Importera lösenord i versionen av DuckDuckGo-webbläsaren för dator och synkronisera sedan mellan enheter.
Hämta webbläsare för dator
- Länken har kopierats
Hämta DuckDuckGo-webbläsaren för Mac eller Windows
Sök privat och blockera spårare med DuckDuckGo-webbläsaren för datorn. Gå till denna länk på din dator för att ladda ner den idag.\n\n%1$s
Synkronisera med dator
@@ -215,8 +214,6 @@
Hämta app för dator
Hämta DuckDuckGo för Mac eller Windows
På din dator går du till:
- duckduckgo.com/browser
- Dela nedladdningslänk
Rapportera ett problem med automatisk ifyllning
Rapportera anonymt att automatisk ifyllning inte fungerar på den här webbplatsen. Lösenord delas aldrig.
diff --git a/autofill/autofill-impl/src/main/res/values-tr/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-tr/strings-autofill-impl.xml
index 7c4d577eaf54..fe67c0ba418b 100644
--- a/autofill/autofill-impl/src/main/res/values-tr/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values-tr/strings-autofill-impl.xml
@@ -202,7 +202,6 @@
Masaüstü Şifreleri Nasıl Senkronize Edilir
DuckDuckGo tarayıcısının masaüstü sürümünde şifreleri içe aktarın, ardından cihazlar arasında senkronize edin.
Masaüstü Tarayıcısını Edinin
- Bağlantı kopyalandı
Mac veya Windows için DuckDuckGo Tarayıcısını edinin
DuckDuckGo masaüstü tarayıcısı ile gizli arama yapın ve izleyicileri engelleyin. Bugün indirmek için bilgisayarınızda bu bağlantıyı ziyaret edin.\n\n%1$s
Masaüstü ile Senkronize Et
@@ -215,8 +214,6 @@
Masaüstü Uygulamasını İndirin
Mac veya Windows için DuckDuckGo\'yu edinin
Bilgisayarınızda şu adrese gidin:
- duckduckgo.com/browser
- İndirme Bağlantısını Paylaş
Otomatik Doldurmayla ilgili bir sorunu bildirin
Otomatik doldurmanın bu sitede çalışmadığını anonim olarak bildirin. Şifreler asla paylaşılmaz.
diff --git a/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml
index b02072db2314..9f1469561271 100644
--- a/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml
+++ b/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml
@@ -198,7 +198,6 @@
How to Sync Desktop Passwords
Import passwords in the desktop version of the DuckDuckGo browser, then sync across devices.
Get Desktop Browser
- Link copied
Get DuckDuckGo Browser for Mac or Windows
Search privately and block trackers with the DuckDuckGo desktop browser. Visit this link on your computer to download today.\n\n%1$s
Sync With Desktop
@@ -211,8 +210,6 @@
Get Desktop App
Get DuckDuckGo for Mac or Windows
On your computer, go to:
- duckduckgo.com/browser
- Share Download Link
Report a problem with autofill
Anonymously report autofill not working on this site. Passwords are never shared.
diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsDesktopAppPromotionParamsTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsDesktopAppPromotionParamsTest.kt
new file mode 100644
index 000000000000..8b89fda8a0f5
--- /dev/null
+++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsDesktopAppPromotionParamsTest.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.autofill.impl.ui.credential.management.importpassword
+
+import android.content.Context
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/**
+ * Pixel names reach the shared promo screen as plain strings, so these assert the exact wire names
+ * this entry point fired before the screens were consolidated.
+ */
+class ImportPasswordsDesktopAppPromotionParamsTest {
+
+ private val contextMock: Context = mock().apply {
+ whenever(getString(any())).thenReturn("copy")
+ whenever(getString(any(), any())).thenReturn("copy with url")
+ }
+
+ @Test
+ fun whenLaunchedThenShareAndLinkPixelsAreUnchanged() {
+ val params = ImportPasswordsDesktopAppPromotionParams.create(contextMock)
+
+ assertEquals("m_get_desktop_share", params.pixels.shareClicked?.pixelName)
+ assertEquals("m_get_desktop_copy", params.pixels.linkClicked?.pixelName)
+ }
+
+ @Test
+ fun whenLaunchedThenNoImpressionOrDismissPixelIsConfigured() {
+ val params = ImportPasswordsDesktopAppPromotionParams.create(contextMock)
+
+ assertNull(params.pixels.impression)
+ assertNull(params.pixels.dismissed)
+ }
+
+ @Test
+ fun whenLaunchedThenAttributedUrlIsUnchanged() {
+ val params = ImportPasswordsDesktopAppPromotionParams.create(contextMock)
+
+ assertEquals("https://duckduckgo.com/browser?origin=funnel_browser_android_sync", params.downloadUrl)
+ }
+
+ @Test
+ fun whenLaunchedThenNoDismissButtonAndNoInteractionHandler() {
+ val params = ImportPasswordsDesktopAppPromotionParams.create(contextMock)
+
+ assertFalse(params.showDismissButton)
+ assertNull(params.handlerId)
+ }
+
+ @Test
+ fun whenLaunchedThenShareSheetCarriesTheLongerMarketingMessage() {
+ val params = ImportPasswordsDesktopAppPromotionParams.create(contextMock)
+
+ assertEquals("copy with url", params.shareIntentBody)
+ }
+}
diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/desktopapp/ImportPasswordsGetDesktopAppViewModelTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/desktopapp/ImportPasswordsGetDesktopAppViewModelTest.kt
deleted file mode 100644
index 46badfe6fc13..000000000000
--- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/desktopapp/ImportPasswordsGetDesktopAppViewModelTest.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package com.duckduckgo.autofill.impl.ui.credential.management.importpassword.desktopapp
-
-import app.cash.turbine.test
-import com.duckduckgo.app.statistics.pixels.Pixel
-import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_COPIED_DESKTOP_LINK
-import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_SHARED_DESKTOP_LINK
-import com.duckduckgo.autofill.impl.ui.credential.management.AutofillClipboardInteractor
-import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.desktopapp.ImportPasswordsGetDesktopAppViewModel.Command
-import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.desktopapp.ImportPasswordsGetDesktopAppViewModel.Command.ShareLink
-import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.desktopapp.ImportPasswordsGetDesktopAppViewModel.Command.ShowCopiedNotification
-import com.duckduckgo.common.test.CoroutineTestRule
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.mockito.kotlin.any
-import org.mockito.kotlin.eq
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
-
-class ImportPasswordsGetDesktopAppViewModelTest {
-
- @get:Rule
- val coroutineTestRule: CoroutineTestRule = CoroutineTestRule()
-
- private lateinit var testee: ImportPasswordsGetDesktopAppViewModel
- private val pixel: Pixel = mock()
- private val autofillClipboardInteractor: AutofillClipboardInteractor = mock()
-
- @Before
- fun setup() {
- testee = ImportPasswordsGetDesktopAppViewModel(
- pixel = pixel,
- dispatchers = coroutineTestRule.testDispatcherProvider,
- autofillClipboardInteractor = autofillClipboardInteractor,
- )
- }
-
- @Test
- fun whenLinkClickedThenCopiedToClipboard() = runTest {
- testee.onLinkClicked()
- verify(autofillClipboardInteractor).copyToClipboard(toCopy = any(), isSensitive = eq(false))
- }
-
- @Test
- fun whenLinkCopiedToClipboardAndSystemNotificationNotShownThenWeShowOurOwnNotification() = runTest {
- whenever(autofillClipboardInteractor.shouldShowCopyNotification()).thenReturn(true)
- testee.onLinkClicked()
- testee.commands.test {
- awaitItem().assertIsShowNotification()
- cancelAndIgnoreRemainingEvents()
- }
- }
-
- @Test
- fun whenLinkClickedThenPixelFired() = runTest {
- testee.onLinkClicked()
- verify(pixel).fire(AUTOFILL_IMPORT_PASSWORDS_COPIED_DESKTOP_LINK)
- }
-
- @Test
- fun whenShareClickedThenCommandSent() = runTest {
- testee.onShareClicked()
- testee.commands.test {
- val command = awaitItem().assertIsShareLink()
- assertEquals("https://duckduckgo.com/browser?origin=funnel_browser_android_sync", command.link)
- cancelAndIgnoreRemainingEvents()
- }
- }
-
- @Test
- fun whenShareClickedThenPixelFired() = runTest {
- testee.onShareClicked()
- verify(pixel).fire(AUTOFILL_IMPORT_PASSWORDS_SHARED_DESKTOP_LINK)
- }
-
- private fun Command.assertIsShareLink() = this as ShareLink
- private fun Command.assertIsShowNotification() = this as ShowCopiedNotification
-}
diff --git a/desktop-app-promotion/desktop-app-promotion-api/build.gradle b/desktop-app-promotion/desktop-app-promotion-api/build.gradle
new file mode 100644
index 000000000000..63740b91e585
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-api/build.gradle
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+apply from: "$rootProject.projectDir/gradle/android-library.gradle"
+
+dependencies {
+ implementation project(':navigation-api')
+
+ implementation AndroidX.core.ktx
+}
+
+android {
+ namespace 'com.duckduckgo.desktopapppromotion.api'
+}
diff --git a/desktop-app-promotion/desktop-app-promotion-api/src/main/java/com/duckduckgo/desktopapppromotion/api/DesktopAppPromotionInteractionHandler.kt b/desktop-app-promotion/desktop-app-promotion-api/src/main/java/com/duckduckgo/desktopapppromotion/api/DesktopAppPromotionInteractionHandler.kt
new file mode 100644
index 000000000000..e919c27e2d01
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-api/src/main/java/com/duckduckgo/desktopapppromotion/api/DesktopAppPromotionInteractionHandler.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.desktopapppromotion.api
+
+/**
+ * Lets a caller of the desktop-app promo screen react to what the user did on it, without the promo
+ * screen knowing anything about the caller. Contribute an implementation from the module that owns
+ * the side effect, and set [DesktopAppPromotionParams.handlerId] to its [handlerId] when launching.
+ *
+ * Handlers are resolved by an exact [handlerId] match, never notified as a group — a launch that
+ * carries no `handlerId`, or one naming a handler nobody contributed, triggers nothing.
+ */
+interface DesktopAppPromotionInteractionHandler {
+
+ /** Matches the [DesktopAppPromotionParams.handlerId] of the launch that produced the interaction. */
+ val handlerId: String
+
+ suspend fun onInteraction(interaction: Interaction)
+
+ enum class Interaction {
+ LINK_COPIED,
+
+ /** The user picked a target in the share sheet, not merely opened it. */
+ SHARE_COMPLETED,
+
+ DISMISSED,
+ }
+}
diff --git a/desktop-app-promotion/desktop-app-promotion-api/src/main/java/com/duckduckgo/desktopapppromotion/api/DesktopAppPromotionParams.kt b/desktop-app-promotion/desktop-app-promotion-api/src/main/java/com/duckduckgo/desktopapppromotion/api/DesktopAppPromotionParams.kt
new file mode 100644
index 000000000000..d712ef54bdaa
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-api/src/main/java/com/duckduckgo/desktopapppromotion/api/DesktopAppPromotionParams.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.desktopapppromotion.api
+
+import androidx.annotation.DrawableRes
+import com.duckduckgo.navigation.api.GlobalActivityStarter
+import java.io.Serializable
+
+/**
+ * Launch params for the shared "get the DuckDuckGo desktop app/browser" promo screen.
+ *
+ * The screen is a renderer: every piece of content and every pixel it may fire is supplied here, and
+ * it never infers behaviour from which feature launched it.
+ *
+ * Every content field is nullable, and `null` means "use the promo screen's own default". The
+ * defaults are real, translated resources owned by the implementation module, so a caller that wants
+ * the canonical copy — including a deeplink, which can only supply JSON — passes nothing.
+ */
+data class DesktopAppPromotionParams(
+
+ /** Toolbar title. */
+ val toolbarTitle: String? = null,
+
+ /** Screen title. */
+ val title: String? = null,
+
+ /** Body copy shown under the title. */
+ val body: String? = null,
+
+ /** Illustration shown above the title. `0` means use the default. */
+ @DrawableRes val illustration: Int = 0,
+
+ /** The human-readable URL shown on screen, e.g. `"duckduckgo.com/browser"`. */
+ val downloadUrlDisplay: String? = null,
+
+ /**
+ * The full, attributed URL used for the share sheet and the copy-to-clipboard action,
+ * e.g. `"https://duckduckgo.com/browser?origin=funnel_appsettings_android"`.
+ * Callers own their own attribution origin — this module does not construct or validate it.
+ */
+ val downloadUrl: String? = null,
+
+ /** Label of the primary share button. */
+ val shareButtonLabel: String? = null,
+
+ /** Title used for the OS share-sheet chooser when the user taps the share button. */
+ val shareIntentTitle: String? = null,
+
+ /**
+ * Optional complete message shared to the OS share sheet, already containing [downloadUrl] if
+ * the caller wants it there. When `null`, the bare URL is shared.
+ */
+ val shareIntentBody: String? = null,
+
+ /** Whether to show the dismiss ("No Thanks") button. */
+ val showDismissButton: Boolean = false,
+
+ /** Label of the dismiss button, when shown. */
+ val dismissButtonLabel: String? = null,
+
+ /** What to fire, and with what params, for each of this screen's four interaction points. */
+ val pixels: PixelConfig = PixelConfig(),
+
+ /**
+ * Opaque key routing post-interaction side effects back to a
+ * [DesktopAppPromotionInteractionHandler] contributed by the caller's module. `null` means this
+ * caller wants no side effects.
+ */
+ val handlerId: String? = null,
+
+) : GlobalActivityStarter.ActivityParams
+
+/**
+ * Per-interaction pixel configuration. Each field is independently nullable: `null` means "this
+ * caller doesn't track this interaction" and the screen fires nothing for it. There is no
+ * default/fallback pixel — callers keep firing their own already-reviewed pixel names, and this
+ * module never invents its own pixel taxonomy.
+ */
+data class PixelConfig(
+
+ /** Fired once, when the screen is first shown. Not re-fired on rotation or recreation. */
+ val impression: PixelFireSpec? = null,
+
+ /** Fired when the user taps the share button. */
+ val shareClicked: PixelFireSpec? = null,
+
+ /** Fired when the user taps the on-screen URL to copy it to the clipboard. */
+ val linkClicked: PixelFireSpec? = null,
+
+ /**
+ * Fired when the user taps the dismiss button. Only reachable when
+ * [DesktopAppPromotionParams.showDismissButton] is `true`.
+ */
+ val dismissed: PixelFireSpec? = null,
+) : Serializable
+
+/**
+ * One pixel to fire: a wire-format pixel name plus its parameters.
+ *
+ * [pixelName] is a plain `String`, not `Pixel.PixelName`, on purpose — the caller passes its own
+ * enum's `.pixelName` so this module never has to know about any feature-specific pixel-name enum.
+ *
+ * [parameters] must already satisfy the repo's pixel privacy rules; that responsibility stays with
+ * the caller that owns the pixel definition. `HashMap` rather than `Map` because
+ * [GlobalActivityStarter.ActivityParams] is `Serializable` and `Map` is not a serializable type.
+ */
+data class PixelFireSpec(
+ val pixelName: String,
+ val parameters: HashMap = HashMap(),
+) : Serializable
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/build.gradle b/desktop-app-promotion/desktop-app-promotion-impl/build.gradle
new file mode 100644
index 000000000000..57d603bcf298
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/build.gradle
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+ id 'com.squareup.anvil'
+ id 'com.google.devtools.ksp'
+}
+
+apply from: "$rootProject.projectDir/gradle/android-library.gradle"
+
+android {
+ namespace 'com.duckduckgo.desktopapppromotion.impl'
+ anvil {
+ generateDaggerFactories = true // default is false
+ }
+}
+
+dependencies {
+ ksp project(':anvil-ksp')
+ implementation project(':anvil-annotations')
+
+ implementation project(':desktop-app-promotion-api')
+
+ implementation project(':di')
+ implementation project(':browser-api')
+ implementation project(':design-system')
+ implementation project(':navigation-api')
+ implementation project(':common-utils')
+ implementation project(':statistics-api')
+
+ implementation "com.squareup.logcat:logcat:_"
+
+ implementation AndroidX.appCompat
+ implementation AndroidX.core.ktx
+ implementation AndroidX.constraintLayout
+ implementation AndroidX.lifecycle.runtime.ktx
+ implementation AndroidX.lifecycle.viewModelKtx
+ implementation Google.android.material
+ implementation KotlinX.coroutines.android
+
+ // Dagger
+ implementation Google.dagger
+
+ testImplementation Testing.junit4
+ testImplementation "org.mockito.kotlin:mockito-kotlin:_"
+ testImplementation project(path: ':common-test')
+ testImplementation CashApp.turbine
+ testImplementation(KotlinX.coroutines.test) {
+ exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug"
+ }
+}
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/AndroidManifest.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/AndroidManifest.xml
new file mode 100644
index 000000000000..5a36456e3c02
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionActivity.kt b/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionActivity.kt
new file mode 100644
index 000000000000..70c9abf76414
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionActivity.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.desktopapppromotion.impl
+
+import android.app.PendingIntent
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.addCallback
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
+import com.duckduckgo.anvil.annotations.InjectWith
+import com.duckduckgo.common.ui.DuckDuckGoActivity
+import com.duckduckgo.common.ui.viewbinding.viewBinding
+import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeBucket
+import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeHandler
+import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeProvider
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionParams
+import com.duckduckgo.desktopapppromotion.impl.DesktopAppPromotionShareBroadcastReceiver.Companion.EXTRA_HANDLER_ID
+import com.duckduckgo.desktopapppromotion.impl.DesktopAppPromotionViewModel.Command
+import com.duckduckgo.desktopapppromotion.impl.databinding.ActivityDesktopAppPromotionBinding
+import com.duckduckgo.di.scopes.ActivityScope
+import com.duckduckgo.navigation.api.getActivityParams
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import logcat.LogPriority.WARN
+import logcat.asLog
+import logcat.logcat
+import javax.inject.Inject
+
+// The screen name is inherited from the Settings screen this replaces, so existing deeplinks keep
+// resolving. A deeplink supplies no content, so every field falls back to this module's defaults.
+@InjectWith(ActivityScope::class)
+@ContributeToActivityStarter(DesktopAppPromotionParams::class, screenName = "getDesktopBrowser")
+class DesktopAppPromotionActivity : DuckDuckGoActivity() {
+
+ @Inject
+ lateinit var promotionViewModelFactory: DesktopAppPromotionViewModel.Factory
+
+ @Inject
+ lateinit var edgeToEdgeProvider: EdgeToEdgeProvider
+
+ @Inject
+ lateinit var edgeToEdgeHandler: EdgeToEdgeHandler
+
+ private val binding: ActivityDesktopAppPromotionBinding by viewBinding()
+
+ private val params: DesktopAppPromotionParams by lazy {
+ intent.getActivityParams(DesktopAppPromotionParams::class.java) ?: DesktopAppPromotionParams()
+ }
+
+ private val content: DesktopAppPromotionContent by lazy { params.resolveContent(this) }
+
+ private val viewModel: DesktopAppPromotionViewModel by lazy {
+ ViewModelProvider.create(
+ store = viewModelStore,
+ factory = object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class) = promotionViewModelFactory.create(params, content) as T
+ },
+ extras = this.defaultViewModelCreationExtras,
+ )[DesktopAppPromotionViewModel::class.java]
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val edgeToEdgeEnabled = edgeToEdgeProvider.isEnabled(EdgeToEdgeBucket.MISC)
+ if (edgeToEdgeEnabled) {
+ enableTransparentEdgeToEdge()
+ }
+
+ setContentView(binding.root)
+ setupToolbar(binding.includeToolbar.toolbar)
+ supportActionBar?.title = content.toolbarTitle
+
+ if (edgeToEdgeEnabled) {
+ configureEdgeToEdgeInsets()
+ }
+
+ setupObservers()
+ setupBackNavigationHandler()
+ setupClickListeners()
+ }
+
+ private fun configureEdgeToEdgeInsets() {
+ edgeToEdgeHandler.applyHorizontalSystemBarInsets(binding.root)
+ edgeToEdgeHandler.applyStatusBarInsets(binding.includeToolbar.appBarLayout)
+ // Content ends in fixed bottom buttons, so keep them clear of the nav bar in every mode
+ // rather than drawing behind the gesture handle.
+ edgeToEdgeHandler.applyNavigationBarInsets(binding.contentScrollView, drawBehindGestureNav = false)
+ }
+
+ private fun setupObservers() {
+ viewModel.viewState
+ .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
+ .onEach { render(it) }
+ .launchIn(lifecycleScope)
+
+ viewModel.commands
+ .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
+ .onEach { processCommand(it) }
+ .launchIn(lifecycleScope)
+ }
+
+ private fun render(viewState: DesktopAppPromotionViewModel.ViewState) {
+ with(viewState.content) {
+ binding.titleText.text = title
+ binding.bodyText.text = body
+ binding.desktopBrowserIcon.setImageResource(illustration)
+ binding.browserUrl.text = downloadUrlDisplay
+ binding.shareDownloadLinkButton.text = shareButtonLabel
+ binding.noThanksButton.text = dismissButtonLabel
+ binding.noThanksButton.isVisible = showDismissButton
+ }
+ }
+
+ private fun processCommand(command: Command) {
+ when (command) {
+ is Command.ShareLink -> launchShareSheet(command.shareText, command.chooserTitle)
+ is Command.ShowCopiedNotification -> showCopiedNotification()
+ is Command.Close -> finish()
+ }
+ }
+
+ private fun launchShareSheet(
+ shareText: String,
+ chooserTitle: String,
+ ) {
+ val shareIntent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TEXT, shareText)
+ putExtra(Intent.EXTRA_TITLE, chooserTitle)
+ }
+
+ try {
+ startActivity(Intent.createChooser(shareIntent, chooserTitle, shareCompletionIntentSender()))
+ } catch (e: ActivityNotFoundException) {
+ logcat(WARN) { "Activity not found for share: ${e.asLog()}" }
+ }
+ }
+
+ /**
+ * Only worth building when a caller registered a handler — without one there is nothing to
+ * report a completed share to, and callers that never had this behaviour keep a plain chooser.
+ */
+ private fun shareCompletionIntentSender() = params.handlerId?.let { handlerId ->
+ PendingIntent.getBroadcast(
+ this,
+ 0,
+ Intent(this, DesktopAppPromotionShareBroadcastReceiver::class.java).putExtra(EXTRA_HANDLER_ID, handlerId),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+ ).intentSender
+ }
+
+ private fun setupClickListeners() {
+ binding.shareDownloadLinkButton.setOnClickListener { viewModel.onShareClicked() }
+ binding.noThanksButton.setOnClickListener { viewModel.onDismissClicked() }
+ binding.browserUrl.setOnClickListener { viewModel.onLinkClicked() }
+ }
+
+ private fun setupBackNavigationHandler() {
+ onBackPressedDispatcher.addCallback(this) {
+ viewModel.onBackPressed()
+ }
+ }
+
+ private fun showCopiedNotification() {
+ Snackbar.make(binding.root, content.linkCopiedMessage, Snackbar.LENGTH_SHORT).show()
+ }
+}
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionContent.kt b/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionContent.kt
new file mode 100644
index 000000000000..29f97d26fbfc
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionContent.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.desktopapppromotion.impl
+
+import android.content.Context
+import androidx.annotation.DrawableRes
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionParams
+
+/**
+ * [DesktopAppPromotionParams] with every caller-optional field filled in. Resolving the defaults
+ * needs a `Context`, so it happens in the Activity rather than the ViewModel.
+ */
+data class DesktopAppPromotionContent(
+ val toolbarTitle: String,
+ val title: String,
+ val body: String,
+ @DrawableRes val illustration: Int,
+ val downloadUrlDisplay: String,
+ val downloadUrl: String,
+ val shareButtonLabel: String,
+ val shareIntentTitle: String,
+ val shareIntentBody: String?,
+ val showDismissButton: Boolean,
+ val dismissButtonLabel: String,
+ val linkCopiedMessage: String,
+)
+
+fun DesktopAppPromotionParams.resolveContent(context: Context): DesktopAppPromotionContent {
+ return DesktopAppPromotionContent(
+ toolbarTitle = toolbarTitle ?: context.getString(R.string.desktopAppPromotionToolbarTitle),
+ title = title ?: context.getString(R.string.desktopAppPromotionTitle),
+ body = body ?: context.getString(R.string.desktopAppPromotionBody),
+ illustration = if (illustration != 0) illustration else R.drawable.image_get_desktop_browser,
+ downloadUrlDisplay = downloadUrlDisplay ?: context.getString(R.string.desktopAppPromotionUrl),
+ downloadUrl = downloadUrl ?: DEFAULT_DOWNLOAD_URL,
+ shareButtonLabel = shareButtonLabel ?: context.getString(R.string.desktopAppPromotionShareDownloadLink),
+ shareIntentTitle = shareIntentTitle ?: context.getString(R.string.desktopAppPromotionShareDownloadLink),
+ shareIntentBody = shareIntentBody,
+ showDismissButton = showDismissButton,
+ dismissButtonLabel = dismissButtonLabel ?: context.getString(R.string.desktopAppPromotionNoThanks),
+ linkCopiedMessage = context.getString(R.string.desktopAppPromotionLinkCopied),
+ )
+}
+
+// Matches the attribution the Settings entry points use; deeplink launches land on the same funnel.
+private const val DEFAULT_DOWNLOAD_URL = "https://duckduckgo.com/browser?origin=funnel_appsettings_android"
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionInteractionDispatcher.kt b/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionInteractionDispatcher.kt
new file mode 100644
index 000000000000..9a8072317c77
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionInteractionDispatcher.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.desktopapppromotion.impl
+
+import com.duckduckgo.common.utils.plugins.PluginPoint
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionInteractionHandler
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionInteractionHandler.Interaction
+import com.duckduckgo.di.scopes.AppScope
+import com.squareup.anvil.annotations.ContributesBinding
+import javax.inject.Inject
+
+/**
+ * Routes an interaction to the one handler a launch named. Shared by the ViewModel and the share
+ * broadcast receiver so both resolve handlers the same way.
+ */
+interface DesktopAppPromotionInteractionDispatcher {
+ suspend fun dispatch(
+ handlerId: String?,
+ interaction: Interaction,
+ )
+}
+
+@ContributesBinding(AppScope::class)
+class RealDesktopAppPromotionInteractionDispatcher @Inject constructor(
+ private val handlers: PluginPoint,
+) : DesktopAppPromotionInteractionDispatcher {
+
+ override suspend fun dispatch(
+ handlerId: String?,
+ interaction: Interaction,
+ ) {
+ // Exact match only: a launch with no handler, or one naming a handler nobody contributed,
+ // must not reach another caller's handler.
+ val id = handlerId ?: return
+ handlers.getPlugins()
+ .firstOrNull { it.handlerId == id }
+ ?.onInteraction(interaction)
+ }
+}
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionInteractionHandlerPluginPoint.kt b/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionInteractionHandlerPluginPoint.kt
new file mode 100644
index 000000000000..741925a6aeee
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionInteractionHandlerPluginPoint.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.desktopapppromotion.impl
+
+import com.duckduckgo.anvil.annotations.ContributesPluginPoint
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionInteractionHandler
+import com.duckduckgo.di.scopes.AppScope
+
+// AppScope, not ActivityScope: the share-completion broadcast is delivered with no activity alive.
+@ContributesPluginPoint(
+ scope = AppScope::class,
+ boundType = DesktopAppPromotionInteractionHandler::class,
+)
+private interface DesktopAppPromotionInteractionHandlerTrigger
diff --git a/app/src/main/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserShareBroadcastReceiver.kt b/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionShareBroadcastReceiver.kt
similarity index 58%
rename from app/src/main/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserShareBroadcastReceiver.kt
rename to desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionShareBroadcastReceiver.kt
index d0a4514a2f2f..8b4f5b9407aa 100644
--- a/app/src/main/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserShareBroadcastReceiver.kt
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionShareBroadcastReceiver.kt
@@ -14,33 +14,38 @@
* limitations under the License.
*/
-package com.duckduckgo.app.desktopbrowser
+package com.duckduckgo.desktopapppromotion.impl
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.di.AppCoroutineScope
-import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionInteractionHandler.Interaction
import com.duckduckgo.di.scopes.ReceiverScope
import dagger.android.AndroidInjection
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
+/**
+ * Reports a *completed* share — the user picked a target in the chooser, rather than merely opening
+ * it. The chooser calls back after the promo screen may already be gone, which is why this arrives
+ * as a broadcast and not as an activity result.
+ */
@InjectWith(ReceiverScope::class)
-class GetDesktopBrowserShareBroadcastReceiver : BroadcastReceiver() {
+class DesktopAppPromotionShareBroadcastReceiver : BroadcastReceiver() {
@Inject
- lateinit var settingsDataStore: SettingsDataStore
+ lateinit var interactionDispatcher: DesktopAppPromotionInteractionDispatcher
@Inject
@AppCoroutineScope
- lateinit var coroutineScope: CoroutineScope
+ lateinit var appCoroutineScope: CoroutineScope
@Inject
- lateinit var dispatcherProvider: DispatcherProvider
+ lateinit var dispatchers: DispatcherProvider
override fun onReceive(
context: Context,
@@ -48,11 +53,16 @@ class GetDesktopBrowserShareBroadcastReceiver : BroadcastReceiver() {
) {
AndroidInjection.inject(this, context)
+ val handlerId = intent.getStringExtra(EXTRA_HANDLER_ID) ?: return
val pendingResult = goAsync()
- coroutineScope.launch(dispatcherProvider.io()) {
- settingsDataStore.getDesktopBrowserSettingDismissed = true
+ appCoroutineScope.launch(dispatchers.io()) {
+ interactionDispatcher.dispatch(handlerId, Interaction.SHARE_COMPLETED)
pendingResult.finish()
}
}
+
+ companion object {
+ const val EXTRA_HANDLER_ID = "desktopAppPromotion.handlerId"
+ }
}
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionViewModel.kt b/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionViewModel.kt
new file mode 100644
index 000000000000..43362370ce78
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionViewModel.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.desktopapppromotion.impl
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.duckduckgo.app.clipboard.ClipboardInteractor
+import com.duckduckgo.app.statistics.pixels.Pixel
+import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionInteractionHandler.Interaction
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionParams
+import com.duckduckgo.desktopapppromotion.api.PixelFireSpec
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+
+class DesktopAppPromotionViewModel @AssistedInject constructor(
+ @Assisted private val params: DesktopAppPromotionParams,
+ @Assisted private val content: DesktopAppPromotionContent,
+ private val pixel: Pixel,
+ private val dispatchers: DispatcherProvider,
+ private val clipboardInteractor: ClipboardInteractor,
+ private val interactionDispatcher: DesktopAppPromotionInteractionDispatcher,
+) : ViewModel() {
+
+ private val _viewState = MutableStateFlow(ViewState(content))
+ private val _commands = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+
+ val viewState: Flow = _viewState.asStateFlow()
+ val commands: Flow = _commands.receiveAsFlow()
+
+ init {
+ // The ViewModel outlives configuration changes, so this fires once per screen instance.
+ viewModelScope.launch(dispatchers.io()) {
+ fire(params.pixels.impression)
+ }
+ }
+
+ fun onShareClicked() {
+ viewModelScope.launch(dispatchers.io()) {
+ fire(params.pixels.shareClicked)
+ _commands.send(
+ Command.ShareLink(
+ shareText = content.shareIntentBody ?: content.downloadUrl,
+ chooserTitle = content.shareIntentTitle,
+ ),
+ )
+ }
+ }
+
+ fun onLinkClicked() {
+ viewModelScope.launch(dispatchers.io()) {
+ if (!clipboardInteractor.copyToClipboard(content.downloadUrl, isSensitive = false)) {
+ _commands.send(Command.ShowCopiedNotification)
+ }
+ fire(params.pixels.linkClicked)
+ interactionDispatcher.dispatch(params.handlerId, Interaction.LINK_COPIED)
+ }
+ }
+
+ fun onDismissClicked() {
+ viewModelScope.launch(dispatchers.io()) {
+ fire(params.pixels.dismissed)
+ interactionDispatcher.dispatch(params.handlerId, Interaction.DISMISSED)
+ _commands.send(Command.Close)
+ }
+ }
+
+ fun onBackPressed() {
+ viewModelScope.launch {
+ _commands.send(Command.Close)
+ }
+ }
+
+ private fun fire(spec: PixelFireSpec?) {
+ spec?.let { pixel.fire(it.pixelName, it.parameters) }
+ }
+
+ data class ViewState(val content: DesktopAppPromotionContent)
+
+ sealed class Command {
+ data class ShareLink(
+ val shareText: String,
+ val chooserTitle: String,
+ ) : Command()
+
+ data object ShowCopiedNotification : Command()
+ data object Close : Command()
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ params: DesktopAppPromotionParams,
+ content: DesktopAppPromotionContent,
+ ): DesktopAppPromotionViewModel
+ }
+}
diff --git a/app/src/main/res/drawable/image_get_desktop_browser.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/drawable/image_get_desktop_browser.xml
similarity index 100%
rename from app/src/main/res/drawable/image_get_desktop_browser.xml
rename to desktop-app-promotion/desktop-app-promotion-impl/src/main/res/drawable/image_get_desktop_browser.xml
diff --git a/app/src/main/res/layout/activity_get_desktop_browser.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/layout/activity_desktop_app_promotion.xml
similarity index 83%
rename from app/src/main/res/layout/activity_get_desktop_browser.xml
rename to desktop-app-promotion/desktop-app-promotion-impl/src/main/res/layout/activity_desktop_app_promotion.xml
index 440a4dd11eb7..c75570195f1f 100644
--- a/app/src/main/res/layout/activity_get_desktop_browser.xml
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/layout/activity_desktop_app_promotion.xml
@@ -45,10 +45,10 @@
android:layout_height="96dp"
android:layout_marginTop="@dimen/keyline_5"
android:importantForAccessibility="no"
- android:src="@drawable/image_get_desktop_browser"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toTopOf="parent"
+ tools:src="@drawable/image_get_desktop_browser" />
+ app:layout_constraintWidth_max="@dimen/desktopAppPromotionMaxItemWidth"
+ app:typography="title"
+ tools:text="@string/desktopAppPromotionTitle" />
+ app:typography="body1"
+ tools:text="@string/desktopAppPromotionBody" />
+ app:typography="body1_bold"
+ tools:text="@string/desktopAppPromotionUrl" />
+ app:layout_constraintWidth_max="@dimen/desktopAppPromotionMaxItemWidth"
+ app:layout_goneMarginBottom="@dimen/keyline_4"
+ tools:text="@string/desktopAppPromotionShareDownloadLink" />
-
\ No newline at end of file
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-bg/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-bg/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..5308b9f257c7
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-bg/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Вземете браузъра за работен плот
+ Защитете личната си информация и на Mac, и на Windows!
+ За да изтеглите DuckDuckGo за Mac или Windows, посетете:
+ duckduckgo.com/browser
+ Споделяне на връзка за изтегляне
+ Не, благодаря
+ Връзката е копирана
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-cs/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-cs/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..e01b5e975a7f
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-cs/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Nainstalovat prohlížeč pro počítač
+ Chraň své osobní údaje i na Macu a Windows!
+ DuckDuckGo pro Mac nebo Windows si můžeš stáhnout na:
+ duckduckgo.com/browser
+ Sdílet odkaz ke stažení
+ Ne, děkuji
+ Odkaz se zkopíroval
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-da/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-da/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..377f1ba8bb97
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-da/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Hent browser til computeren
+ Beskyt dine personlige oplysninger, også på Mac og Windows!
+ For at downloade DuckDuckGo på Mac eller Windows, skal du besøge:
+ duckduckgo.com/browser
+ Del downloadlink
+ Nej tak
+ Link kopieret
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-de/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-de/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..9e3803969df1
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-de/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Desktop-Browser herunterladen
+ Schütze deine persönlichen Daten auch auf Mac und Windows!
+ Um DuckDuckGo auf Mac oder Windows herunterzuladen, besuche:
+ duckduckgo.com/browser
+ Download-Link teilen
+ Nein, danke
+ Link kopiert
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-el/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-el/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..d9d288772af1
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-el/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Αποκτήστε το πρόγραμμα περιήγησης για υπολογιστές
+ Προστατέψτε τα προσωπικά στοιχεία σας τόσο σε Mac όσο και σε Windows!
+ Για να κάνετ ελήψη του DuckDuckGo σε Mac ή Windows, επισκεφθείτε τη διεύθυνση:
+ duckduckgo.com/browser
+ Κοινή χρήση συνδέσμου λήψης
+ Όχι, ευχαριστώ
+ Ο σύνδεσμος αντιγράφηκε
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-es/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-es/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..a35f44c9455d
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-es/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Obtén el navegador de escritorio
+ ¡Protege tu información personal en Mac y Windows también!
+ Para descargar DuckDuckGo en Mac o Windows, visita:
+ duckduckgo.com/browser
+ Compartir enlace de descarga
+ No, gracias
+ Enlace copiado
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-et/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-et/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..334fc36c807f
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-et/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Hangi töölaua brauser
+ Kaitse oma isikuandmeid nii Macis kui ka Windowsis!
+ DuckDuckGo allalaadimiseks Macile või Windowsile külasta:
+ duckduckgo.com/browser
+ Jaga allalaadimise linki
+ Ei, aitäh
+ Link kopeeritud
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-fi/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-fi/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..7d69fd63f4c1
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-fi/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Hanki pöytäkoneselain
+ Suojaa henkilökohtaiset tietosi myös Mac- ja Windows-laitteessa!
+ Lataa DuckDuckGo Mac- ja Windows-laitteeseen osoitteesta:
+ duckduckgo.com/browser
+ Jaa latauslinkki
+ Ei kiitos
+ Linkki kopioitu
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-fr/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-fr/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..9805578567d1
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-fr/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Télécharger le navigateur de bureau
+ Protégez aussi vos informations personnelles sur Mac et Windows !
+ Pour télécharger DuckDuckGo sur Mac ou Windows, visitez :
+ duckduckgo.com/browser
+ Partager le lien de téléchargement
+ Non merci
+ Lien copié
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-hr/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-hr/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..3198d27cf127
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-hr/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Nabavi preglednik za PC
+ Zaštiti svoje osobne podatke na Macu i Windowsima!
+ Za preuzimanje DuckDuckGo na Mac ili Windows, posjeti:
+ duckduckgo.com/browser
+ Podijeli poveznicu za preuzimanje
+ Ne, hvala
+ Poveznica je kopirana
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-hu/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-hu/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..ff53e157ea56
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-hu/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Asztali böngésző letöltése
+ Védd a személyes adataidat Mac és Windows rendszeren is!
+ A DuckDuckGo Mac vagy Windows rendszerre történő letöltéséhez látogass el ide:
+ duckduckgo.com/browser
+ Letöltési link megosztása
+ Nem, köszönöm
+ Link másolva
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-it/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-it/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..d53c844a3a7c
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-it/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Scarica il browser per desktop
+ Proteggi i tuoi dati personali anche su Mac e Windows.
+ Per scaricare DuckDuckGo su Mac o Windows, visita:
+ duckduckgo.com/browser
+ Condividi link per il download
+ No, grazie
+ Link copiato
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-lt/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-lt/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..7f99e63f7b45
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-lt/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Gaukite kompiuterio naršyklę
+ Apsaugok asmeninę informaciją ir „Mac“ bei „Windows“ kompiuteriuose!
+ Kad atsisiųstum „DuckDuckGo“ į „Mac“ arba „Windows“, apsilankyk:
+ duckduckgo.com/browser
+ Bendrinti atsisiuntimo nuorodą
+ Ne, dėkoju
+ Nuoroda nukopijuota
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-lv/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-lv/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..8939b4436ee7
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-lv/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Iegūsti galddatora pārlūku
+ Aizsargā savu personisko informāciju arī Mac un Windows!
+ Lai lejupielādētu DuckDuckGo operētājsistēmā Mac vai Windows, apmeklē:
+ duckduckgo.com/browser
+ Kopīgot lejupielādes saiti
+ Nē, paldies
+ Saite nokopēta
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-nb/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-nb/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..a8d8186290ee
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-nb/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Skaff deg nettleseren for datamaskin
+ Beskytt personopplysningene dine på Mac og Windows også!
+ For å laste ned DuckDuckGo på Mac eller Windows kan du gå til:
+ duckduckgo.com/browser
+ Del nedlastingslenke
+ Nei takk
+ Lenken er kopiert
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-nl/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-nl/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..e57b1ea19566
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-nl/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Download de desktopbrowser
+ Bescherm je persoonlijke informatie ook op Mac en Windows!
+ Om DuckDuckGo te downloaden op Mac of Windows, bezoek:
+ duckduckgo.com/browser
+ Downloadlink delen
+ Nee, bedankt
+ Link gekopieerd
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-pl/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-pl/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..a1d0847f7c8c
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-pl/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Pobierz przeglądarkę komputerową
+ Chroń swoje dane osobowe także na Macu i Windowsie!
+ Aby pobrać DuckDuckGo na komputer Mac lub Windows, odwiedź stronę:
+ duckduckgo.com/browser
+ Udostępnij link pobierania
+ Nie, dziękuję
+ Skopiowano łącze
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-pt/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-pt/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..78d21a3173db
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-pt/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Obter navegador para computador
+ Protege a tua informação pessoal também no Mac e Windows!
+ Para transferir o DuckDuckGo no Mac ou Windows, visita:
+ duckduckgo.com/browser
+ Partilhar link de transferência
+ Não, obrigado
+ Link copiado
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-ro/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-ro/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..68b002b4a686
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-ro/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Obține browserul pentru desktop
+ Protejează-ți informațiile personale și pe Mac și Windows!
+ Pentru a descărca DuckDuckGo pe Mac sau Windows, vizitează:
+ duckduckgo.com/browser
+ Trimite linkul de descărcare
+ Nu, mulțumesc
+ Link copiat
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-ru/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-ru/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..e40370d04ce7
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-ru/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Скачать настольный браузер
+ Защитите личную информацию на Mac и Windows!
+ Скачивайте DuckDuckGo:
+ duckduckgo.com/browser
+ Поделиться ссылкой для загрузки
+ Нет, спасибо
+ Ссылка скопирована
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-sk/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-sk/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..634dda7ab2a5
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-sk/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Získajte prehliadač pre desktop PC
+ Chráň si svoje osobné údaje na Macu aj vo Windows!
+ Ak si chceš stiahnuť aplikáciu DuckDuckGo v systéme Mac alebo Windows, navštív túto stránku:
+ duckduckgo.com/browser
+ Zdieľať odkaz na stiahnutie
+ Nie, ďakujem
+ Odkaz bol skopírovaný
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-sl/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-sl/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..33fe814e4216
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-sl/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Namesti namizni brskalnik
+ Zaščitite svoje osebne podatke tudi v računalnikih Mac in Windows!
+ Če želite prenesti DuckDuckGo v računalniku Mac ali Windows, obiščite:
+ duckduckgo.com/browser
+ Deli povezavo za prenos
+ Ne, hvala
+ Povezava je kopirana
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-sv/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-sv/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..b2b575c4a0df
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-sv/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Hämta webbläsare för dator
+ Skydda din personliga information på Mac och Windows också!
+ För att ladda ner DuckDuckGo på Mac eller Windows, besök:
+ duckduckgo.com/browser
+ Dela nedladdningslänk
+ Nej tack
+ Länken har kopierats
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-tr/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-tr/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..7c50ca4cfb72
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-tr/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Masaüstü Tarayıcısını Edinin
+ Mac ve Windows\'ta da kişisel bilgilerinizi koruyun!
+ DuckDuckGo\'yu Mac veya Windows\'ta indirmek için şu adresi ziyaret edin:
+ duckduckgo.com/browser
+ İndirme Bağlantısını Paylaş
+ Hayır Teşekkürler
+ Bağlantı kopyalandı
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values/dimens.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values/dimens.xml
new file mode 100644
index 000000000000..eec3b6bc6c6a
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values/dimens.xml
@@ -0,0 +1,4 @@
+
+
+ 600dp
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values/strings-desktop-app-promotion.xml b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values/strings-desktop-app-promotion.xml
new file mode 100644
index 000000000000..76d7f4a0b63b
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values/strings-desktop-app-promotion.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Get Desktop Browser
+ Protect your personal info on Mac and Windows too!
+ To download DuckDuckGo on Mac or Windows, visit:
+ duckduckgo.com/browser
+ Share Download Link
+ No Thanks
+ Link copied
+
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/test/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionViewModelTest.kt b/desktop-app-promotion/desktop-app-promotion-impl/src/test/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionViewModelTest.kt
new file mode 100644
index 000000000000..482ae15d36da
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/test/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionViewModelTest.kt
@@ -0,0 +1,265 @@
+/*
+ * 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.desktopapppromotion.impl
+
+import app.cash.turbine.test
+import com.duckduckgo.app.clipboard.ClipboardInteractor
+import com.duckduckgo.app.statistics.pixels.Pixel
+import com.duckduckgo.common.test.CoroutineTestRule
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionInteractionHandler.Interaction
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionParams
+import com.duckduckgo.desktopapppromotion.api.PixelConfig
+import com.duckduckgo.desktopapppromotion.api.PixelFireSpec
+import com.duckduckgo.desktopapppromotion.impl.DesktopAppPromotionViewModel.Command
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class DesktopAppPromotionViewModelTest {
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ private val pixelMock: Pixel = mock()
+ private val clipboardInteractorMock: ClipboardInteractor = mock()
+ private val interactionDispatcher = FakeInteractionDispatcher()
+
+ private fun createViewModel(
+ params: DesktopAppPromotionParams = DesktopAppPromotionParams(),
+ ) = DesktopAppPromotionViewModel(
+ params = params,
+ content = content,
+ pixel = pixelMock,
+ dispatchers = coroutineTestRule.testDispatcherProvider,
+ clipboardInteractor = clipboardInteractorMock,
+ interactionDispatcher = interactionDispatcher,
+ )
+
+ @Test
+ fun whenCreatedThenViewStateCarriesResolvedContent() = runTest {
+ createViewModel().viewState.test {
+ assertEquals(content, awaitItem().content)
+ }
+ }
+
+ @Test
+ fun whenShareClickedThenShareLinkCommandCarriesShareIntentBody() = runTest {
+ val testee = createViewModel()
+
+ testee.commands.test {
+ testee.onShareClicked()
+
+ val command = awaitItem() as Command.ShareLink
+ assertEquals(SHARE_BODY, command.shareText)
+ assertEquals(SHARE_TITLE, command.chooserTitle)
+ }
+ }
+
+ @Test
+ fun whenShareClickedAndNoShareIntentBodyThenShareLinkCommandCarriesBareUrl() = runTest {
+ val testee = DesktopAppPromotionViewModel(
+ params = DesktopAppPromotionParams(),
+ content = content.copy(shareIntentBody = null),
+ pixel = pixelMock,
+ dispatchers = coroutineTestRule.testDispatcherProvider,
+ clipboardInteractor = clipboardInteractorMock,
+ interactionDispatcher = interactionDispatcher,
+ )
+
+ testee.commands.test {
+ testee.onShareClicked()
+
+ val command = awaitItem() as Command.ShareLink
+ assertEquals(DOWNLOAD_URL, command.shareText)
+ }
+ }
+
+ @Test
+ fun whenLinkClickedAndSystemShowsNotificationThenDoNotEmitShowCopiedNotification() = runTest {
+ whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(true)
+ val testee = createViewModel()
+
+ testee.commands.test {
+ testee.onLinkClicked()
+ expectNoEvents()
+ }
+ }
+
+ @Test
+ fun whenLinkClickedAndSystemDoesNotShowNotificationThenEmitShowCopiedNotification() = runTest {
+ whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(false)
+ val testee = createViewModel()
+
+ testee.commands.test {
+ testee.onLinkClicked()
+
+ assertEquals(Command.ShowCopiedNotification, awaitItem())
+ }
+ }
+
+ @Test
+ fun whenLinkClickedThenCopiesTheAttributedUrl() = runTest {
+ whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(true)
+
+ createViewModel().onLinkClicked()
+
+ verify(clipboardInteractorMock).copyToClipboard(eq(DOWNLOAD_URL), eq(false))
+ }
+
+ @Test
+ fun whenDismissClickedThenEmitClose() = runTest {
+ val testee = createViewModel()
+
+ testee.commands.test {
+ testee.onDismissClicked()
+
+ assertEquals(Command.Close, awaitItem())
+ }
+ }
+
+ @Test
+ fun whenBackPressedThenEmitCloseWithoutPixelOrHandler() = runTest {
+ val testee = createViewModel(params = paramsWithAllPixels())
+
+ testee.commands.test {
+ testee.onBackPressed()
+
+ assertEquals(Command.Close, awaitItem())
+ }
+
+ assertTrue(interactionDispatcher.dispatched.isEmpty())
+ verify(pixelMock, never()).fire(eq(DISMISS_PIXEL), any(), any(), any())
+ }
+
+ @Test
+ fun whenImpressionSpecPresentThenImpressionPixelFiresOnceOnCreation() = runTest {
+ createViewModel(params = paramsWithAllPixels())
+
+ verify(pixelMock).fire(eq(IMPRESSION_PIXEL), eq(hashMapOf("source" to "test")), any(), any())
+ }
+
+ @Test
+ fun whenSpecsPresentThenEachInteractionFiresItsConfiguredPixel() = runTest {
+ whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(true)
+ val testee = createViewModel(params = paramsWithAllPixels())
+
+ testee.onShareClicked()
+ testee.onLinkClicked()
+ testee.onDismissClicked()
+
+ verify(pixelMock).fire(eq(SHARE_PIXEL), eq(hashMapOf("source" to "test")), any(), any())
+ verify(pixelMock).fire(eq(LINK_PIXEL), eq(hashMapOf("source" to "test")), any(), any())
+ verify(pixelMock).fire(eq(DISMISS_PIXEL), eq(hashMapOf("source" to "test")), any(), any())
+ }
+
+ @Test
+ fun whenSpecsAbsentThenNoPixelIsFiredForAnyInteraction() = runTest {
+ whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(true)
+ val testee = createViewModel(params = DesktopAppPromotionParams(pixels = PixelConfig()))
+
+ testee.onShareClicked()
+ testee.onLinkClicked()
+ testee.onDismissClicked()
+
+ verify(pixelMock, never()).fire(any(), any(), any(), any())
+ }
+
+ @Test
+ fun whenInteractionsHappenThenTheyAreDispatchedWithTheLaunchHandlerId() = runTest {
+ whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(true)
+ val testee = createViewModel(params = DesktopAppPromotionParams(handlerId = HANDLER_ID))
+
+ testee.onLinkClicked()
+ testee.onDismissClicked()
+
+ assertEquals(
+ listOf(HANDLER_ID to Interaction.LINK_COPIED, HANDLER_ID to Interaction.DISMISSED),
+ interactionDispatcher.dispatched,
+ )
+ }
+
+ @Test
+ fun whenLaunchCarriesNoHandlerIdThenNullIsDispatchedAndNoHandlerCanMatch() = runTest {
+ whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(true)
+ val testee = createViewModel(params = DesktopAppPromotionParams(handlerId = null))
+
+ testee.onLinkClicked()
+ testee.onDismissClicked()
+
+ assertEquals(
+ listOf(null to Interaction.LINK_COPIED, null to Interaction.DISMISSED),
+ interactionDispatcher.dispatched,
+ )
+ }
+
+ private fun paramsWithAllPixels() = DesktopAppPromotionParams(
+ handlerId = HANDLER_ID,
+ pixels = PixelConfig(
+ impression = PixelFireSpec(IMPRESSION_PIXEL, sourceParams()),
+ shareClicked = PixelFireSpec(SHARE_PIXEL, sourceParams()),
+ linkClicked = PixelFireSpec(LINK_PIXEL, sourceParams()),
+ dismissed = PixelFireSpec(DISMISS_PIXEL, sourceParams()),
+ ),
+ )
+
+ private fun sourceParams() = hashMapOf("source" to "test")
+
+ private class FakeInteractionDispatcher : DesktopAppPromotionInteractionDispatcher {
+ val dispatched = mutableListOf>()
+
+ override suspend fun dispatch(
+ handlerId: String?,
+ interaction: Interaction,
+ ) {
+ dispatched += handlerId to interaction
+ }
+ }
+
+ companion object {
+ private const val HANDLER_ID = "test_handler"
+ private const val DOWNLOAD_URL = "https://duckduckgo.com/browser?origin=funnel_test"
+ private const val SHARE_BODY = "Get DuckDuckGo: https://duckduckgo.com/browser?origin=funnel_test"
+ private const val SHARE_TITLE = "Share Download Link"
+ private const val IMPRESSION_PIXEL = "m_test_impression"
+ private const val SHARE_PIXEL = "m_test_share"
+ private const val LINK_PIXEL = "m_test_link"
+ private const val DISMISS_PIXEL = "m_test_dismiss"
+
+ private val content = DesktopAppPromotionContent(
+ toolbarTitle = "Get Desktop Browser",
+ title = "Protect your personal info on Mac and Windows too!",
+ body = "To download DuckDuckGo on Mac or Windows, visit:",
+ illustration = 1,
+ downloadUrlDisplay = "duckduckgo.com/browser",
+ downloadUrl = DOWNLOAD_URL,
+ shareButtonLabel = SHARE_TITLE,
+ shareIntentTitle = SHARE_TITLE,
+ shareIntentBody = SHARE_BODY,
+ showDismissButton = true,
+ dismissButtonLabel = "No Thanks",
+ linkCopiedMessage = "Link copied",
+ )
+ }
+}
diff --git a/desktop-app-promotion/desktop-app-promotion-impl/src/test/java/com/duckduckgo/desktopapppromotion/impl/RealDesktopAppPromotionInteractionDispatcherTest.kt b/desktop-app-promotion/desktop-app-promotion-impl/src/test/java/com/duckduckgo/desktopapppromotion/impl/RealDesktopAppPromotionInteractionDispatcherTest.kt
new file mode 100644
index 000000000000..cdb5d2cfa4fa
--- /dev/null
+++ b/desktop-app-promotion/desktop-app-promotion-impl/src/test/java/com/duckduckgo/desktopapppromotion/impl/RealDesktopAppPromotionInteractionDispatcherTest.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.desktopapppromotion.impl
+
+import com.duckduckgo.common.utils.plugins.PluginPoint
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionInteractionHandler
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionInteractionHandler.Interaction
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class RealDesktopAppPromotionInteractionDispatcherTest {
+
+ private val settingsHandler = FakeInteractionHandler("settings_desktop_browser")
+ private val otherHandler = FakeInteractionHandler("some_other_caller")
+
+ private val testee = RealDesktopAppPromotionInteractionDispatcher(
+ handlers = FakePluginPoint(listOf(settingsHandler, otherHandler)),
+ )
+
+ @Test
+ fun whenHandlerIdMatchesThenOnlyThatHandlerIsNotified() = runTest {
+ testee.dispatch("settings_desktop_browser", Interaction.DISMISSED)
+
+ assertEquals(listOf(Interaction.DISMISSED), settingsHandler.interactions)
+ assertTrue(otherHandler.interactions.isEmpty())
+ }
+
+ @Test
+ fun whenHandlerIdIsNullThenNoHandlerIsNotified() = runTest {
+ testee.dispatch(null, Interaction.DISMISSED)
+
+ assertTrue(settingsHandler.interactions.isEmpty())
+ assertTrue(otherHandler.interactions.isEmpty())
+ }
+
+ @Test
+ fun whenHandlerIdMatchesNoRegisteredHandlerThenNoHandlerIsNotified() = runTest {
+ testee.dispatch("nobody_contributed_this", Interaction.SHARE_COMPLETED)
+
+ assertTrue(settingsHandler.interactions.isEmpty())
+ assertTrue(otherHandler.interactions.isEmpty())
+ }
+
+ @Test
+ fun whenEveryInteractionIsDispatchedThenAllReachTheMatchingHandler() = runTest {
+ testee.dispatch("settings_desktop_browser", Interaction.LINK_COPIED)
+ testee.dispatch("settings_desktop_browser", Interaction.SHARE_COMPLETED)
+ testee.dispatch("settings_desktop_browser", Interaction.DISMISSED)
+
+ assertEquals(
+ listOf(Interaction.LINK_COPIED, Interaction.SHARE_COMPLETED, Interaction.DISMISSED),
+ settingsHandler.interactions,
+ )
+ }
+
+ private class FakeInteractionHandler(override val handlerId: String) : DesktopAppPromotionInteractionHandler {
+ val interactions = mutableListOf()
+
+ override suspend fun onInteraction(interaction: Interaction) {
+ interactions += interaction
+ }
+ }
+
+ private class FakePluginPoint(
+ private val plugins: List,
+ ) : PluginPoint {
+ override fun getPlugins(): Collection = plugins
+ }
+}
diff --git a/sync/sync-impl/build.gradle b/sync/sync-impl/build.gradle
index 481fb99b469a..53551595965b 100644
--- a/sync/sync-impl/build.gradle
+++ b/sync/sync-impl/build.gradle
@@ -45,6 +45,7 @@ dependencies {
implementation project(path: ':saved-sites-api')
implementation project(':feature-toggles-api')
implementation project(':navigation-api')
+ implementation project(':desktop-app-promotion-api')
implementation project(':remote-messaging-api')
implementation project(path: ':autofill-api')
implementation project(path: ':settings-api') // temporary until we release new settings
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/promotion/SyncDesktopAppPromotionLauncher.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/promotion/SyncDesktopAppPromotionLauncher.kt
new file mode 100644
index 000000000000..c3f97308ce67
--- /dev/null
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/promotion/SyncDesktopAppPromotionLauncher.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.sync.impl.promotion
+
+import android.content.Context
+import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionParams
+import com.duckduckgo.desktopapppromotion.api.PixelConfig
+import com.duckduckgo.desktopapppromotion.api.PixelFireSpec
+import com.duckduckgo.di.scopes.AppScope
+import com.duckduckgo.navigation.api.GlobalActivityStarter
+import com.duckduckgo.settings.api.SettingsPageFeature
+import com.duckduckgo.sync.impl.R
+import com.duckduckgo.sync.impl.pixels.SyncPixelName
+import com.duckduckgo.sync.impl.pixels.SyncPixelParameters.GET_OTHER_DEVICES_SCREEN_LAUNCH_SOURCE
+import com.squareup.anvil.annotations.ContributesBinding
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+import com.duckduckgo.mobile.android.R as CommonR
+
+/**
+ * Sends the user to either the shared desktop-app promo screen or Sync's own multi-platform screen.
+ * The choice is made here, at the point of launch, so the screen the user lands on is the first one
+ * they see.
+ */
+interface SyncDesktopAppPromotionLauncher {
+ suspend fun launch(
+ context: Context,
+ source: SyncGetOnOtherPlatformsLaunchSource,
+ )
+}
+
+@ContributesBinding(AppScope::class)
+class RealSyncDesktopAppPromotionLauncher @Inject constructor(
+ private val globalActivityStarter: GlobalActivityStarter,
+ private val settingsPageFeature: SettingsPageFeature,
+ private val dispatchers: DispatcherProvider,
+) : SyncDesktopAppPromotionLauncher {
+
+ override suspend fun launch(
+ context: Context,
+ source: SyncGetOnOtherPlatformsLaunchSource,
+ ) {
+ val desktopBrowserPromoEnabled = withContext(dispatchers.io()) {
+ settingsPageFeature.newDesktopBrowserSettingEnabled().isEnabled()
+ }
+
+ val params = if (desktopBrowserPromoEnabled) {
+ desktopAppPromotionParams(context, source)
+ } else {
+ SyncGetOnOtherPlatformsParams(source)
+ }
+
+ globalActivityStarter.start(context, params)
+ }
+
+ private fun desktopAppPromotionParams(
+ context: Context,
+ source: SyncGetOnOtherPlatformsLaunchSource,
+ ): DesktopAppPromotionParams {
+ val sourceParams = hashMapOf(GET_OTHER_DEVICES_SCREEN_LAUNCH_SOURCE to source.value)
+
+ return DesktopAppPromotionParams(
+ toolbarTitle = context.getString(R.string.syncGetAppsOnOtherPlatformsActivityTitle),
+ title = context.getString(R.string.syncGetAppsOnOtherPlatformsTitle),
+ body = context.getString(R.string.syncGetAppsOnOtherPlatformInstruction),
+ illustration = CommonR.drawable.ic_app_download_128,
+ downloadUrl = DESKTOP_BROWSER_URL,
+ shareIntentTitle = context.getString(R.string.syncGetAppsOnOtherPlatforms),
+ showDismissButton = false,
+ pixels = PixelConfig(
+ impression = PixelFireSpec(SyncPixelName.SYNC_GET_OTHER_DEVICES_SCREEN_SHOWN.pixelName, sourceParams),
+ shareClicked = PixelFireSpec(SyncPixelName.SYNC_GET_OTHER_DEVICES_LINK_SHARED.pixelName, sourceParams),
+ linkClicked = PixelFireSpec(SyncPixelName.SYNC_GET_OTHER_DEVICES_LINK_COPIED.pixelName, sourceParams),
+ ),
+ )
+ }
+
+ companion object {
+ private const val DESKTOP_BROWSER_URL = "https://duckduckgo.com/browser?origin=funnel_browser_android_sync"
+ }
+}
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/promotion/SyncGetOnOtherPlatformsActivity.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/promotion/SyncGetOnOtherPlatformsActivity.kt
index 33b439085ac9..82817b1203f9 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/promotion/SyncGetOnOtherPlatformsActivity.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/promotion/SyncGetOnOtherPlatformsActivity.kt
@@ -38,7 +38,6 @@ import com.duckduckgo.sync.impl.databinding.ActivitySyncGetOnOtherDevicesBinding
import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsViewModel.Command
import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsViewModel.Command.ShareLink
import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsViewModel.Command.ShowCopiedNotification
-import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsViewModel.ViewState
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -75,10 +74,6 @@ class SyncGetOnOtherPlatformsActivity : DuckDuckGoActivity() {
.onEach { executeCommand(it) }
.launchIn(lifecycleScope)
- viewModel.viewState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
- .onEach { renderViewState(it) }
- .launchIn(lifecycleScope)
-
setContentView(binding.root)
setupToolbar(binding.includeToolbar.toolbar)
@@ -107,12 +102,6 @@ class SyncGetOnOtherPlatformsActivity : DuckDuckGoActivity() {
}
}
- private fun renderViewState(viewState: ViewState) {
- if (viewState.showDesktopBrowserUrl) {
- binding.downloadLinkText.text = getString(R.string.getDesktopBrowserUrl)
- }
- }
-
private fun executeCommand(command: Command) {
when (command) {
is ShareLink -> launchSharePageChooser(command.link)
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/promotion/SyncGetOnOtherPlatformsViewModel.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/promotion/SyncGetOnOtherPlatformsViewModel.kt
index 881a6d027df5..5b49510bdc0f 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/promotion/SyncGetOnOtherPlatformsViewModel.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/promotion/SyncGetOnOtherPlatformsViewModel.kt
@@ -23,18 +23,14 @@ import com.duckduckgo.app.clipboard.ClipboardInteractor
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
-import com.duckduckgo.settings.api.SettingsPageFeature
import com.duckduckgo.sync.impl.pixels.SyncPixelName
import com.duckduckgo.sync.impl.pixels.SyncPixelParameters.GET_OTHER_DEVICES_SCREEN_LAUNCH_SOURCE
import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsViewModel.Command.ShareLink
import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsViewModel.Command.ShowCopiedNotification
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import javax.inject.Inject
@ContributesViewModel(ActivityScope::class)
@@ -42,31 +38,16 @@ class SyncGetOnOtherPlatformsViewModel @Inject constructor(
private val pixel: Pixel,
private val dispatchers: DispatcherProvider,
private val clipboardInteractor: ClipboardInteractor,
- private val settingsPageFeature: SettingsPageFeature,
) : ViewModel() {
private val commandChannel = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val commands = commandChannel.receiveAsFlow()
- private val _viewState = MutableStateFlow(ViewState())
- val viewState = _viewState.asStateFlow()
-
sealed class Command {
data class ShareLink(val link: String) : Command()
data object ShowCopiedNotification : Command()
}
- data class ViewState(val showDesktopBrowserUrl: Boolean = false)
-
- init {
- viewModelScope.launch {
- val isEnabled = withContext(dispatchers.io()) {
- settingsPageFeature.newDesktopBrowserSettingEnabled().isEnabled()
- }
- _viewState.value = ViewState(showDesktopBrowserUrl = isEnabled)
- }
- }
-
fun onShareClicked(launchSource: String?) {
viewModelScope.launch(dispatchers.io()) {
commandChannel.send(ShareLink(buildLink()))
@@ -86,8 +67,7 @@ class SyncGetOnOtherPlatformsViewModel @Inject constructor(
}
private fun buildLink(): String {
- val baseLink = if (settingsPageFeature.newDesktopBrowserSettingEnabled().isEnabled()) DESKTOP_BROWSER_LINK else BASE_LINK
- return "$baseLink?$ATTRIBUTION"
+ return "$BASE_LINK?$ATTRIBUTION"
}
fun onScreenShownToUser(launchSource: String?) {
@@ -104,7 +84,6 @@ class SyncGetOnOtherPlatformsViewModel @Inject constructor(
companion object {
private const val BASE_LINK = "https://duckduckgo.com/app"
- private const val DESKTOP_BROWSER_LINK = "https://duckduckgo.com/browser"
private const val ATTRIBUTION = "origin=funnel_browser_android_sync"
}
}
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivity.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivity.kt
index 2a1d83aceacd..5d8f68fdd1c9 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivity.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivity.kt
@@ -55,8 +55,8 @@ import com.duckduckgo.sync.impl.auth.DeviceAuthenticator.AuthConfiguration
import com.duckduckgo.sync.impl.auth.DeviceAuthenticator.AuthResult.Success
import com.duckduckgo.sync.impl.databinding.ActivitySyncBinding
import com.duckduckgo.sync.impl.databinding.DialogEditDeviceBinding
+import com.duckduckgo.sync.impl.promotion.SyncDesktopAppPromotionLauncher
import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsLaunchSource
-import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsParams
import com.duckduckgo.sync.impl.ui.SyncActivityViewModel.Command
import com.duckduckgo.sync.impl.ui.SyncActivityViewModel.Command.AddAnotherDevice
import com.duckduckgo.sync.impl.ui.SyncActivityViewModel.Command.AskDeleteAccount
@@ -116,6 +116,9 @@ class SyncActivity : DuckDuckGoActivity() {
@Inject
lateinit var deviceAuthenticator: DeviceAuthenticator
+ @Inject
+ lateinit var syncDesktopAppPromotionLauncher: SyncDesktopAppPromotionLauncher
+
@Inject
lateinit var globalActivityStarter: GlobalActivityStarter
@@ -486,7 +489,7 @@ class SyncActivity : DuckDuckGoActivity() {
}
private fun launchSyncGetOnOtherPlatforms(source: SyncGetOnOtherPlatformsLaunchSource) {
- globalActivityStarter.start(this, SyncGetOnOtherPlatformsParams(source))
+ lifecycleScope.launch { syncDesktopAppPromotionLauncher.launch(this@SyncActivity, source) }
}
private fun showError(it: ShowError) {
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/setup/SetupAccountActivity.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/setup/SetupAccountActivity.kt
index fc16ce98f752..da29b262afeb 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/setup/SetupAccountActivity.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/setup/SetupAccountActivity.kt
@@ -35,8 +35,8 @@ import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.navigation.api.GlobalActivityStarter
import com.duckduckgo.sync.impl.R.id
import com.duckduckgo.sync.impl.databinding.ActivitySyncSetupAccountBinding
+import com.duckduckgo.sync.impl.promotion.SyncDesktopAppPromotionLauncher
import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsLaunchSource
-import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsParams
import com.duckduckgo.sync.impl.ui.setup.SetupAccountActivity.Companion.Screen.PREVIOUS_SESSION_READY
import com.duckduckgo.sync.impl.ui.setup.SetupAccountActivity.Companion.Screen.RECOVERY_CODE
import com.duckduckgo.sync.impl.ui.setup.SetupAccountActivity.Companion.Screen.RECOVERY_INTRO
@@ -59,6 +59,7 @@ import com.duckduckgo.sync.impl.ui.setup.SetupAccountViewModel.ViewMode.SyncSetu
import com.duckduckgo.sync.impl.ui.setup.SetupAccountViewModel.ViewState
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import javax.inject.Inject
@InjectWith(ActivityScope::class)
@@ -69,6 +70,9 @@ class SetupAccountActivity : DuckDuckGoActivity(), SyncSetupNavigationFlowListen
@Inject
lateinit var globalActivityStarter: GlobalActivityStarter
+ @Inject
+ lateinit var syncDesktopAppPromotionLauncher: SyncDesktopAppPromotionLauncher
+
@Inject
lateinit var edgeToEdgeProvider: EdgeToEdgeProvider
@@ -249,7 +253,7 @@ class SetupAccountActivity : DuckDuckGoActivity(), SyncSetupNavigationFlowListen
}
private fun launchSyncGetOnOtherPlatforms(source: SyncGetOnOtherPlatformsLaunchSource) {
- globalActivityStarter.start(this, SyncGetOnOtherPlatformsParams(source))
+ lifecycleScope.launch { syncDesktopAppPromotionLauncher.launch(this@SetupAccountActivity, source) }
}
private fun extractSource(): String? {
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/v2/SyncActivity.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/v2/SyncActivity.kt
index 00ece50ed6c6..911a405854a5 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/v2/SyncActivity.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/v2/SyncActivity.kt
@@ -61,7 +61,7 @@ import com.duckduckgo.sync.impl.auth.DeviceAuthenticator.AuthResult.Error
import com.duckduckgo.sync.impl.auth.DeviceAuthenticator.AuthResult.Success
import com.duckduckgo.sync.impl.auth.DeviceAuthenticator.AuthResult.UserCancelled
import com.duckduckgo.sync.impl.databinding.ActivitySyncV2Binding
-import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsParams
+import com.duckduckgo.sync.impl.promotion.SyncDesktopAppPromotionLauncher
import com.duckduckgo.sync.impl.ui.DeviceUnsupportedActivity
import com.duckduckgo.sync.impl.ui.SyncActivityViewModel
import com.duckduckgo.sync.impl.ui.SyncActivityViewModel.Command
@@ -110,6 +110,9 @@ class SyncActivity : DuckDuckGoActivity() {
@Inject
lateinit var syncSettingsPlugin: DaggerMap
+ @Inject
+ lateinit var syncDesktopAppPromotionLauncher: SyncDesktopAppPromotionLauncher
+
@Inject
lateinit var syncMessagesPlugin: DaggerSet
@@ -528,7 +531,7 @@ class SyncActivity : DuckDuckGoActivity() {
}
is LaunchSyncGetOnOtherPlatforms -> {
- globalActivityStarter.start(this, SyncGetOnOtherPlatformsParams(command.source))
+ lifecycleScope.launch { syncDesktopAppPromotionLauncher.launch(this@SyncActivity, command.source) }
}
is RecoveryCodePDFSuccess -> {
diff --git a/sync/sync-impl/src/main/res/values-bg/strings-sync.xml b/sync/sync-impl/src/main/res/values-bg/strings-sync.xml
index dd2c8c05b2c4..07bffab39974 100644
--- a/sync/sync-impl/src/main/res/values-bg/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-bg/strings-sync.xml
@@ -221,7 +221,6 @@
Вземете браузъра за работен плот
DuckDuckGo за Mac и Windows
- duckduckgo.com/browser
Възстановяване
diff --git a/sync/sync-impl/src/main/res/values-cs/strings-sync.xml b/sync/sync-impl/src/main/res/values-cs/strings-sync.xml
index 94ad59db80fe..e444725b26ba 100644
--- a/sync/sync-impl/src/main/res/values-cs/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-cs/strings-sync.xml
@@ -221,7 +221,6 @@
Nainstalovat prohlížeč pro počítač
DuckDuckGo pro Mac a Windows
- duckduckgo.com/browser
Obnova
diff --git a/sync/sync-impl/src/main/res/values-da/strings-sync.xml b/sync/sync-impl/src/main/res/values-da/strings-sync.xml
index 1fea9ad82969..84793dbda1c3 100644
--- a/sync/sync-impl/src/main/res/values-da/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-da/strings-sync.xml
@@ -221,7 +221,6 @@
Hent browser til computeren
DuckDuckGo til Mac og Windows
- duckduckgo.com/browser
Gendannelse
diff --git a/sync/sync-impl/src/main/res/values-de/strings-sync.xml b/sync/sync-impl/src/main/res/values-de/strings-sync.xml
index 505087aa6cb6..4f61a55b6eaf 100644
--- a/sync/sync-impl/src/main/res/values-de/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-de/strings-sync.xml
@@ -221,7 +221,6 @@
Desktop-Browser herunterladen
DuckDuckGo für Mac und Windows
- duckduckgo.com/browser
Wiederherstellung
diff --git a/sync/sync-impl/src/main/res/values-el/strings-sync.xml b/sync/sync-impl/src/main/res/values-el/strings-sync.xml
index c9d685bbc84b..e9f999bd46f9 100644
--- a/sync/sync-impl/src/main/res/values-el/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-el/strings-sync.xml
@@ -221,7 +221,6 @@
Αποκτήστε το πρόγραμμα περιήγησης για υπολογιστές
DuckDuckGo για Mac και Windows
- duckduckgo.com/browser
Ανάκτηση
diff --git a/sync/sync-impl/src/main/res/values-es/strings-sync.xml b/sync/sync-impl/src/main/res/values-es/strings-sync.xml
index 7d56bb798a8d..a2fb759e2d54 100644
--- a/sync/sync-impl/src/main/res/values-es/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-es/strings-sync.xml
@@ -221,7 +221,6 @@
Obtén el navegador de escritorio
DuckDuckGo para Mac y Windows
- duckduckgo.com/browser
Recuperación
diff --git a/sync/sync-impl/src/main/res/values-et/strings-sync.xml b/sync/sync-impl/src/main/res/values-et/strings-sync.xml
index e7161149e9e2..bc4fe2da0661 100644
--- a/sync/sync-impl/src/main/res/values-et/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-et/strings-sync.xml
@@ -221,7 +221,6 @@
Hangi töölaua brauser
DuckDuckGo Maci ja Windowsi jaoks
- duckduckgo.com/browser
Taastamine
diff --git a/sync/sync-impl/src/main/res/values-fi/strings-sync.xml b/sync/sync-impl/src/main/res/values-fi/strings-sync.xml
index 579a3832e665..4dbe413c27ce 100644
--- a/sync/sync-impl/src/main/res/values-fi/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-fi/strings-sync.xml
@@ -221,7 +221,6 @@
Hanki pöytäkoneselain
DuckDuckGo Mac- ja Windows-laitteeseen
- duckduckgo.com/browser
Palautus
diff --git a/sync/sync-impl/src/main/res/values-fr/strings-sync.xml b/sync/sync-impl/src/main/res/values-fr/strings-sync.xml
index 5e5291ff14c6..2d08afeaeea6 100644
--- a/sync/sync-impl/src/main/res/values-fr/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-fr/strings-sync.xml
@@ -221,7 +221,6 @@
Télécharger le navigateur de bureau
DuckDuckGo pour Mac et Windows
- duckduckgo.com/browser
Récupération
diff --git a/sync/sync-impl/src/main/res/values-hr/strings-sync.xml b/sync/sync-impl/src/main/res/values-hr/strings-sync.xml
index e948144bfc65..b0d0f187983e 100644
--- a/sync/sync-impl/src/main/res/values-hr/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-hr/strings-sync.xml
@@ -221,7 +221,6 @@
Nabavi preglednik za PC
DuckDuckGo za Mac i Windows
- duckduckgo.com/browser
Oporavak
diff --git a/sync/sync-impl/src/main/res/values-hu/strings-sync.xml b/sync/sync-impl/src/main/res/values-hu/strings-sync.xml
index bf5ee43db736..adc4a22257a8 100644
--- a/sync/sync-impl/src/main/res/values-hu/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-hu/strings-sync.xml
@@ -221,7 +221,6 @@
Asztali böngésző letöltése
DuckDuckGo Mac és Windows rendszerre
- duckduckgo.com/browser
Helyreállítás
diff --git a/sync/sync-impl/src/main/res/values-it/strings-sync.xml b/sync/sync-impl/src/main/res/values-it/strings-sync.xml
index afe43f79b674..51dce3776821 100644
--- a/sync/sync-impl/src/main/res/values-it/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-it/strings-sync.xml
@@ -221,7 +221,6 @@
Scarica il browser per desktop
DuckDuckGo per Mac e Windows
- duckduckgo.com/browser
Recupero
diff --git a/sync/sync-impl/src/main/res/values-lt/strings-sync.xml b/sync/sync-impl/src/main/res/values-lt/strings-sync.xml
index 1b5d051df483..64e6c1801462 100644
--- a/sync/sync-impl/src/main/res/values-lt/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-lt/strings-sync.xml
@@ -221,7 +221,6 @@
Gaukite kompiuterio naršyklę
„DuckDuckGo“, skirta „Mac“ ir „Windows“
- duckduckgo.com/browser
Atkūrimas
diff --git a/sync/sync-impl/src/main/res/values-lv/strings-sync.xml b/sync/sync-impl/src/main/res/values-lv/strings-sync.xml
index 9a22ef09a912..e9c90a5b5a0e 100644
--- a/sync/sync-impl/src/main/res/values-lv/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-lv/strings-sync.xml
@@ -221,7 +221,6 @@
Iegūsti galddatora pārlūku
DuckDuckGo operētājsistēmām Mac un Windows
- duckduckgo.com/browser
Atgūšana
diff --git a/sync/sync-impl/src/main/res/values-nb/strings-sync.xml b/sync/sync-impl/src/main/res/values-nb/strings-sync.xml
index e7c58b655d9e..e5844d758a62 100644
--- a/sync/sync-impl/src/main/res/values-nb/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-nb/strings-sync.xml
@@ -221,7 +221,6 @@
Skaff deg nettleseren for datamaskin
DuckDuckGo for Mac og Windows
- duckduckgo.com/browser
Gjenoppretting
diff --git a/sync/sync-impl/src/main/res/values-nl/strings-sync.xml b/sync/sync-impl/src/main/res/values-nl/strings-sync.xml
index 3561aae2acf6..435f31a1cd31 100644
--- a/sync/sync-impl/src/main/res/values-nl/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-nl/strings-sync.xml
@@ -221,7 +221,6 @@
Download de desktopbrowser
DuckDuckGo voor Mac en Windows
- duckduckgo.com/browser
Herstellen
diff --git a/sync/sync-impl/src/main/res/values-pl/strings-sync.xml b/sync/sync-impl/src/main/res/values-pl/strings-sync.xml
index 12bcb59f7c26..1fd2c9ff3c31 100644
--- a/sync/sync-impl/src/main/res/values-pl/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-pl/strings-sync.xml
@@ -221,7 +221,6 @@
Pobierz przeglądarkę komputerową
DuckDuckGo dla komputerów Mac i Windows
- duckduckgo.com/browser
Odzyskiwanie
diff --git a/sync/sync-impl/src/main/res/values-pt/strings-sync.xml b/sync/sync-impl/src/main/res/values-pt/strings-sync.xml
index 0dfe2588bd7a..8da5163302bf 100644
--- a/sync/sync-impl/src/main/res/values-pt/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-pt/strings-sync.xml
@@ -221,7 +221,6 @@
Obter navegador para computador
DuckDuckGo para Mac e Windows
- duckduckgo.com/browser
Recuperação
diff --git a/sync/sync-impl/src/main/res/values-ro/strings-sync.xml b/sync/sync-impl/src/main/res/values-ro/strings-sync.xml
index 558dfaac155a..a238196d89d9 100644
--- a/sync/sync-impl/src/main/res/values-ro/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-ro/strings-sync.xml
@@ -221,7 +221,6 @@
Obține browserul pentru desktop
DuckDuckGo pentru Mac și Windows
- duckduckgo.com/browser
Recuperare
diff --git a/sync/sync-impl/src/main/res/values-ru/strings-sync.xml b/sync/sync-impl/src/main/res/values-ru/strings-sync.xml
index 790eb29ce1b3..b8f2494c9668 100644
--- a/sync/sync-impl/src/main/res/values-ru/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-ru/strings-sync.xml
@@ -221,7 +221,6 @@
Скачать настольный браузер
DuckDuckGo для Mac и Windows
- duckduckgo.com/browser
Восстановление данных
diff --git a/sync/sync-impl/src/main/res/values-sk/strings-sync.xml b/sync/sync-impl/src/main/res/values-sk/strings-sync.xml
index 0df819b893fa..c28b5ec4c4f8 100644
--- a/sync/sync-impl/src/main/res/values-sk/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-sk/strings-sync.xml
@@ -221,7 +221,6 @@
Získajte prehliadač pre desktop PC
DuckDuckGo pre Mac a Windows
- duckduckgo.com/browser
Obnoviť
diff --git a/sync/sync-impl/src/main/res/values-sl/strings-sync.xml b/sync/sync-impl/src/main/res/values-sl/strings-sync.xml
index ca9a62722b55..0fe40e3cfe69 100644
--- a/sync/sync-impl/src/main/res/values-sl/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-sl/strings-sync.xml
@@ -221,7 +221,6 @@
Namesti namizni brskalnik
DuckDuckGo za računalnike Mac in Windows
- duckduckgo.com/browser
Obnovitev
diff --git a/sync/sync-impl/src/main/res/values-sv/strings-sync.xml b/sync/sync-impl/src/main/res/values-sv/strings-sync.xml
index c39b9d425fe6..37d6edc7b48c 100644
--- a/sync/sync-impl/src/main/res/values-sv/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-sv/strings-sync.xml
@@ -221,7 +221,6 @@
Hämta webbläsare för dator
DuckDuckGo för Mac och Windows
- duckduckgo.com/browser
Återställning
diff --git a/sync/sync-impl/src/main/res/values-tr/strings-sync.xml b/sync/sync-impl/src/main/res/values-tr/strings-sync.xml
index ca465ef967bd..0a79926cf47a 100644
--- a/sync/sync-impl/src/main/res/values-tr/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values-tr/strings-sync.xml
@@ -221,7 +221,6 @@
Masaüstü Tarayıcısını Edinin
Mac ve Windows için DuckDuckGo
- duckduckgo.com/browser
Kurtarma
diff --git a/sync/sync-impl/src/main/res/values/strings-sync.xml b/sync/sync-impl/src/main/res/values/strings-sync.xml
index 9781969dd5c9..4572b53ff09e 100644
--- a/sync/sync-impl/src/main/res/values/strings-sync.xml
+++ b/sync/sync-impl/src/main/res/values/strings-sync.xml
@@ -221,7 +221,6 @@
Get Desktop Browser
DuckDuckGo for Mac and Windows
- duckduckgo.com/browser
Recovery
diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/promotion/RealSyncDesktopAppPromotionLauncherTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/promotion/RealSyncDesktopAppPromotionLauncherTest.kt
new file mode 100644
index 000000000000..7ff5a1f16ce5
--- /dev/null
+++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/promotion/RealSyncDesktopAppPromotionLauncherTest.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.sync.impl.promotion
+
+import android.annotation.SuppressLint
+import android.content.Context
+import com.duckduckgo.common.test.CoroutineTestRule
+import com.duckduckgo.desktopapppromotion.api.DesktopAppPromotionParams
+import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
+import com.duckduckgo.feature.toggles.api.Toggle.State
+import com.duckduckgo.navigation.api.GlobalActivityStarter
+import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
+import com.duckduckgo.settings.api.SettingsPageFeature
+import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsLaunchSource.SOURCE_SYNC_ENABLED
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SuppressLint("DenyListedApi")
+class RealSyncDesktopAppPromotionLauncherTest {
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ private val globalActivityStarterMock: GlobalActivityStarter = mock()
+ private val contextMock: Context = mock()
+ private val fakeSettingsPageFeature = FakeFeatureToggleFactory.create(SettingsPageFeature::class.java)
+
+ private val testee = RealSyncDesktopAppPromotionLauncher(
+ globalActivityStarter = globalActivityStarterMock,
+ settingsPageFeature = fakeSettingsPageFeature,
+ dispatchers = coroutineTestRule.testDispatcherProvider,
+ )
+
+ @Test
+ fun whenDesktopBrowserPromoDisabledThenSyncOwnScreenIsLaunched() = runTest {
+ fakeSettingsPageFeature.newDesktopBrowserSettingEnabled().setRawStoredState(State(false))
+
+ testee.launch(contextMock, SOURCE_SYNC_ENABLED)
+
+ verify(globalActivityStarterMock).start(
+ eq(contextMock),
+ eq(SyncGetOnOtherPlatformsParams(SOURCE_SYNC_ENABLED)),
+ anyOrNull(),
+ )
+ }
+
+ @Test
+ fun whenDesktopBrowserPromoEnabledThenSharedPromoScreenIsLaunchedWithSyncAttribution() = runTest {
+ givenPromoEnabled()
+
+ testee.launch(contextMock, SOURCE_SYNC_ENABLED)
+
+ assertEquals("https://duckduckgo.com/browser?origin=funnel_browser_android_sync", capturedParams().downloadUrl)
+ }
+
+ @Test
+ fun whenSharedPromoScreenIsLaunchedThenSyncPixelNamesAndSourceAreUnchanged() = runTest {
+ givenPromoEnabled()
+
+ testee.launch(contextMock, SOURCE_SYNC_ENABLED)
+
+ val expectedSource = mapOf("source" to "activated")
+ with(capturedParams().pixels) {
+ assertEquals("sync_get_other_devices", impression?.pixelName)
+ assertEquals(expectedSource, impression?.parameters)
+ assertEquals("sync_get_other_devices_share", shareClicked?.pixelName)
+ assertEquals(expectedSource, shareClicked?.parameters)
+ assertEquals("sync_get_other_devices_copy", linkClicked?.pixelName)
+ assertEquals(expectedSource, linkClicked?.parameters)
+ assertEquals(null, dismissed)
+ }
+ }
+
+ @Test
+ fun whenSharedPromoScreenIsLaunchedThenNoDismissButtonAndNoInteractionHandler() = runTest {
+ givenPromoEnabled()
+
+ testee.launch(contextMock, SOURCE_SYNC_ENABLED)
+
+ val params = capturedParams()
+ assertEquals(false, params.showDismissButton)
+ assertEquals(null, params.handlerId)
+ }
+
+ private fun givenPromoEnabled() {
+ fakeSettingsPageFeature.newDesktopBrowserSettingEnabled().setRawStoredState(State(true))
+ whenever(contextMock.getString(any())).thenReturn("copy")
+ }
+
+ private fun capturedParams(): DesktopAppPromotionParams {
+ val captor = argumentCaptor()
+ verify(globalActivityStarterMock).start(eq(contextMock), captor.capture(), anyOrNull())
+ return captor.firstValue as DesktopAppPromotionParams
+ }
+}
diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/promotion/SyncGetOnOtherPlatformsViewModelTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/promotion/SyncGetOnOtherPlatformsViewModelTest.kt
index fe843b5c06a3..bbf57bf1be45 100644
--- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/promotion/SyncGetOnOtherPlatformsViewModelTest.kt
+++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/promotion/SyncGetOnOtherPlatformsViewModelTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2026 DuckDuckGo
+ * Copyright (c) 2023 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,14 @@
package com.duckduckgo.sync.impl.promotion
-import android.annotation.SuppressLint
import app.cash.turbine.test
import com.duckduckgo.app.clipboard.ClipboardInteractor
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.test.CoroutineTestRule
-import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
-import com.duckduckgo.feature.toggles.api.Toggle.State
-import com.duckduckgo.settings.api.SettingsPageFeature
import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsViewModel.Command.ShareLink
import com.duckduckgo.sync.impl.promotion.SyncGetOnOtherPlatformsViewModel.Command.ShowCopiedNotification
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
@@ -38,7 +32,6 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
-@SuppressLint("DenyListedApi")
class SyncGetOnOtherPlatformsViewModelTest {
@get:Rule
@@ -46,55 +39,17 @@ class SyncGetOnOtherPlatformsViewModelTest {
private val pixelMock: Pixel = mock()
private val clipboardInteractorMock: ClipboardInteractor = mock()
- private val fakeSettingsPageFeature = FakeFeatureToggleFactory.create(SettingsPageFeature::class.java)
private fun createViewModel(): SyncGetOnOtherPlatformsViewModel {
return SyncGetOnOtherPlatformsViewModel(
pixel = pixelMock,
dispatchers = coroutineTestRule.testDispatcherProvider,
clipboardInteractor = clipboardInteractorMock,
- settingsPageFeature = fakeSettingsPageFeature,
)
}
@Test
- fun whenDesktopBrowserFeatureEnabledThenViewStateShowsDesktopBrowserUrl() = runTest {
- fakeSettingsPageFeature.newDesktopBrowserSettingEnabled().setRawStoredState(State(true))
-
- val testee = createViewModel()
-
- testee.viewState.test {
- assertTrue(awaitItem().showDesktopBrowserUrl)
- }
- }
-
- @Test
- fun whenDesktopBrowserFeatureDisabledThenViewStateDoesNotShowDesktopBrowserUrl() = runTest {
- fakeSettingsPageFeature.newDesktopBrowserSettingEnabled().setRawStoredState(State(false))
-
- val testee = createViewModel()
-
- testee.viewState.test {
- assertFalse(awaitItem().showDesktopBrowserUrl)
- }
- }
-
- @Test
- fun whenShareClickedAndFeatureEnabledThenShareDesktopBrowserLink() = runTest {
- fakeSettingsPageFeature.newDesktopBrowserSettingEnabled().setRawStoredState(State(true))
- val testee = createViewModel()
-
- testee.commands.test {
- testee.onShareClicked(null)
-
- val command = awaitItem() as ShareLink
- assertEquals("https://duckduckgo.com/browser?origin=funnel_browser_android_sync", command.link)
- }
- }
-
- @Test
- fun whenShareClickedAndFeatureDisabledThenShareAppLink() = runTest {
- fakeSettingsPageFeature.newDesktopBrowserSettingEnabled().setRawStoredState(State(false))
+ fun whenShareClickedThenShareAppLink() = runTest {
val testee = createViewModel()
testee.commands.test {
@@ -106,22 +61,7 @@ class SyncGetOnOtherPlatformsViewModelTest {
}
@Test
- fun whenLinkClickedAndFeatureEnabledThenCopyDesktopBrowserLink() = runTest {
- fakeSettingsPageFeature.newDesktopBrowserSettingEnabled().setRawStoredState(State(true))
- whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(true)
- val testee = createViewModel()
-
- testee.onLinkClicked(null)
-
- verify(clipboardInteractorMock).copyToClipboard(
- eq("https://duckduckgo.com/browser?origin=funnel_browser_android_sync"),
- eq(false),
- )
- }
-
- @Test
- fun whenLinkClickedAndFeatureDisabledThenCopyAppLink() = runTest {
- fakeSettingsPageFeature.newDesktopBrowserSettingEnabled().setRawStoredState(State(false))
+ fun whenLinkClickedThenCopyAppLink() = runTest {
whenever(clipboardInteractorMock.copyToClipboard(any(), any())).thenReturn(true)
val testee = createViewModel()