From c80d6bd550351c58489ccf511563569c34e074f1 Mon Sep 17 00:00:00 2001 From: catalinradoiu Date: Thu, 20 Aug 2026 23:47:08 +0300 Subject: [PATCH] Consolidate desktop browser promo screens into one shared module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three separate activities promoted the desktop browser — one each in app, sync-impl and autofill-impl — so every change to the screen had to be made three times, and one of them was missed entirely last time it changed. Replace them with a single configuration-driven screen in a new desktop-app-promotion module. Callers supply their own copy, URL and pixel names; content fields left null fall back to the module's own translated defaults, which is also what the existing getDesktopBrowser deeplink now resolves to. Side effects stay with whoever owns them: the Settings "Complete your setup" dismissal is persisted by a handler contributed from app through the DesktopAppPromotionInteractionHandler plugin point, keyed so Sync and Autofill launches can never trigger it. Pixel names, parameters and attribution origins are unchanged, and Sync's own multi-platform screen stays for the newDesktopBrowserSettingEnabled off path. https://app.asana.com/1/137249556945/task/1213509274333252 Co-Authored-By: Claude Opus 5 (1M context) --- app/build.gradle | 3 + app/src/main/AndroidManifest.xml | 9 - .../GetDesktopBrowserActivity.kt | 198 ------------- .../GetDesktopBrowserViewModel.kt | 118 -------- .../GetDesktopBrowserCompleteSetupSettings.kt | 5 +- .../app/settings/SettingsActivity.kt | 3 +- .../SettingsDesktopBrowserPromotionHandler.kt | 90 ++++++ app/src/main/res/values-bg/strings.xml | 6 - app/src/main/res/values-cs/strings.xml | 6 - app/src/main/res/values-da/strings.xml | 6 - app/src/main/res/values-de/strings.xml | 6 - app/src/main/res/values-el/strings.xml | 6 - app/src/main/res/values-es/strings.xml | 6 - app/src/main/res/values-et/strings.xml | 6 - app/src/main/res/values-fi/strings.xml | 6 - app/src/main/res/values-fr/strings.xml | 6 - app/src/main/res/values-hr/strings.xml | 6 - app/src/main/res/values-hu/strings.xml | 6 - app/src/main/res/values-it/strings.xml | 6 - app/src/main/res/values-lt/strings.xml | 6 - app/src/main/res/values-lv/strings.xml | 6 - app/src/main/res/values-nb/strings.xml | 6 - app/src/main/res/values-nl/strings.xml | 6 - app/src/main/res/values-pl/strings.xml | 6 - app/src/main/res/values-pt/strings.xml | 6 - app/src/main/res/values-ro/strings.xml | 6 - app/src/main/res/values-ru/strings.xml | 6 - app/src/main/res/values-sk/strings.xml | 6 - app/src/main/res/values-sl/strings.xml | 6 - app/src/main/res/values-sv/strings.xml | 6 - app/src/main/res/values-tr/strings.xml | 6 - app/src/main/res/values/strings.xml | 6 - .../GetDesktopBrowserViewModelTest.kt | 179 ------------ ...tingsDesktopBrowserPromotionHandlerTest.kt | 60 ++++ ...ttingsDesktopBrowserPromotionParamsTest.kt | 95 +++++++ autofill/autofill-impl/build.gradle | 1 + .../src/main/AndroidManifest.xml | 4 - .../importpassword/ImportPasswordsActivity.kt | 3 +- ...mportPasswordsDesktopAppPromotionParams.kt | 52 ++++ .../ImportPasswordsGetDesktopAppActivity.kt | 133 --------- .../ImportPasswordsGetDesktopAppViewModel.kt | 81 ------ .../res/layout/activity_get_desktop_app.xml | 116 -------- .../res/values-bg/strings-autofill-impl.xml | 3 - .../res/values-cs/strings-autofill-impl.xml | 3 - .../res/values-da/strings-autofill-impl.xml | 3 - .../res/values-de/strings-autofill-impl.xml | 3 - .../res/values-el/strings-autofill-impl.xml | 3 - .../res/values-es/strings-autofill-impl.xml | 3 - .../res/values-et/strings-autofill-impl.xml | 3 - .../res/values-fi/strings-autofill-impl.xml | 3 - .../res/values-fr/strings-autofill-impl.xml | 3 - .../res/values-hr/strings-autofill-impl.xml | 3 - .../res/values-hu/strings-autofill-impl.xml | 3 - .../res/values-it/strings-autofill-impl.xml | 3 - .../res/values-lt/strings-autofill-impl.xml | 3 - .../res/values-lv/strings-autofill-impl.xml | 3 - .../res/values-nb/strings-autofill-impl.xml | 3 - .../res/values-nl/strings-autofill-impl.xml | 3 - .../res/values-pl/strings-autofill-impl.xml | 3 - .../res/values-pt/strings-autofill-impl.xml | 3 - .../res/values-ro/strings-autofill-impl.xml | 3 - .../res/values-ru/strings-autofill-impl.xml | 3 - .../res/values-sk/strings-autofill-impl.xml | 3 - .../res/values-sl/strings-autofill-impl.xml | 3 - .../res/values-sv/strings-autofill-impl.xml | 3 - .../res/values-tr/strings-autofill-impl.xml | 3 - .../main/res/values/strings-autofill-impl.xml | 3 - ...tPasswordsDesktopAppPromotionParamsTest.kt | 76 +++++ ...portPasswordsGetDesktopAppViewModelTest.kt | 81 ------ .../desktop-app-promotion-api/build.gradle | 32 +++ .../DesktopAppPromotionInteractionHandler.kt | 42 +++ .../api/DesktopAppPromotionParams.kt | 124 ++++++++ .../desktop-app-promotion-impl/build.gradle | 66 +++++ .../src/main/AndroidManifest.xml | 34 +++ .../impl/DesktopAppPromotionActivity.kt | 191 +++++++++++++ .../impl/DesktopAppPromotionContent.kt | 60 ++++ ...esktopAppPromotionInteractionDispatcher.kt | 53 ++++ ...pPromotionInteractionHandlerPluginPoint.kt | 28 ++ ...sktopAppPromotionShareBroadcastReceiver.kt | 26 +- .../impl/DesktopAppPromotionViewModel.kt | 119 ++++++++ .../drawable/image_get_desktop_browser.xml | 0 .../layout/activity_desktop_app_promotion.xml | 34 +-- .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../strings-desktop-app-promotion.xml | 27 ++ .../src/main/res/values/dimens.xml | 4 + .../values/strings-desktop-app-promotion.xml | 27 ++ .../impl/DesktopAppPromotionViewModelTest.kt | 265 ++++++++++++++++++ ...opAppPromotionInteractionDispatcherTest.kt | 85 ++++++ sync/sync-impl/build.gradle | 1 + .../SyncDesktopAppPromotionLauncher.kt | 96 +++++++ .../SyncGetOnOtherPlatformsActivity.kt | 11 - .../SyncGetOnOtherPlatformsViewModel.kt | 23 +- .../duckduckgo/sync/impl/ui/SyncActivity.kt | 7 +- .../impl/ui/setup/SetupAccountActivity.kt | 8 +- .../sync/impl/ui/v2/SyncActivity.kt | 7 +- .../src/main/res/values-bg/strings-sync.xml | 1 - .../src/main/res/values-cs/strings-sync.xml | 1 - .../src/main/res/values-da/strings-sync.xml | 1 - .../src/main/res/values-de/strings-sync.xml | 1 - .../src/main/res/values-el/strings-sync.xml | 1 - .../src/main/res/values-es/strings-sync.xml | 1 - .../src/main/res/values-et/strings-sync.xml | 1 - .../src/main/res/values-fi/strings-sync.xml | 1 - .../src/main/res/values-fr/strings-sync.xml | 1 - .../src/main/res/values-hr/strings-sync.xml | 1 - .../src/main/res/values-hu/strings-sync.xml | 1 - .../src/main/res/values-it/strings-sync.xml | 1 - .../src/main/res/values-lt/strings-sync.xml | 1 - .../src/main/res/values-lv/strings-sync.xml | 1 - .../src/main/res/values-nb/strings-sync.xml | 1 - .../src/main/res/values-nl/strings-sync.xml | 1 - .../src/main/res/values-pl/strings-sync.xml | 1 - .../src/main/res/values-pt/strings-sync.xml | 1 - .../src/main/res/values-ro/strings-sync.xml | 1 - .../src/main/res/values-ru/strings-sync.xml | 1 - .../src/main/res/values-sk/strings-sync.xml | 1 - .../src/main/res/values-sl/strings-sync.xml | 1 - .../src/main/res/values-sv/strings-sync.xml | 1 - .../src/main/res/values-tr/strings-sync.xml | 1 - .../src/main/res/values/strings-sync.xml | 1 - ...RealSyncDesktopAppPromotionLauncherTest.kt | 118 ++++++++ .../SyncGetOnOtherPlatformsViewModelTest.kt | 66 +---- 144 files changed, 2428 insertions(+), 1304 deletions(-) delete mode 100644 app/src/main/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserActivity.kt delete mode 100644 app/src/main/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserViewModel.kt create mode 100644 app/src/main/java/com/duckduckgo/app/settings/SettingsDesktopBrowserPromotionHandler.kt delete mode 100644 app/src/test/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserViewModelTest.kt create mode 100644 app/src/test/java/com/duckduckgo/app/settings/SettingsDesktopBrowserPromotionHandlerTest.kt create mode 100644 app/src/test/java/com/duckduckgo/app/settings/SettingsDesktopBrowserPromotionParamsTest.kt create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsDesktopAppPromotionParams.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/desktopapp/ImportPasswordsGetDesktopAppActivity.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/desktopapp/ImportPasswordsGetDesktopAppViewModel.kt delete mode 100644 autofill/autofill-impl/src/main/res/layout/activity_get_desktop_app.xml create mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsDesktopAppPromotionParamsTest.kt delete mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/desktopapp/ImportPasswordsGetDesktopAppViewModelTest.kt create mode 100644 desktop-app-promotion/desktop-app-promotion-api/build.gradle create mode 100644 desktop-app-promotion/desktop-app-promotion-api/src/main/java/com/duckduckgo/desktopapppromotion/api/DesktopAppPromotionInteractionHandler.kt create mode 100644 desktop-app-promotion/desktop-app-promotion-api/src/main/java/com/duckduckgo/desktopapppromotion/api/DesktopAppPromotionParams.kt create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/build.gradle create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/AndroidManifest.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionActivity.kt create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionContent.kt create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionInteractionDispatcher.kt create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionInteractionHandlerPluginPoint.kt rename app/src/main/java/com/duckduckgo/app/desktopbrowser/GetDesktopBrowserShareBroadcastReceiver.kt => desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionShareBroadcastReceiver.kt (58%) create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionViewModel.kt rename {app => desktop-app-promotion/desktop-app-promotion-impl}/src/main/res/drawable/image_get_desktop_browser.xml (100%) rename app/src/main/res/layout/activity_get_desktop_browser.xml => desktop-app-promotion/desktop-app-promotion-impl/src/main/res/layout/activity_desktop_app_promotion.xml (83%) create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-bg/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-cs/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-da/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-de/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-el/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-es/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-et/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-fi/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-fr/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-hr/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-hu/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-it/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-lt/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-lv/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-nb/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-nl/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-pl/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-pt/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-ro/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-ru/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-sk/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-sl/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-sv/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values-tr/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values/dimens.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/main/res/values/strings-desktop-app-promotion.xml create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/test/java/com/duckduckgo/desktopapppromotion/impl/DesktopAppPromotionViewModelTest.kt create mode 100644 desktop-app-promotion/desktop-app-promotion-impl/src/test/java/com/duckduckgo/desktopapppromotion/impl/RealDesktopAppPromotionInteractionDispatcherTest.kt create mode 100644 sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/promotion/SyncDesktopAppPromotionLauncher.kt create mode 100644 sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/promotion/RealSyncDesktopAppPromotionLauncherTest.kt 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()