Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Duck Player custom error #5698

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't pass this from native when CSS already has access to the RC JSON. See DuckPlayerContentScopeConfigPlugin

}
DUCK_PLAYER_FEATURE_NAME -> {
jsonObject.put("platform", JSONObject("""{ name: "android" }"""))
Expand All @@ -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)
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Extract to constants

"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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to what I said above, we shouldn't expose this on the public API, but rather rely on CSS to get this values internally from the RC JSON


/**
* 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
*
Expand All @@ -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,
Expand Down
15 changes: 13 additions & 2 deletions duckplayer/duckplayer-impl/lint-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt"
line="743"
line="860"
column="9"/>
</issue>

Expand All @@ -19,7 +19,18 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt"
line="744"
line="861"
column="9"/>
</issue>

<issue
id="DenyListedApi"
message="If you find yourself using this API in production, you&apos;re doing something wrong!!"
errorLine1=" duckPlayerFeature.customError().setRawStoredState(newState)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt"
line="867"
column="9"/>
</issue>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these pixels are fired from the app module, they should be moved to AppPixelName. Other modules should never access the impl directly

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"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class DuckPlayerScriptsJsMessaging @Inject constructor(
"setUserValues",
"reportPageException",
"reportInitException",
"reportYouTubeError",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<OpenDuckPlayerInNewTab> {
return duckPlayerFeatureRepository.observeOpenInNewTab().map {
(if (!duckPlayerFeature.openInNewTab().isEnabled()) Unavailable else if (it) On else Off)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -816,11 +816,55 @@ 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) {
duckPlayerFeature.self().setRawStoredState(State(enabled))
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()
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
Loading