Skip to content

Commit d6aab83

Browse files
committed
Update documentation
1 parent 27e1501 commit d6aab83

File tree

3 files changed

+256
-0
lines changed

3 files changed

+256
-0
lines changed

.rules/web-messaging.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
---
2+
title: "WebMessaging"
3+
description: "How we use WebMessaging for JavaScript communication"
4+
keywords: ["webmessaging", "webMessaging", "WebMessaging", "web messaging", "javascript", "js", "WebView", "webview", "js-messaging", "js messaging", "message handlers", "subscription events", "WebViewCompat", "messaging strategy", "messaging", "js messaging", "javascript messaging", "JavaScript messaging", "javaScript messaging", "new message handler", "new messaging interface", "WebMessagingDelegate", ]
5+
alwaysApply: false
6+
---
7+
8+
# Using WebMessaging for JavaScript Communication
9+
10+
There are 2 ways of implementing WebMessaging functionality:
11+
1. (Recommended) Using `WebMessagingDelegate` to implement `WebMessaging` via delegation pattern. This approach already takes care of managing WebView lifecycle, as well as supporting `WebViewCompatMessageHandler` and `GlobalJsMessageHandler` for a standardized way to handle messaging across different features .
12+
2. (Manual approach, only recommended if more flexibility than the one provided by the delegate is needed) Manually implementing `WebMessaging`
13+
14+
## Using `WebMessagingDelegate` to implement `WebMessaging` via delegation pattern
15+
16+
```kotlin
17+
class <YourFeature>WebMessaging @Inject constructor(
18+
webMessagingDelegate: WebMessagingDelegate,
19+
) : WebMessaging by webMessagingDelegate.createPlugin(
20+
object : WebMessagingStrategy {
21+
override val context: String
22+
get() = TODO("Return a string representing the context of this messaging implementation, e.g., \"YourFeature\"")
23+
24+
override val allowedDomains: Set<String>
25+
get() = TODO("Return the set of allowed domains for messaging. For example:" +
26+
"- if messaging should work on all domains, return setOf(\"*\")" +
27+
"- if messaging should work only on specific domains, return setOf(\"https://example.com\", \"https://another.com\")" +
28+
"- if messaging should work on all subdomains of a domain, return setOf(\"https://*.example.com\")")
29+
30+
override val objectName: String
31+
get() = TODO("Return the JavaScript object name that will be available in the WebView, e.g., \"YourFeatureMessaging\"")
32+
33+
override suspend fun canHandleMessaging(): Boolean {
34+
TODO("Implement logic to determine if messaging can be handled (i.e. checking feature flags or user settings)" +
35+
"or return true if always applicable")
36+
}
37+
38+
override fun getMessageHandlers(): List<WebViewCompatMessageHandler> {
39+
TODO("Return the list of message handlers that will process incoming JavaScript messages")
40+
}
41+
42+
override fun getGlobalMessageHandler(): List<GlobalJsMessageHandler> {
43+
TODO("Return the list of global message handlers that should always be processed" +
44+
"regardless of whether a specific feature handler matches the message. For example DebugFlagGlobalHandler")
45+
}
46+
},
47+
)
48+
```
49+
50+
## Manually implementing `WebMessaging`
51+
52+
Since the `WebMessagingDelegate` already solves most of the issues and dangers of working with JavaScript messaging, manual implementation isn't recommended. If absolutely necessary, having a look at `RealWebMessagingDelegate` is recommended, in order to replicate some best practices:
53+
* Always check WebView lifecycle before registering/unregistering handlers
54+
* Ensure thread safety when working with WebView operations
55+
* Use `WebViewCompatWrapper` instead of calling `WebViewCompat` directly, as it includes several checks on the `WebView` lifecycle and ensures proper threading is used
56+
57+
# Adding WebMessaging to the browser (DuckDuckGoWebView/BrowserTabFragment)
58+
59+
If you need your messaging functionality to be available on the main browser WebView, you need to create a browser plugin that wraps your `WebMessaging` implementation.
60+
61+
## Step 1: Create the core implementation
62+
63+
Follow the patterns described in the [delegation pattern section](#using-webmessagingdelegate-to-implement-webmessaging-via-delegation-pattern) above.
64+
65+
## Step 2: Create the browser plugin wrapper
66+
67+
```kotlin
68+
@ContributesMultibinding(FragmentScope::class)
69+
class <YourFeature>WebMessagingBrowserPlugin @Inject constructor(
70+
private val <yourFeature>WebMessaging: <YourFeature>WebMessaging,
71+
) : WebMessagingBrowserPlugin {
72+
override fun webMessaging(): WebMessaging =
73+
<yourFeature>WebMessaging
74+
}
75+
```
76+
77+
## How it works
78+
79+
The `WebMessagingDelegate` handles lifecycle management, WebView safety checks, and proper JavaScript interface management.
80+
81+
## Message Handler Implementation
82+
83+
When implementing message handlers, you need to implement the appropriate plugin interfaces and follow these patterns:
84+
85+
### WebViewCompatMessageHandler
86+
```kotlin
87+
import com.duckduckgo.contentscopescripts.api.WebViewCompatContentScopeJsMessageHandlersPlugin
88+
import com.duckduckgo.di.scopes.ActivityScope
89+
import com.duckduckgo.js.messaging.api.JsMessage
90+
import com.duckduckgo.js.messaging.api.ProcessResult
91+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler
92+
import com.squareup.anvil.annotations.ContributesMultibinding
93+
import javax.inject.Inject
94+
95+
@ContributesMultibinding(ActivityScope::class)
96+
class YourFeatureMessageHandler @Inject constructor() : WebViewCompatContentScopeJsMessageHandlersPlugin {
97+
98+
override fun getJsMessageHandler(): WebViewCompatMessageHandler = object : WebViewCompatMessageHandler {
99+
100+
override fun process(jsMessage: JsMessage): ProcessResult? {
101+
102+
TODO("Process the message and return appropriate result" +
103+
" - Return SendToConsumer to pass message to consumer callback (normally UI layer)" +
104+
" - Return SendResponse(response) to send direct response without going through the UI layer" +
105+
" - Return null if no further action required. For example, if you need to store something"+
106+
"from the handler and don't need to send a response or notify the UI layer"
107+
)
108+
}
109+
110+
override val featureName: String = TODO("Return feature name that should match this handler")
111+
override val methods: List<String> = TODO("Return list of methods that should match this handler")
112+
}
113+
}
114+
```
115+
116+
### GlobalJsMessageHandler
117+
```kotlin
118+
import com.duckduckgo.contentscopescripts.impl.messaging.GlobalContentScopeJsMessageHandlersPlugin
119+
import com.duckduckgo.di.scopes.AppScope
120+
import com.duckduckgo.js.messaging.api.GlobalJsMessageHandler
121+
import com.duckduckgo.js.messaging.api.JsMessage
122+
import com.duckduckgo.js.messaging.api.ProcessResult
123+
import com.squareup.anvil.annotations.ContributesMultibinding
124+
import javax.inject.Inject
125+
126+
@ContributesMultibinding(AppScope::class)
127+
class YourFeatureGlobalHandler @Inject constructor() : GlobalContentScopeJsMessageHandlersPlugin {
128+
129+
override fun getGlobalJsMessageHandler(): GlobalJsMessageHandler = object : GlobalJsMessageHandler {
130+
131+
override fun process(jsMessage: JsMessage): ProcessResult? {
132+
TODO("Process the message and return appropriate result" +
133+
" - Return SendToConsumer to pass message to consumer callback (normally UI layer)" +
134+
" - Return SendResponse(response) to send direct response without going through the UI layer" +
135+
" - Return null if no further action required. For example, if you need to store something" +
136+
"from the handler and don't need to send a response or notify the UI layer"
137+
)
138+
}
139+
140+
override val method: String = TODO("Return the name of the method that should match this handler")
141+
}
142+
}
143+
```
144+
145+
## Posting Messages to JavaScript
146+
147+
There are 2 ways of sending messages to JavaScript
148+
1. If you don't require backwards compatibility with the old way of handling messages, you can use `WebMessaging` directly
149+
2. Otherwise, you can use `PostMessageWrapperPlugin`
150+
151+
152+
**Important**: To send messages using the new `WebMessaging` interface using the [delegation pattern](#using-webmessagingdelegate-to-implement-webmessaging-via-delegation-pattern), you must first receive a message from JavaScript. This is because the delegate needs to establish a `replyProxy` to ensure proper context communication. The system automatically sets up the reply proxy when it receives an `initialPing` message from JavaScript, which allows subsequent `postMessage` calls to work correctly. If you're not using the [delegation pattern](#using-webmessagingdelegate-to-implement-webmessaging-via-delegation-pattern), establishing a `replyProxy` for message posting is still recommended to ensure messages are only received by the appropriate script.
153+
154+
155+
### Using `WebMessaging` directly
156+
157+
To send messages from native code to JavaScript using the new WebMessaging interface:
158+
159+
```kotlin
160+
// Create subscription event data
161+
val subscriptionEventData = SubscriptionEventData(
162+
featureName = "yourFeature",
163+
subscriptionName = "yourEventType",
164+
params = JSONObject().put("key", "value")
165+
)
166+
167+
// Post message to WebView
168+
webMessaging.postMessage(webView, subscriptionEventData)
169+
```
170+
171+
### Using `PostMessageWrapperPlugin`
172+
173+
Use `PostMessageWrapperPlugin` when:
174+
- You need to support both new and legacy messaging interfaces
175+
- You have feature flags that determine which messaging system to use
176+
- You want to gradually migrate from legacy to new messaging
177+
178+
The plugin handles the complexity of choosing the right messaging interface, allowing consumers to simply call `postMessage()` without worrying about the underlying implementation details.
179+
180+
When you need to post messages using either the new WebMessaging interface or the legacy JsMessageHelper depending on feature flags or other conditions, you can create an implementation of `PostMessageWrapperPlugin`:
181+
182+
```kotlin
183+
@ContributesMultibinding(FragmentScope::class)
184+
class YourFeaturePostMessageWrapperPlugin @Inject constructor(
185+
@Named("yourFeature") private val webMessaging: WebMessaging,
186+
private val jsMessageHelper: JsMessageHelper,
187+
private val yourFeatureFlags: YourFeatureFlags,
188+
) : PostMessageWrapperPlugin {
189+
190+
override suspend fun postMessage(message: SubscriptionEventData, webView: WebView) {
191+
if (yourFeatureFlags.isNewMessagingEnabled()) {
192+
// Use new WebMessaging interface
193+
webMessaging.postMessage(webView, message)
194+
} else {
195+
// Use legacy JsMessageHelper
196+
jsMessageHelper.sendSubscriptionEvent(
197+
subscriptionEvent = SubscriptionEvent(
198+
context = webMessaging.context,
199+
featureName = message.featureName,
200+
subscriptionName = message.subscriptionName,
201+
params = message.params,
202+
),
203+
callbackName = "yourCallbackName",
204+
secret = "yourSecret",
205+
webView = webView,
206+
)
207+
}
208+
}
209+
210+
override val context: String
211+
get() = webMessaging.context
212+
}
213+
```
214+
215+
In order to make sure you're only sending the message to the appropriate consumer, inject your `WebMessaging` implementation, not the entire list of available implementations.
216+
217+
218+
## Important Notes
219+
220+
- Use appropriate scoping and consider using `@SingleInstanceIn(<Scope>)` with appropriate scoping to make sure only one instance of `WebMessaging` exists per `WebView`
221+
- The `context` string should be unique and descriptive
222+

browser-api/src/main/java/com/duckduckgo/browser/api/WebMessagingBrowserPlugin.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,17 @@
1616

1717
package com.duckduckgo.browser.api
1818

19+
import com.duckduckgo.common.utils.plugins.PluginPoint
1920
import com.duckduckgo.js.messaging.api.WebMessaging
2021

22+
/**
23+
* Interface to provide implementations of [WebMessaging] to the browser, through
24+
* [PluginPoint]<[WebMessaging]>
25+
*/
2126
interface WebMessagingBrowserPlugin {
27+
/**
28+
* Provides an implementation of [WebMessaging] to be used by the browser.
29+
* @return an instance of [WebMessaging]
30+
*/
2231
fun webMessaging(): WebMessaging
2332
}

js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/WebMessaging.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,43 @@ package com.duckduckgo.js.messaging.api
1919
import android.webkit.WebView
2020

2121
interface WebMessaging {
22+
/**
23+
* Registers the given [jsMessageCallback] to handle messages from the provided [webView].
24+
* @param jsMessageCallback the callback to handle incoming messages
25+
* @param webView the WebView to register the callback with
26+
* Notes:
27+
* - It's not recommended to unregister and then register again on the same WebView instance.
28+
*/
2229
suspend fun register(
2330
jsMessageCallback: WebViewCompatMessageCallback,
2431
webView: WebView,
2532
)
2633

34+
/**
35+
* Unregisters any previously registered message handlers from the given [webView].
36+
* Notes:
37+
* - This does not remove the JavaScript interface from the WebView, just the handlers.
38+
* - It's not required to call this when the WebView is being destroyed.
39+
* - It's not recommended to unregister and then register again on the same WebView instance.
40+
* @param webView the WebView to unregister the handlers from
41+
*/
2742
suspend fun unregister(webView: WebView)
2843

44+
/**
45+
* Posts a message to the given [webView] using the provided [subscriptionEventData].
46+
* @param webView the WebView to which the message should be posted
47+
* @param subscriptionEventData the data to be sent in the message
48+
*/
2949
suspend fun postMessage(
3050
webView: WebView,
3151
subscriptionEventData: SubscriptionEventData,
3252
)
3353

54+
/**
55+
* The context for this instance.
56+
* This can be used to differentiate between different messaging implementations.
57+
* @return context string
58+
*/
3459
val context: String
3560
}
3661

0 commit comments

Comments
 (0)