Skip to content

Commit

Permalink
Breaking API change: only allow apps to set a User-Agent prefix to re…
Browse files Browse the repository at this point in the history
…move error-prone situations where an application wasn't including the Hotwire substring in their custom user agnet.
  • Loading branch information
jayohms committed Dec 12, 2024
1 parent 83528c7 commit b7794de
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 26 deletions.
33 changes: 19 additions & 14 deletions core/src/main/kotlin/dev/hotwire/core/config/HotwireConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,27 @@ class HotwireConfig internal constructor() {
}

/**
* Provides a standard substring to be included in your WebView's user agent
* to identify itself as a Hotwire Native app.
*
* Important: Ensure that you've registered your bridge components before
* calling this so the bridge component names are included in your user agent.
* Set a custom user agent application prefix for every WebView instance. The
* library will automatically append a substring to your prefix which includes:
* - "Hotwire Native Android; Turbo Native Android;"
* - "bridge-components: [your bridge components];"
* - The WebView's default Chromium user agent string
*/
fun userAgentSubstring(): String {
val components = registeredBridgeComponentFactories.joinToString(" ") { it.name }
return "Hotwire Native Android; Turbo Native Android; bridge-components: [$components];"
}
var applicationUserAgentPrefix: String? = null

/**
* Set a custom user agent for every WebView instance.
*
* Important: Include `Hotwire.userAgentSubstring()` as part of your
* custom user agent for compatibility with your server.
* Gets the full user agent that is used for every WebView instance. This includes:
* - Your (optional) custom `applicationUserAgentPrefix`
* - "Hotwire Native Android; Turbo Native Android;"
* - "bridge-components: [your bridge components];"
* - The WebView's default Chromium user agent string
*/
var userAgent: String = userAgentSubstring()
fun userAgent(context: Context): String {
val components = registeredBridgeComponentFactories.joinToString(" ") { it.name }

return applicationUserAgentPrefix?.let { "$it " }.orEmpty() +
"Hotwire Native Android; Turbo Native Android; " +
"bridge-components: [$components]; " +
Hotwire.webViewInfo(context).defaultUserAgent
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.webkit.WebView
import android.widget.FrameLayout
import android.widget.FrameLayout.LayoutParams.MATCH_PARENT
Expand Down Expand Up @@ -36,10 +35,10 @@ open class HotwireWebView @JvmOverloads constructor(
internal set

init {
id = View.generateViewId()
id = generateViewId()
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.userAgentString = "${Hotwire.config.userAgent} ${settings.userAgentString}"
settings.userAgentString = Hotwire.config.userAgent(context)
settings.setSupportMultipleWindows(true)
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
initDayNightTheming()
Expand Down
50 changes: 42 additions & 8 deletions core/src/test/kotlin/dev/hotwire/core/bridge/UserAgentTest.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,58 @@
package dev.hotwire.core.bridge

import android.content.Context
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import dev.hotwire.core.config.Hotwire
import dev.hotwire.core.turbo.BaseUnitTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.R])
class UserAgentTest : BaseUnitTest() {
private lateinit var context: Context

@Before
override fun setup() {
super.setup()
context = ApplicationProvider.getApplicationContext()
Hotwire.config.applicationUserAgentPrefix = null
}

class UserAgentTest {
@Test
fun userAgentSubstring() {
fun `user agent with no prefix`() {
Hotwire.config.registeredBridgeComponentFactories = TestData.componentFactories

val userAgentSubstring = Hotwire.config.userAgentSubstring()
assertEquals(userAgentSubstring, "Hotwire Native Android; Turbo Native Android; bridge-components: [one two];")
val userAgent = Hotwire.config.userAgent(context)
val expectedUserAgent =
"Hotwire Native Android; Turbo Native Android; " +
"bridge-components: [one two]; " +
TEST_USER_AGENT

assertEquals(expectedUserAgent, userAgent)
}

@Test
fun userAgent() {
fun `user agent with prefix`() {
Hotwire.config.applicationUserAgentPrefix = "My Application Prefix;"
Hotwire.config.registeredBridgeComponentFactories = TestData.componentFactories
Hotwire.config.userAgent = "Test; ${Hotwire.config.userAgentSubstring()}"

val userAgent = Hotwire.config.userAgent
assertEquals(userAgent, "Test; Hotwire Native Android; Turbo Native Android; bridge-components: [one two];")
val userAgent = Hotwire.config.userAgent(context)
val expectedUserAgent =
"My Application Prefix; " +
"Hotwire Native Android; Turbo Native Android; " +
"bridge-components: [one two]; " +
TEST_USER_AGENT

assertEquals(expectedUserAgent, userAgent)
}

companion object {
private const val TEST_USER_AGENT = "user"
}
}
2 changes: 1 addition & 1 deletion demo/src/main/kotlin/dev/hotwire/demo/DemoApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,6 @@ class DemoApplication : Application() {
Hotwire.config.debugLoggingEnabled = BuildConfig.DEBUG
Hotwire.config.webViewDebuggingEnabled = BuildConfig.DEBUG
Hotwire.config.jsonConverter = KotlinXJsonConverter()
Hotwire.config.userAgent = "Hotwire Demo; ${Hotwire.config.userAgentSubstring()}"
Hotwire.config.applicationUserAgentPrefix = "Hotwire Demo;"
}
}

0 comments on commit b7794de

Please sign in to comment.