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"
}