Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 36df4c5

Browse files
Adds support for JavaScript channels to the new iOS implementation of webview_flutter_wkwebview. (#4782)
1 parent f45bf32 commit 36df4c5

File tree

4 files changed

+525
-34
lines changed

4 files changed

+525
-34
lines changed

packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
/// Times at which to inject script content into a webpage.
6+
///
7+
/// Wraps [WKUserScriptInjectionTime](https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime?language=objc).
8+
enum WKUserScriptInjectionTime {
9+
/// Inject the script after the creation of the webpage’s document element, but before loading any other content.
10+
///
11+
/// See https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentstart?language=objc.
12+
atDocumentStart,
13+
14+
/// Inject the script after the document finishes loading, but before loading any other subresources.
15+
///
16+
/// See https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentend?language=objc.
17+
atDocumentEnd,
18+
}
19+
520
/// The media types that require a user gesture to begin playing.
621
///
722
/// Wraps [WKAudiovisualMediaTypes](https://developer.apple.com/documentation/webkit/wkaudiovisualmediatypes?language=objc).
@@ -27,10 +42,149 @@ enum WKAudiovisualMediaType {
2742
all,
2843
}
2944

45+
/// A script that the web view injects into a webpage.
46+
///
47+
/// Wraps [WKUserScript](https://developer.apple.com/documentation/webkit/wkuserscript?language=objc).
48+
class WKUserScript {
49+
/// Constructs a [UserScript].
50+
WKUserScript(
51+
this.source,
52+
this.injectionTime, {
53+
required this.isMainFrameOnly,
54+
});
55+
56+
/// The script’s source code.
57+
final String source;
58+
59+
/// The time at which to inject the script into the webpage.
60+
final WKUserScriptInjectionTime injectionTime;
61+
62+
/// Indicates whether to inject the script into the main frame or all frames.
63+
final bool isMainFrameOnly;
64+
}
65+
66+
/// An object that encapsulates a message sent by JavaScript code from a webpage.
67+
///
68+
/// Wraps [WKScriptMessage](https://developer.apple.com/documentation/webkit/wkscriptmessage?language=objc).
69+
class WKScriptMessage {
70+
/// Constructs a [WKScriptMessage].
71+
WKScriptMessage({required this.name, this.body});
72+
73+
/// The name of the message handler to which the message is sent.
74+
final String name;
75+
76+
/// The body of the message.
77+
///
78+
/// Allowed types are [num], [String], [List], [Map], and `null`.
79+
final Object? body;
80+
}
81+
82+
/// An interface for receiving messages from JavaScript code running in a webpage.
83+
///
84+
/// Wraps [WKScriptMessageHandler](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc)
85+
class WKScriptMessageHandler {
86+
/// Tells the handler that a webpage sent a script message.
87+
///
88+
/// Use this method to respond to a message sent from the webpage’s
89+
/// JavaScript code. Use the [message] parameter to get the message contents and
90+
/// to determine the originating web view.
91+
Future<void> setDidReceiveScriptMessage(
92+
void Function(
93+
WKUserContentController userContentController,
94+
WKScriptMessage message,
95+
)
96+
didReceiveScriptMessage,
97+
) {
98+
throw UnimplementedError();
99+
}
100+
}
101+
102+
/// Manages interactions between JavaScript code and your web view.
103+
///
104+
/// Use this object to do the following:
105+
///
106+
/// * Inject JavaScript code into webpages running in your web view.
107+
/// * Install custom JavaScript functions that call through to your app’s native
108+
/// code.
109+
///
110+
/// Wraps [WKUserContentController](https://developer.apple.com/documentation/webkit/wkusercontentcontroller?language=objc).
111+
class WKUserContentController {
112+
/// Constructs a [WKUserContentController].
113+
WKUserContentController();
114+
115+
// A WKUserContentController that is owned by configuration.
116+
WKUserContentController._fromWebViewConfiguretion(
117+
// TODO(bparrishMines): Remove ignore once constructor is implemented.
118+
// ignore: avoid_unused_constructor_parameters
119+
WKWebViewConfiguration configuration,
120+
);
121+
122+
/// Installs a message handler that you can call from your JavaScript code.
123+
///
124+
/// This name of the parameter must be unique within the user content
125+
/// controller and must not be an empty string. The user content controller
126+
/// uses this parameter to define a JavaScript function for your message
127+
/// handler in the page’s main content world. The name of this function is
128+
/// `window.webkit.messageHandlers.<name>.postMessage(<messageBody>)`, where
129+
/// `<name>` corresponds to the value of this parameter. For example, if you
130+
/// specify the string `MyFunction`, the user content controller defines the `
131+
/// `window.webkit.messageHandlers.MyFunction.postMessage()` function in
132+
/// JavaScript.
133+
Future<void> addScriptMessageHandler(
134+
WKScriptMessageHandler handler,
135+
String name,
136+
) {
137+
assert(name.isNotEmpty);
138+
throw UnimplementedError();
139+
}
140+
141+
/// Uninstalls the custom message handler with the specified name from your JavaScript code.
142+
///
143+
/// If no message handler with this name exists in the user content
144+
/// controller, this method does nothing.
145+
///
146+
/// Use this method to remove a message handler that you previously installed
147+
/// using the [addScriptMessageHandler] method. This method removes the
148+
/// message handler from the page content world. If you installed the message
149+
/// handler in a different content world, this method doesn’t remove it.
150+
Future<void> removeScriptMessageHandler(String name) {
151+
throw UnimplementedError();
152+
}
153+
154+
/// Uninstalls all custom message handlers associated with the user content controller.
155+
Future<void> removeAllScriptMessageHandlers() {
156+
throw UnimplementedError();
157+
}
158+
159+
/// Injects the specified script into the webpage’s content.
160+
Future<void> addUserScript(WKUserScript userScript) {
161+
throw UnimplementedError();
162+
}
163+
164+
/// Removes all user scripts from the web view.
165+
Future<void> removeAllUserScripts() {
166+
throw UnimplementedError();
167+
}
168+
}
169+
30170
/// A collection of properties that you use to initialize a web view.
31171
///
32172
/// Wraps [WKWebViewConfiguration](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration?language=objc)
33173
class WKWebViewConfiguration {
174+
/// Constructs a [WKWebViewConfiguration].
175+
WKWebViewConfiguration({required this.userContentController});
176+
177+
// A WKWebViewConfiguration that is owned by webView.
178+
// TODO(bparrishMines): Remove ignore once constructor is implemented.
179+
// ignore: avoid_unused_constructor_parameters
180+
WKWebViewConfiguration._fromWebView(WKWebView webView) {
181+
userContentController =
182+
WKUserContentController._fromWebViewConfiguretion(this);
183+
}
184+
185+
/// Coordinates interactions between your app’s code and the webpage’s scripts and other content.
186+
late final WKUserContentController userContentController;
187+
34188
/// Indicates whether HTML5 videos play inline or use the native full-screen controller.
35189
set allowsInlineMediaPlayback(bool allow) {
36190
throw UnimplementedError();
@@ -52,7 +206,7 @@ class WKWebViewConfiguration {
52206
///
53207
/// Wraps [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview?language=objc).
54208
class WKWebView {
55-
/// Construct a [WKWebView].
209+
/// Constructs a [WKWebView].
56210
///
57211
/// [configuration] contains the configuration details for the web view. This
58212
/// method saves a copy of your configuration object. Changes you make to your
@@ -66,4 +220,16 @@ class WKWebView {
66220
WKWebView([WKWebViewConfiguration? configuration]) {
67221
throw UnimplementedError();
68222
}
223+
224+
/// Contains the configuration details for the web view.
225+
///
226+
/// Use the object in this property to obtain information about your web
227+
/// view’s configuration. Because this property returns a copy of the
228+
/// configuration object, changes you make to that object don’t affect the web
229+
/// view’s configuration.
230+
///
231+
/// If you didn’t create your web view with a [WKWebViewConfiguration] this
232+
/// property contains a default configuration object.
233+
late final WKWebViewConfiguration configuration =
234+
WKWebViewConfiguration._fromWebView(this);
69235
}

packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class WebKitWebViewWidget extends StatefulWidget {
1919
required this.javascriptChannelRegistry,
2020
required this.onBuildWidget,
2121
this.configuration,
22-
@visibleForTesting this.webViewProxy = const WebViewProxy(),
22+
@visibleForTesting this.webViewProxy = const WebViewWidgetProxy(),
2323
});
2424

2525
/// The initial parameters used to setup the WebView.
@@ -39,7 +39,7 @@ class WebKitWebViewWidget extends StatefulWidget {
3939
/// The handler for constructing [WKWebView]s and calling static methods.
4040
///
4141
/// This should only be changed for testing purposes.
42-
final WebViewProxy webViewProxy;
42+
final WebViewWidgetProxy webViewProxy;
4343

4444
/// A callback to build a widget once [WKWebView] has been initialized.
4545
final Widget Function(WebKitWebViewPlatformController controller)
@@ -78,15 +78,19 @@ class WebKitWebViewPlatformController extends WebViewPlatformController {
7878
required this.callbacksHandler,
7979
required this.javascriptChannelRegistry,
8080
WKWebViewConfiguration? configuration,
81-
@visibleForTesting this.webViewProxy = const WebViewProxy(),
81+
@visibleForTesting this.webViewProxy = const WebViewWidgetProxy(),
8282
}) : super(callbacksHandler) {
8383
_setCreationParams(
8484
creationParams,
85-
configuration: configuration ?? WKWebViewConfiguration(),
86-
).then((_) => _initializationCompleter.complete());
85+
configuration: configuration ??
86+
WKWebViewConfiguration(
87+
userContentController: WKUserContentController(),
88+
),
89+
);
8790
}
8891

89-
final Completer<void> _initializationCompleter = Completer<void>();
92+
final Map<String, WKScriptMessageHandler> _scriptMessageHandlers =
93+
<String, WKScriptMessageHandler>{};
9094

9195
/// Handles callbacks that are made by navigation.
9296
final WebViewPlatformCallbacksHandler callbacksHandler;
@@ -97,7 +101,7 @@ class WebKitWebViewPlatformController extends WebViewPlatformController {
97101
/// Handles constructing a [WKWebView].
98102
///
99103
/// This should only be changed when used for testing.
100-
final WebViewProxy webViewProxy;
104+
final WebViewWidgetProxy webViewProxy;
101105

102106
/// Represents the WebView maintained by platform code.
103107
late final WKWebView webView;
@@ -113,6 +117,8 @@ class WebKitWebViewPlatformController extends WebViewPlatformController {
113117
);
114118

115119
webView = webViewProxy.createWebView(configuration);
120+
121+
await addJavascriptChannels(params.javascriptChannelNames);
116122
}
117123

118124
void _setWebViewConfiguration(
@@ -140,18 +146,89 @@ class WebKitWebViewPlatformController extends WebViewPlatformController {
140146
if (!requiresUserAction) WKAudiovisualMediaType.none,
141147
};
142148
}
149+
150+
@override
151+
Future<void> addJavascriptChannels(Set<String> javascriptChannelNames) async {
152+
await Future.wait<void>(
153+
javascriptChannelNames.where(
154+
(String channelName) {
155+
return !_scriptMessageHandlers.containsKey(channelName);
156+
},
157+
).map<Future<void>>(
158+
(String channelName) {
159+
final WKScriptMessageHandler handler =
160+
webViewProxy.createScriptMessageHandler()
161+
..setDidReceiveScriptMessage(
162+
(
163+
WKUserContentController userContentController,
164+
WKScriptMessage message,
165+
) {
166+
javascriptChannelRegistry.onJavascriptChannelMessage(
167+
message.name,
168+
message.body!.toString(),
169+
);
170+
},
171+
);
172+
_scriptMessageHandlers[channelName] = handler;
173+
174+
final String wrapperSource =
175+
'window.$channelName = webkit.messageHandlers.$channelName;';
176+
final WKUserScript wrapperScript = WKUserScript(
177+
wrapperSource,
178+
WKUserScriptInjectionTime.atDocumentStart,
179+
isMainFrameOnly: false,
180+
);
181+
webView.configuration.userContentController
182+
.addUserScript(wrapperScript);
183+
return webView.configuration.userContentController
184+
.addScriptMessageHandler(
185+
handler,
186+
channelName,
187+
);
188+
},
189+
),
190+
);
191+
}
192+
193+
@override
194+
Future<void> removeJavascriptChannels(
195+
Set<String> javascriptChannelNames,
196+
) async {
197+
if (javascriptChannelNames.isEmpty) {
198+
return;
199+
}
200+
201+
// WKWebView does not support removing a single user script, so this removes
202+
// all user scripts and all message handlers and re-registers channels that
203+
// shouldn't be removed. Note that this workaround could interfere with
204+
// exposing support for custom scripts from applications.
205+
webView.configuration.userContentController.removeAllUserScripts();
206+
webView.configuration.userContentController
207+
.removeAllScriptMessageHandlers();
208+
209+
javascriptChannelNames.forEach(_scriptMessageHandlers.remove);
210+
final Set<String> remainingNames = _scriptMessageHandlers.keys.toSet();
211+
_scriptMessageHandlers.clear();
212+
213+
await addJavascriptChannels(remainingNames);
214+
}
143215
}
144216

145-
/// Handles constructing [WKWebView]s and calling static methods.
217+
/// Handles constructing objects and calling static methods.
146218
///
147219
/// This should only be used for testing purposes.
148220
@visibleForTesting
149-
class WebViewProxy {
150-
/// Creates a [WebViewProxy].
151-
const WebViewProxy();
221+
class WebViewWidgetProxy {
222+
/// Constructs a [WebViewWidgetProxy].
223+
const WebViewWidgetProxy();
152224

153225
/// Constructs a [WKWebView].
154226
WKWebView createWebView(WKWebViewConfiguration configuration) {
155227
return WKWebView(configuration);
156228
}
229+
230+
/// Constructs a [WKScriptMessageHandler].
231+
WKScriptMessageHandler createScriptMessageHandler() {
232+
return WKScriptMessageHandler();
233+
}
157234
}

0 commit comments

Comments
 (0)