diff --git a/app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt b/app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt index 56a6e9a6005b..9d1b6aaf32c4 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt @@ -32,6 +32,7 @@ import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_ALWAYS_SERP import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_NEVER_SERP import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerOrigin.AUTO @@ -41,6 +42,15 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.On import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences import com.duckduckgo.duckplayer.api.PrivatePlayerMode import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_YOUTUBE_ERROR_AGE_RESTRICTED_DAILY_UNIQUE +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_YOUTUBE_ERROR_AGE_RESTRICTED_IMPRESSION +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_YOUTUBE_ERROR_NO_EMBED_DAILY_UNIQUE +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_YOUTUBE_ERROR_NO_EMBED_IMPRESSION +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_YOUTUBE_ERROR_SIGN_IN_REQUIRED_DAILY_UNIQUE +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_YOUTUBE_ERROR_SIGN_IN_REQUIRED_IMPRESSION +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_YOUTUBE_ERROR_UNKNOWN_DAILY_UNIQUE +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_YOUTUBE_ERROR_UNKNOWN_IMPRESSION import com.duckduckgo.js.messaging.api.JsCallbackData import com.duckduckgo.js.messaging.api.SubscriptionEventData import javax.inject.Inject @@ -127,6 +137,9 @@ class DuckPlayerJSHelper @Inject constructor( jsonObject.put("platform", JSONObject("""{ name: "android" }""")) jsonObject.put("locale", java.util.Locale.getDefault().language) jsonObject.put("env", if (appBuildConfig.isDebug) "development" else "production") + + // Custom Error Settings + jsonObject.getJSONObject("settings").put("customError", getCustomErrorSettings()) } DUCK_PLAYER_FEATURE_NAME -> { jsonObject.put("platform", JSONObject("""{ name: "android" }""")) @@ -142,6 +155,17 @@ class DuckPlayerJSHelper @Inject constructor( ) } + private fun getCustomErrorSettings(): JSONObject { + val customErrorObject = JSONObject() + customErrorObject.put("state", if (duckPlayer.shouldShowCustomError()) "enabled" else "disabled") + + duckPlayer.customErrorSettings()?.let { settings -> + customErrorObject.put("signInRequiredSelector", settings.signInRequiredSelector.takeIf { it.isNotEmpty() } ?: "") + } + + return customErrorObject + } + private suspend fun setUserPreferences(data: JSONObject) { val overlayInteracted = data.getBoolean(OVERLAY_INTERACTED) val privatePlayerModeObject = data.getJSONObject(PRIVATE_PLAYER_MODE) @@ -247,6 +271,24 @@ class DuckPlayerJSHelper @Inject constructor( else -> null } } + "reportYouTubeError" -> { + val impressionPixelName: DuckPlayerPixelName = when (data?.getString("error")) { + "age-restricted" -> DUCK_PLAYER_YOUTUBE_ERROR_AGE_RESTRICTED_IMPRESSION + "no-embed" -> DUCK_PLAYER_YOUTUBE_ERROR_NO_EMBED_IMPRESSION + "sign-in-restricted" -> DUCK_PLAYER_YOUTUBE_ERROR_SIGN_IN_REQUIRED_IMPRESSION + else -> DUCK_PLAYER_YOUTUBE_ERROR_UNKNOWN_IMPRESSION + } + + val dailyPixelName: DuckPlayerPixelName = when (data?.getString("error")) { + "age-restricted" -> DUCK_PLAYER_YOUTUBE_ERROR_AGE_RESTRICTED_DAILY_UNIQUE + "no-embed" -> DUCK_PLAYER_YOUTUBE_ERROR_NO_EMBED_DAILY_UNIQUE + "sign-in-restricted" -> DUCK_PLAYER_YOUTUBE_ERROR_SIGN_IN_REQUIRED_DAILY_UNIQUE + else -> DUCK_PLAYER_YOUTUBE_ERROR_UNKNOWN_DAILY_UNIQUE + } + + pixel.fire(impressionPixelName) + pixel.fire(dailyPixelName, emptyMap(), emptyMap(), Daily()) + } else -> { return null } diff --git a/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayer.kt b/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayer.kt index b70287f7ba39..1a932c1fd982 100644 --- a/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayer.kt +++ b/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayer.kt @@ -184,6 +184,20 @@ interface DuckPlayer { */ fun shouldOpenDuckPlayerInNewTab(): OpenDuckPlayerInNewTab + /** + * Checks whether Duck Player should show a custom error view based on RC flag + * + * @return True if should show a custom error view, false otherwise. + */ + fun shouldShowCustomError(): Boolean + + /** + * Retrieves settings for the Custom Error feature from RC + * + * @return A CustomErrorSettings Object with the settings or null if not settings are available + */ + fun customErrorSettings(): CustomErrorSettings? + /** * Observes whether a duck Player will be opened in a new tab based on RC flag and user settings * @@ -209,6 +223,15 @@ interface DuckPlayer { val privatePlayerMode: PrivatePlayerMode, ) + /** + * Data class representing custom error settings for Duck Player. + * + * @property signInRequiredSelector A a CSS selector used in detecting a client-side sign-in error + */ + data class CustomErrorSettings( + val signInRequiredSelector: String, + ) + enum class DuckPlayerState { ENABLED, DISABLED, diff --git a/duckplayer/duckplayer-impl/lint-baseline.xml b/duckplayer/duckplayer-impl/lint-baseline.xml index c1f778a75e90..da5a6893877d 100644 --- a/duckplayer/duckplayer-impl/lint-baseline.xml +++ b/duckplayer/duckplayer-impl/lint-baseline.xml @@ -8,7 +8,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -19,7 +19,18 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + + + + diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeature.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeature.kt index 6b6329da6c07..af634f5a22e2 100644 --- a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeature.kt +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeature.kt @@ -49,4 +49,17 @@ interface DuckPlayerFeature { */ @Toggle.DefaultValue(false) fun openInNewTab(): Toggle + + /** + * @return `true` when the remote config has the "customError" feature flag enabled + * If the remote feature is not present defaults to `false` + */ + @Toggle.DefaultValue(false) + fun customError(): Toggle + + // /** + // * @return the value of "signInRequiredSelector" when present in the "customError" feature settings + // * If the remote feature is not present defaults to `""` + // */ + // fun customErrorSignInRequiredSelector(): String } diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerPixelName.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerPixelName.kt index 0a902cc3fbaf..a4970e3d1d09 100644 --- a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerPixelName.kt +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerPixelName.kt @@ -33,4 +33,12 @@ enum class DuckPlayerPixelName(override val pixelName: String) : Pixel.PixelName DUCK_PLAYER_SETTINGS_PRESSED("duckplayer_setting_pressed"), DUCK_PLAYER_NEWTAB_SETTING_ON("duckplayer_newtab_setting-on"), DUCK_PLAYER_NEWTAB_SETTING_OFF("duckplayer_newtab_setting-off"), + DUCK_PLAYER_YOUTUBE_ERROR_SIGN_IN_REQUIRED_IMPRESSION("duckplayer_youtube-signin-error_impression"), + DUCK_PLAYER_YOUTUBE_ERROR_AGE_RESTRICTED_IMPRESSION("duckplayer_youtube-age-restricted-error_impression"), + DUCK_PLAYER_YOUTUBE_ERROR_NO_EMBED_IMPRESSION("duckplayer_youtube-no-embed-error_impression"), + DUCK_PLAYER_YOUTUBE_ERROR_UNKNOWN_IMPRESSION("duckplayer_youtube-unknown-error_impression"), + DUCK_PLAYER_YOUTUBE_ERROR_SIGN_IN_REQUIRED_DAILY_UNIQUE("duckplayer_youtube-signin-error_daily-unique"), + DUCK_PLAYER_YOUTUBE_ERROR_AGE_RESTRICTED_DAILY_UNIQUE("duckplayer_youtube-age-restricted-error_daily-unique"), + DUCK_PLAYER_YOUTUBE_ERROR_NO_EMBED_DAILY_UNIQUE("duckplayer_youtube-no-embed-error_daily-unique"), + DUCK_PLAYER_YOUTUBE_ERROR_UNKNOWN_DAILY_UNIQUE("duckplayer_youtube-unknown-error_daily-unique"), } diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerScriptsJsMessaging.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerScriptsJsMessaging.kt index 39ab67ccfb48..601effde7955 100644 --- a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerScriptsJsMessaging.kt +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerScriptsJsMessaging.kt @@ -131,6 +131,7 @@ class DuckPlayerScriptsJsMessaging @Inject constructor( "setUserValues", "reportPageException", "reportInitException", + "reportYouTubeError", ) } } diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/RealDuckPlayer.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/RealDuckPlayer.kt index 607a918a7341..d2be5d6415ef 100644 --- a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/RealDuckPlayer.kt +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/RealDuckPlayer.kt @@ -65,6 +65,7 @@ import com.duckduckgo.duckplayer.impl.ui.DuckPlayerPrimeDialogFragment import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesMultibinding +import com.squareup.moshi.Moshi.Builder import dagger.SingleInstanceIn import java.io.InputStream import javax.inject.Inject @@ -116,6 +117,7 @@ class RealDuckPlayer @Inject constructor( private var duckPlayerOrigin: DuckPlayerOrigin? = null private var isFeatureEnabled = false private var duckPlayerDisabledHelpLink = "" + private val moshi = Builder().add(JSONObjectAdapter()).build() init { if (isMainProcess) { @@ -477,6 +479,24 @@ class RealDuckPlayer @Inject constructor( return if (duckPlayerFeatureRepository.shouldOpenInNewTab()) On else Off } + override fun shouldShowCustomError(): Boolean { + return duckPlayerFeature.customError().isEnabled() + } + + override fun customErrorSettings(): CustomErrorSettings? { + val settings = duckPlayerFeature.customError().getSettings() + return if (settings != null) { + try { + val adapter = moshi.adapter(CustomErrorSettings::class.java) + adapter.fromJson(settings) + } catch (e: Exception) { + null + } + } else { + null + } + } + override fun observeShouldOpenInNewTab(): Flow { return duckPlayerFeatureRepository.observeOpenInNewTab().map { (if (!duckPlayerFeature.openInNewTab().isEnabled()) Unavailable else if (it) On else Off) diff --git a/duckplayer/duckplayer-impl/src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt b/duckplayer/duckplayer-impl/src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt index 2040672ccb70..cf22202980c1 100644 --- a/duckplayer/duckplayer-impl/src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt +++ b/duckplayer/duckplayer-impl/src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt @@ -816,6 +816,44 @@ class RealDuckPlayerTest { verify(mockPixel).fire(DUCK_PLAYER_NEWTAB_SETTING_OFF) } + // region shouldShowCustomError + + @Test + fun whenDuckPlayerCustomErrorIsEnabled_shouldShowCustomErrorReturnsTrue() = runTest { + setCustomError(true, null) + + val result = testee.shouldShowCustomError() + + assertEquals(true, result) + } + + @Test + fun whenDuckPlayerCustomErrorIsDisabled_shouldShowCustomErrorReturnsFalse() = runTest { + setCustomError(false, null) + + val result = testee.shouldShowCustomError() + + assertEquals(false, result) + } + + @Test + fun whenDuckPlayerCustomErrorSettingsDoNotExist_signInRequiredSelectorReturnsNull() = runTest { + setCustomError(true, "") + + val settings = testee.customErrorSettings() + + assertEquals(settings?.signInRequiredSelector, null) + } + + @Test + fun whenDuckPlayerCustomErrorSettingsExist_customErrorSettingsReturnsString() = runTest { + setCustomError(true, "{\"signInRequiredSelector\":\"[href*=\\\"//support.google.com/youtube/answer/3037019\\\"]\"}") + + val settings = testee.customErrorSettings() + + assertEquals(settings?.signInRequiredSelector, "[href*=\"//support.google.com/youtube/answer/3037019\"]") + } + // endregion private fun setFeatureToggle(enabled: Boolean) { @@ -823,4 +861,10 @@ class RealDuckPlayerTest { duckPlayerFeature.enableDuckPlayer().setRawStoredState(State(enabled)) testee.onPrivacyConfigDownloaded() } + + private fun setCustomError(isEnabled: Boolean, customSettings: String?) { + val newState = State(enable = isEnabled, settings = customSettings) + duckPlayerFeature.customError().setRawStoredState(newState) + testee.onPrivacyConfigDownloaded() + } } diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/index.js b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/index.js index dc12da725da2..384eea0f78bb 100644 --- a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/index.js +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/index.js @@ -3033,7 +3033,7 @@ if (YOUTUBE_ERROR_IDS.includes(eventError) || eventError === null) { if (eventError && eventError !== error) { setFocusMode("paused"); - if (platformName === "macos" || platformName === "ios") { + if (platformName === "macos" || platformName === "ios" || platformName === "android") { messaging2.reportYouTubeError({ error: eventError }); } } else { @@ -3068,8 +3068,8 @@ */ iframeDidLoad(iframe) { this.iframe = iframe; - if (!this.options || !this.options.signInRequiredSelector) { - console.log("Missing Custom Error options"); + if (this.options?.state !== "enabled") { + console.log("Error detection disabled"); return null; } const documentBody = iframe.contentWindow?.document?.body; diff --git a/package-lock.json b/package-lock.json index e31cef00df78..c801a584c79c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@duckduckgo/autoconsent": "^12.12.0", "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#16.2.2", - "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#7.23.0", + "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#c082f919fb31af8ffa12eed1b6a9b5abb0f9f7a2", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#8.4.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1739774089" }, @@ -63,7 +63,8 @@ "license": "Apache-2.0" }, "node_modules/@duckduckgo/content-scope-scripts": { - "resolved": "git+ssh://git@github.com/duckduckgo/content-scope-scripts.git#f7c579bc18d91093114036d37ef41c48e4977fb8", + "resolved": "git+ssh://git@github.com/duckduckgo/content-scope-scripts.git#c082f919fb31af8ffa12eed1b6a9b5abb0f9f7a2", + "integrity": "sha512-+/yX4yxCEWs3s2ZUeEK7qcCRV/xfVcFDtF5zTouQUKrd9uUCJQpjtPj48KGcusu9BhcziRWKuAH4gFFf6BOc/g==", "license": "Apache-2.0", "workspaces": [ "injected", diff --git a/package.json b/package.json index a9075bc5ba96..58af3fad3521 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "dependencies": { "@duckduckgo/autoconsent": "^12.12.0", "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#16.2.2", - "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#7.23.0", + "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#c082f919fb31af8ffa12eed1b6a9b5abb0f9f7a2", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#8.4.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1739774089" }