diff --git a/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteViewModel.kt b/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteViewModel.kt index 3cccb8fad1f0..38304f12a77f 100644 --- a/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteViewModel.kt @@ -38,6 +38,7 @@ import com.duckduckgo.brokensite.api.ReportFlow as BrokenSiteModelReportFlow import com.duckduckgo.browser.api.brokensite.BrokenSiteData.ReportFlow import com.duckduckgo.browser.api.brokensite.BrokenSiteData.ReportFlow.DASHBOARD import com.duckduckgo.browser.api.brokensite.BrokenSiteData.ReportFlow.MENU +import com.duckduckgo.browser.api.brokensite.BrokenSiteData.ReportFlow.PROMPT import com.duckduckgo.browser.api.brokensite.BrokenSiteOpenerContext import com.duckduckgo.common.utils.SingleLiveEvent import com.duckduckgo.common.utils.extractDomain @@ -285,4 +286,5 @@ private fun MutableLiveData.setProtectionsState(state: SiteProtection private fun ReportFlow.mapToBrokenSiteModelReportFlow(): BrokenSiteModelReportFlow = when (this) { MENU -> BrokenSiteModelReportFlow.MENU DASHBOARD -> BrokenSiteModelReportFlow.DASHBOARD + PROMPT -> BrokenSiteModelReportFlow.PROMPT } diff --git a/app/src/main/java/com/duckduckgo/app/brokensite/api/BrokenSiteSender.kt b/app/src/main/java/com/duckduckgo/app/brokensite/api/BrokenSiteSender.kt index 41a66f1a36a5..9e45cab8c265 100644 --- a/app/src/main/java/com/duckduckgo/app/brokensite/api/BrokenSiteSender.kt +++ b/app/src/main/java/com/duckduckgo/app/brokensite/api/BrokenSiteSender.kt @@ -31,6 +31,7 @@ import com.duckduckgo.brokensite.api.BrokenSiteSender import com.duckduckgo.brokensite.api.ReportFlow import com.duckduckgo.brokensite.api.ReportFlow.DASHBOARD import com.duckduckgo.brokensite.api.ReportFlow.MENU +import com.duckduckgo.brokensite.api.ReportFlow.PROMPT import com.duckduckgo.browser.api.WebViewVersionProvider import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.absoluteString @@ -207,4 +208,5 @@ class BrokenSiteSubmitter @Inject constructor( private fun ReportFlow.toStringValue(): String = when (this) { DASHBOARD -> "dashboard" MENU -> "menu" + PROMPT -> "prompt" } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index e5ca6d933faf..afe06e3770e4 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -168,6 +168,7 @@ import com.duckduckgo.app.browser.webshare.WebShareChooser import com.duckduckgo.app.browser.webview.WebContentDebugging import com.duckduckgo.app.browser.webview.WebViewBlobDownloadFeature import com.duckduckgo.app.browser.webview.safewebview.SafeWebViewFeature +import com.duckduckgo.app.cta.ui.BrokenSitePromptDialogCta import com.duckduckgo.app.cta.ui.Cta import com.duckduckgo.app.cta.ui.CtaViewModel import com.duckduckgo.app.cta.ui.DaxBubbleCta @@ -1711,6 +1712,7 @@ class BrowserTabFragment : is Command.HideSSLError -> hideSSLWarning() is Command.LaunchScreen -> launchScreen(it.screen, it.payload) is Command.HideOnboardingDaxDialog -> hideOnboardingDaxDialog(it.onboardingCta) + is Command.HideBrokenSitePromptCta -> hideBrokenSitePromptCta(it.brokenSitePromptDialogCta) is Command.ShowRemoveSearchSuggestionDialog -> showRemoveSearchSuggestionDialog(it.suggestion) is Command.AutocompleteItemRemoved -> autocompleteItemRemoved() is Command.OpenDuckPlayerSettings -> globalActivityStarter.start(binding.root.context, DuckPlayerSettingsNoParams) @@ -2667,6 +2669,10 @@ class BrowserTabFragment : onboardingCta.hideOnboardingCta(binding) } + private fun hideBrokenSitePromptCta(brokenSitePromptDialogCta: BrokenSitePromptDialogCta) { + brokenSitePromptDialogCta.hideOnboardingCta(binding) + } + private fun hideDaxBubbleCta() { newBrowserTab.browserBackground.setBackgroundResource(0) daxDialogIntroBubbleCta.root.gone() @@ -3944,6 +3950,7 @@ class BrowserTabFragment : is HomePanelCta -> showHomeCta(configuration) is DaxBubbleCta -> showDaxOnboardingBubbleCta(configuration) is OnboardingDaxDialogCta -> showOnboardingDialogCta(configuration) + is BrokenSitePromptDialogCta -> showBrokenSitePromptCta(configuration) } } @@ -3999,6 +4006,17 @@ class BrowserTabFragment : viewModel.onCtaShown() } + @SuppressLint("ClickableViewAccessibility") + private fun showBrokenSitePromptCta(configuration: BrokenSitePromptDialogCta) { + hideNewTab() + configuration.showBrokenSitePromptCta( + binding, + onReportBrokenSiteClicked = { viewModel.onUserClickCtaOkButton(configuration) }, + onDismissCtaClicked = { viewModel.onUserClickCtaSecondaryButton(configuration) }, + ) + viewModel.onCtaShown() + } + private fun removeNewTabLayoutClickListener() { newBrowserTab.newTabLayout.setOnClickListener(null) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 359d9fe9613d..699208ee7c45 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -23,6 +23,7 @@ import android.net.http.SslCertificate import android.os.Message import android.print.PrintAttributes import android.provider.MediaStore +import android.util.Log import android.util.Patterns import android.view.ContextMenu import android.view.MenuItem @@ -100,6 +101,7 @@ import com.duckduckgo.app.browser.commands.Command.ExtractUrlFromCloakedAmpLink import com.duckduckgo.app.browser.commands.Command.FindInPageCommand import com.duckduckgo.app.browser.commands.Command.GenerateWebViewPreviewImage import com.duckduckgo.app.browser.commands.Command.HandleNonHttpAppLink +import com.duckduckgo.app.browser.commands.Command.HideBrokenSitePromptCta import com.duckduckgo.app.browser.commands.Command.HideKeyboard import com.duckduckgo.app.browser.commands.Command.HideOnboardingDaxDialog import com.duckduckgo.app.browser.commands.Command.HideSSLError @@ -198,6 +200,7 @@ import com.duckduckgo.app.browser.viewstate.OmnibarViewState import com.duckduckgo.app.browser.viewstate.PrivacyShieldViewState import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState import com.duckduckgo.app.browser.webview.SslWarningLayout.Action +import com.duckduckgo.app.cta.ui.BrokenSitePromptDialogCta import com.duckduckgo.app.cta.ui.Cta import com.duckduckgo.app.cta.ui.CtaViewModel import com.duckduckgo.app.cta.ui.DaxBubbleCta @@ -252,6 +255,7 @@ import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor import com.duckduckgo.browser.api.UserBrowserProperties import com.duckduckgo.browser.api.brokensite.BrokenSiteData import com.duckduckgo.browser.api.brokensite.BrokenSiteData.ReportFlow.MENU +import com.duckduckgo.browser.api.brokensite.BrokenSiteData.ReportFlow.PROMPT import com.duckduckgo.common.utils.AppUrl import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.SingleLiveEvent @@ -797,6 +801,7 @@ class BrowserTabViewModel @Inject constructor( } fun onViewVisible() { + Log.d("Cris", "onViewVisible") setAdClickActiveTabData(url) // we expect refreshCta to be called when a site is fully loaded if browsingShowing -trackers data available-. @@ -2771,6 +2776,8 @@ class BrowserTabViewModel @Inject constructor( } suspend fun refreshCta(): Cta? { + Log.d("Cris", "refreshCta") + if (currentGlobalLayoutState() is Browser) { val isBrowserShowing = currentBrowserViewState().browserShowing if (hasCtaBeenShownForCurrentPage.get() && isBrowserShowing) return null @@ -2815,6 +2822,7 @@ class BrowserTabViewModel @Inject constructor( is HomePanelCta.AddWidgetAuto, is HomePanelCta.AddWidgetInstructions -> LaunchAddWidget is OnboardingDaxDialogCta -> onOnboardingCtaOkButtonClicked(cta) is DaxBubbleCta -> onDaxBubbleCtaOkButtonClicked(cta) + is BrokenSitePromptDialogCta -> onBrokenSiteCtaOkButtonClicked(cta) else -> null } onboardingCommand?.let { @@ -2828,6 +2836,8 @@ class BrowserTabViewModel @Inject constructor( if (cta is DaxBubbleCta.DaxPrivacyProCta) { val updatedCta = refreshCta() ctaViewState.value = currentCtaViewState().copy(cta = updatedCta) + } else if (cta is BrokenSitePromptDialogCta) { + onBrokenSiteCtaDismissButtonClicked(cta) } } } @@ -3568,6 +3578,22 @@ class BrowserTabViewModel @Inject constructor( } } + private fun onBrokenSiteCtaDismissButtonClicked(cta: BrokenSitePromptDialogCta): Command? { + onUserDismissedCta(cta) + viewModelScope.launch { + command.value = HideBrokenSitePromptCta(cta) + } + return null + } + + private fun onBrokenSiteCtaOkButtonClicked(cta: BrokenSitePromptDialogCta): Command? { + viewModelScope.launch { + command.value = BrokenSiteFeedback(BrokenSiteData.fromSite(site, reportFlow = PROMPT)) + command.value = HideBrokenSitePromptCta(cta) + } + return null + } + private fun onOnboardingCtaOkButtonClicked(onboardingCta: OnboardingDaxDialogCta): Command? { onUserDismissedCta(onboardingCta) return when (onboardingCta) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt index 451e6a4f7ef8..0a950fd6d1ec 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt @@ -35,6 +35,7 @@ import com.duckduckgo.app.browser.history.NavigationHistoryEntry import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState +import com.duckduckgo.app.cta.ui.BrokenSitePromptDialogCta import com.duckduckgo.app.cta.ui.OnboardingDaxDialogCta import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.autofill.api.domain.app.LoginCredentials @@ -236,6 +237,7 @@ sealed class Command { val payload: String, ) : Command() data class HideOnboardingDaxDialog(val onboardingCta: OnboardingDaxDialogCta) : Command() + data class HideBrokenSitePromptCta(val brokenSitePromptDialogCta: BrokenSitePromptDialogCta) : Command() data class ShowRemoveSearchSuggestionDialog(val suggestion: AutoCompleteSuggestion) : Command() data object AutocompleteItemRemoved : Command() object OpenDuckPlayerSettings : Command() diff --git a/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt b/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt index 44d1908ca35f..9025ce66e540 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt @@ -35,6 +35,7 @@ enum class CtaId { DAX_END, DAX_FIRE_BUTTON_PULSE, DEVICE_SHIELD_CTA, + BROKEN_SITE_PROMPT, UNKNOWN, } diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 0c2cad73aeb5..09ff3df2edc7 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.cta.ui +import android.animation.ObjectAnimator import android.content.Context import android.net.Uri import android.view.View @@ -25,6 +26,7 @@ import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat +import androidx.core.view.ViewCompat.animate import androidx.transition.AutoTransition import androidx.transition.TransitionManager import com.duckduckgo.app.browser.R @@ -686,6 +688,54 @@ sealed class HomePanelCta( ) } +class BrokenSitePromptDialogCta : Cta { + + override val ctaId: CtaId = CtaId.BROKEN_SITE_PROMPT + override val shownPixel: Pixel.PixelName? = AppPixelName.ONBOARDING_DAX_CTA_SHOWN + override val okPixel: Pixel.PixelName? = AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON + override val cancelPixel: Pixel.PixelName? = null + + override fun pixelCancelParameters(): Map = mapOf() + + override fun pixelOkParameters(): Map = mapOf() + + override fun pixelShownParameters(): Map = mapOf() + + fun hideOnboardingCta(binding: FragmentBrowserTabBinding) { + val view = binding.includeBrokenSitePromptDialog.root + val fadeOutAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1f, 0f).apply { + duration = 3000 + addUpdateListener { animator -> + val alpha = animator.animatedValue as Float + if (alpha <= 0f) { + view.visibility = View.GONE + removeAllListeners() + } + } + } + + fadeOutAnimator.start() + } + + fun showBrokenSitePromptCta( + binding: FragmentBrowserTabBinding, + onReportBrokenSiteClicked: () -> Unit, + onDismissCtaClicked: () -> Unit, + ) { + val daxDialog = binding.includeBrokenSitePromptDialog + daxDialog.root.apply { + visibility = View.VISIBLE + alpha = 0f + animate() + .alpha(1f) + .setDuration(300) + .start() + } + binding.includeBrokenSitePromptDialog.reportButton.setOnClickListener { onReportBrokenSiteClicked.invoke() } + binding.includeBrokenSitePromptDialog.dismissButton.setOnClickListener { onDismissCtaClicked.invoke() } + } +} + fun DaxCta.addCtaToHistory(newCta: String): String { val param = onboardingStore.onboardingDialogJourney?.split("-").orEmpty().toMutableList() val daysInstalled = minOf(appInstallStore.daysInstalled().toInt(), MAX_DAYS_ALLOWED) diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index b44611d0a386..9d5861bc9996 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -171,7 +171,7 @@ class CtaViewModel @Inject constructor( ): Cta? { return withContext(dispatcher) { if (isBrowserShowing) { - getDaxDialogCta(site) + getBrowserCta(site) } else { getHomeCta() } @@ -260,7 +260,7 @@ class CtaViewModel @Inject constructor( } @WorkerThread - private suspend fun getDaxDialogCta(site: Site?): Cta? { + private suspend fun getBrowserCta(site: Site?): Cta? { val nonNullSite = site ?: return null val host = nonNullSite.domain @@ -273,6 +273,11 @@ class CtaViewModel @Inject constructor( return null } + if (!daxOnboardingActive() || hideTips() || extendedOnboardingFeatureToggles.noBrowserCtas().isEnabled()) { + Timber.d("Cris, refresh dialog can be shown") + return BrokenSitePromptDialogCta() + } + if (!canShowDaxDialogCta()) return null // Trackers blocked diff --git a/app/src/main/res/layout/fragment_browser_tab.xml b/app/src/main/res/layout/fragment_browser_tab.xml index e892bd29fd90..6dfb514255e9 100644 --- a/app/src/main/res/layout/fragment_browser_tab.xml +++ b/app/src/main/res/layout/fragment_browser_tab.xml @@ -91,6 +91,11 @@ android:id="@+id/includeOnboardingDaxDialog" layout="@layout/include_onboarding_view_dax_dialog" android:visibility="gone" /> + + ?, ) -enum class ReportFlow { DASHBOARD, MENU } +enum class ReportFlow { DASHBOARD, MENU, PROMPT } diff --git a/broken-site/broken-site-impl/build.gradle b/broken-site/broken-site-impl/build.gradle index 44429ebc4884..e320273a0bc6 100644 --- a/broken-site/broken-site-impl/build.gradle +++ b/broken-site/broken-site-impl/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation project(path: ':broken-site-api') implementation project(path: ':browser-api') implementation project(path: ':di') + implementation project(path: ':common-ui') implementation project(path: ':common-utils') implementation project(path: ':statistics-api') implementation project(path: ':app-build-config-api') diff --git a/broken-site/broken-site-impl/src/main/res/drawable/top_banner.xml b/broken-site/broken-site-impl/src/main/res/drawable/top_banner.xml new file mode 100644 index 000000000000..ac2ea5d30f03 --- /dev/null +++ b/broken-site/broken-site-impl/src/main/res/drawable/top_banner.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/broken-site/broken-site-impl/src/main/res/layout/prompt_broken_site.xml b/broken-site/broken-site-impl/src/main/res/layout/prompt_broken_site.xml new file mode 100644 index 000000000000..6d825c46effd --- /dev/null +++ b/broken-site/broken-site-impl/src/main/res/layout/prompt_broken_site.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + diff --git a/broken-site/broken-site-impl/src/main/res/values/donottranslate.xml b/broken-site/broken-site-impl/src/main/res/values/donottranslate.xml new file mode 100644 index 000000000000..19a0fd0545d2 --- /dev/null +++ b/broken-site/broken-site-impl/src/main/res/values/donottranslate.xml @@ -0,0 +1,22 @@ + + + + Site not working? Let us know. + This helps us improve the browser. + Dismiss + Website Is Broken + \ No newline at end of file diff --git a/browser-api/src/main/java/com/duckduckgo/browser/api/brokensite/BrokenSiteNav.kt b/browser-api/src/main/java/com/duckduckgo/browser/api/brokensite/BrokenSiteNav.kt index 06ab1b0ea2ac..2c9dc477df7b 100644 --- a/browser-api/src/main/java/com/duckduckgo/browser/api/brokensite/BrokenSiteNav.kt +++ b/browser-api/src/main/java/com/duckduckgo/browser/api/brokensite/BrokenSiteNav.kt @@ -44,7 +44,7 @@ data class BrokenSiteData( val openerContext: BrokenSiteOpenerContext?, val jsPerformance: DoubleArray?, ) { - enum class ReportFlow { MENU, DASHBOARD } + enum class ReportFlow { MENU, DASHBOARD, PROMPT } companion object { fun fromSite(site: Site?, reportFlow: ReportFlow): BrokenSiteData { diff --git a/privacy-protections-popup/privacy-protections-popup-impl/src/main/java/com/duckduckgo/privacyprotectionspopup/impl/PrivacyProtectionsPopupManagerImpl.kt b/privacy-protections-popup/privacy-protections-popup-impl/src/main/java/com/duckduckgo/privacyprotectionspopup/impl/PrivacyProtectionsPopupManagerImpl.kt index 2528e6519957..04f18c9bd6e1 100644 --- a/privacy-protections-popup/privacy-protections-popup-impl/src/main/java/com/duckduckgo/privacyprotectionspopup/impl/PrivacyProtectionsPopupManagerImpl.kt +++ b/privacy-protections-popup/privacy-protections-popup-impl/src/main/java/com/duckduckgo/privacyprotectionspopup/impl/PrivacyProtectionsPopupManagerImpl.kt @@ -139,6 +139,7 @@ class PrivacyProtectionsPopupManagerImpl @Inject constructor( val popupConditionsMet = arePopupConditionsMet(state = oldState) + // TODO (cbarreiro) set to test to trigger the old popup var experimentVariant = oldState.popupData.experimentVariant if (experimentVariant == null && popupConditionsMet) {