@javascriptcommon/react-native-webview 13.12.21 → 13.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/android/gradle.properties +1 -1
  2. package/android/src/main/AndroidManifest.xml +12 -0
  3. package/android/src/main/AndroidManifestNew.xml +13 -0
  4. package/android/src/main/assets/contentfilter/rules.txt +57751 -0
  5. package/android/src/main/java/com/reactnativecommunity/webview/ContentFilter.kt +85 -0
  6. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java +76 -25
  7. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewClient.java +168 -25
  8. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt +15 -7
  9. package/android/src/main/java/com/reactnativecommunity/webview/events/SubResourceErrorEvent.kt +25 -0
  10. package/android/src/main/jniLibs/arm64-v8a/libcontentfilter.so +0 -0
  11. package/android/src/main/jniLibs/armeabi-v7a/libcontentfilter.so +0 -0
  12. package/android/src/main/jniLibs/x86_64/libcontentfilter.so +0 -0
  13. package/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java +12 -1
  14. package/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java +7 -0
  15. package/apple/RNCWebView.h +1 -1
  16. package/apple/RNCWebView.mm +26 -17
  17. package/apple/RNCWebViewImpl.h +2 -1
  18. package/apple/RNCWebViewImpl.m +62 -25
  19. package/apple/RNCWebViewManager.mm +4 -0
  20. package/index.d.ts +2 -3
  21. package/lib/RNCWebViewNativeComponent.d.ts +24 -21
  22. package/lib/RNCWebViewNativeComponent.js +1 -1
  23. package/lib/WebView.android.js +1 -1
  24. package/lib/WebViewShared.d.ts +3 -1
  25. package/lib/WebViewShared.js +1 -1
  26. package/lib/WebViewTypes.d.ts +32 -8
  27. package/package.json +9 -1
  28. package/src/RNCWebViewNativeComponent.ts +27 -21
  29. package/src/WebView.android.tsx +4 -0
  30. package/src/WebViewShared.tsx +10 -0
  31. package/src/WebViewTypes.ts +36 -8
@@ -0,0 +1,85 @@
1
+ package com.reactnativecommunity.webview
2
+
3
+ import android.content.Context
4
+ import android.util.Log
5
+
6
+ /**
7
+ * Kotlin bridge to the native content-filtering engine (`libcontentfilter.so`).
8
+ *
9
+ * The engine is a single, process-wide, immutable rule set. It is initialised
10
+ * once, lazily, on a background thread from a bundled rule-list text asset.
11
+ * Everything fails OPEN: if the library cannot be loaded, the asset is missing,
12
+ * or the engine is not ready yet, [shouldBlock] returns false and requests load
13
+ * normally.
14
+ *
15
+ * Matching itself (`nativeCheck`) is serialized behind a Mutex on the native
16
+ * side and costs microseconds, so it is safe to call from WebView worker
17
+ * threads.
18
+ */
19
+ object ContentFilter {
20
+ private const val TAG = "ContentFilter"
21
+ private const val LIBRARY = "contentfilter"
22
+
23
+ // Bundled rule list (ABP/EasyList syntax), merged from the library assets.
24
+ private const val RULES_ASSET = "contentfilter/rules.txt"
25
+
26
+ @Volatile private var libraryLoaded = false
27
+ @Volatile private var initStarted = false
28
+ @Volatile private var ready = false
29
+
30
+ init {
31
+ try {
32
+ System.loadLibrary(LIBRARY)
33
+ libraryLoaded = true
34
+ } catch (t: Throwable) {
35
+ Log.e(TAG, "Could not load native library '$LIBRARY'; filtering disabled.", t)
36
+ }
37
+ }
38
+
39
+ @JvmStatic external fun nativeInit(rules: String): Boolean
40
+ @JvmStatic external fun nativeIsReady(): Boolean
41
+ @JvmStatic external fun nativeCheck(url: String, sourceUrl: String, requestType: String): Boolean
42
+
43
+ /**
44
+ * Kick off a one-time background load of the bundled rule list. Safe to call
45
+ * repeatedly and from any thread; only the first call does work.
46
+ */
47
+ fun ensureInitialized(context: Context) {
48
+ if (!libraryLoaded || initStarted) return
49
+ synchronized(this) {
50
+ if (initStarted) return
51
+ initStarted = true
52
+ }
53
+ val appContext = context.applicationContext
54
+ Thread({
55
+ try {
56
+ val rules = appContext.assets.open(RULES_ASSET)
57
+ .bufferedReader()
58
+ .use { it.readText() }
59
+ val ok = nativeInit(rules)
60
+ ready = ok && nativeIsReady()
61
+ Log.d(TAG, "content filter initialized (ready=$ready)")
62
+ } catch (t: Throwable) {
63
+ Log.e(TAG, "content filter initialization failed; filtering disabled.", t)
64
+ }
65
+ }, "content-filter-init").start()
66
+ }
67
+
68
+ fun isReady(): Boolean = libraryLoaded && ready
69
+
70
+ /**
71
+ * @return true when the request should be BLOCKED. Fails open.
72
+ * @param url full request URL
73
+ * @param sourceUrl full top-frame (document) URL, or null/empty if unknown
74
+ * @param requestType request-type string ("script", "image", "sub_frame",
75
+ * "xmlhttprequest", "font", "media", ...)
76
+ */
77
+ fun shouldBlock(url: String, sourceUrl: String?, requestType: String): Boolean {
78
+ if (!isReady()) return false
79
+ return try {
80
+ nativeCheck(url, sourceUrl ?: "", requestType)
81
+ } catch (t: Throwable) {
82
+ false
83
+ }
84
+ }
85
+ }
@@ -2,6 +2,7 @@ package com.reactnativecommunity.webview;
2
2
 
3
3
  import android.annotation.SuppressLint;
4
4
  import android.graphics.Rect;
5
+ import android.net.Uri;
5
6
  import android.text.TextUtils;
6
7
  import android.view.ActionMode;
7
8
  import android.view.Menu;
@@ -14,7 +15,12 @@ import android.webkit.WebChromeClient;
14
15
  import android.webkit.WebView;
15
16
  import android.webkit.WebViewClient;
16
17
 
18
+ import androidx.annotation.NonNull;
17
19
  import androidx.annotation.Nullable;
20
+ import androidx.webkit.JavaScriptReplyProxy;
21
+ import androidx.webkit.WebMessageCompat;
22
+ import androidx.webkit.WebViewCompat;
23
+ import androidx.webkit.WebViewFeature;
18
24
 
19
25
  import com.facebook.common.logging.FLog;
20
26
  import com.facebook.react.bridge.Arguments;
@@ -38,6 +44,7 @@ import org.json.JSONObject;
38
44
 
39
45
  import java.util.List;
40
46
  import java.util.Map;
47
+ import java.util.Set;
41
48
 
42
49
  public class RNCWebView extends WebView implements LifecycleEventListener {
43
50
  protected @Nullable
@@ -46,7 +53,9 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
46
53
  String injectedJSBeforeContentLoaded;
47
54
  protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView";
48
55
  protected @Nullable
49
- RNCWebViewBridge bridge;
56
+ RNCWebViewBridge fallbackBridge;
57
+ protected @Nullable
58
+ WebViewCompat.WebMessageListener bridgeListener = null;
50
59
 
51
60
  /**
52
61
  * android.webkit.WebChromeClient fundamentally does not support JS injection into frames other
@@ -68,6 +77,30 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
68
77
  protected boolean nestedScrollEnabled = false;
69
78
  protected ProgressChangedFilter progressChangedFilter;
70
79
 
80
+ // Content filter: when enabled, RNCWebViewClient.shouldInterceptRequest
81
+ // filters subresource requests through the native content filter.
82
+ protected volatile boolean contentBlockerEnabled = false;
83
+ // Top-frame URL, captured on the UI thread in RNCWebViewClient callbacks and
84
+ // read from shouldInterceptRequest (which runs off the UI thread). `volatile`
85
+ // gives the cross-thread visibility; do NOT call getUrl() off the UI thread.
86
+ protected volatile String lastTopUrl = null;
87
+
88
+ public void setContentBlockerEnabled(boolean enabled) {
89
+ this.contentBlockerEnabled = enabled;
90
+ }
91
+
92
+ public boolean getContentBlockerEnabled() {
93
+ return this.contentBlockerEnabled;
94
+ }
95
+
96
+ public void setLastTopUrl(String url) {
97
+ this.lastTopUrl = url;
98
+ }
99
+
100
+ public String getLastTopUrl() {
101
+ return this.lastTopUrl;
102
+ }
103
+
71
104
  /**
72
105
  * WebView must be created with an context of the current activity
73
106
  * <p>
@@ -81,10 +114,6 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
81
114
  progressChangedFilter = new ProgressChangedFilter();
82
115
  }
83
116
 
84
- public void setIgnoreErrFailedForThisURL(String url) {
85
- mRNCWebViewClient.setIgnoreErrFailedForThisURL(url);
86
- }
87
-
88
117
  public void setBasicAuthCredential(RNCBasicAuthCredential credential) {
89
118
  mRNCWebViewClient.setBasicAuthCredential(credential);
90
119
  }
@@ -246,12 +275,39 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
246
275
  return this.messagingEnabled;
247
276
  }
248
277
 
249
- protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) {
250
- if (bridge == null) {
251
- bridge = new RNCWebViewBridge(webView);
252
- addJavascriptInterface(bridge, JAVASCRIPT_INTERFACE);
278
+ protected void createRNCWebViewBridge(RNCWebView webView) {
279
+ if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)){
280
+ if (this.bridgeListener == null) {
281
+ this.bridgeListener = new WebViewCompat.WebMessageListener() {
282
+ @Override
283
+ public void onPostMessage(@NonNull WebView view, @NonNull WebMessageCompat message, @NonNull Uri sourceOrigin, boolean isMainFrame, @NonNull JavaScriptReplyProxy replyProxy) {
284
+ RNCWebView.this.onMessage(message.getData(), sourceOrigin.toString());
285
+ }
286
+ };
287
+ WebViewCompat.addWebMessageListener(
288
+ webView,
289
+ JAVASCRIPT_INTERFACE,
290
+ Set.of("*"),
291
+ this.bridgeListener
292
+ );
293
+ }
294
+ } else {
295
+ if (fallbackBridge == null) {
296
+ fallbackBridge = new RNCWebViewBridge(webView);
297
+ addJavascriptInterface(fallbackBridge, JAVASCRIPT_INTERFACE);
298
+ }
253
299
  }
254
- return bridge;
300
+ injectJavascriptObject();
301
+ }
302
+
303
+ private void injectJavascriptObject() {
304
+ if (getSettings().getJavaScriptEnabled()) {
305
+ String js = "(function(){\n" +
306
+ " window." + JAVASCRIPT_INTERFACE + " = window." + JAVASCRIPT_INTERFACE + " || {};\n" +
307
+ " window." + JAVASCRIPT_INTERFACE + ".injectedObjectJson = function () { return " + (injectedJavaScriptObject == null ? null : ("`" + injectedJavaScriptObject + "`")) + "; };\n" +
308
+ "})();";
309
+ evaluateJavascriptWithFallback(js);
310
+ }
255
311
  }
256
312
 
257
313
  @SuppressLint("AddJavascriptInterface")
@@ -276,6 +332,7 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
276
332
  injectedJS != null &&
277
333
  !TextUtils.isEmpty(injectedJS)) {
278
334
  evaluateJavascriptWithFallback("(function() {\n" + injectedJS + ";\n})();");
335
+ injectJavascriptObject(); // re-inject the Javascript object in case it has been overwritten.
279
336
  }
280
337
  }
281
338
 
@@ -284,17 +341,18 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
284
341
  injectedJSBeforeContentLoaded != null &&
285
342
  !TextUtils.isEmpty(injectedJSBeforeContentLoaded)) {
286
343
  evaluateJavascriptWithFallback("(function() {\n" + injectedJSBeforeContentLoaded + ";\n})();");
344
+ injectJavascriptObject(); // re-inject the Javascript object in case it has been overwritten.
287
345
  }
288
346
  }
289
347
 
348
+ protected String injectedJavaScriptObject = null;
349
+
290
350
  public void setInjectedJavaScriptObject(String obj) {
291
- if (getSettings().getJavaScriptEnabled()) {
292
- RNCWebViewBridge b = createRNCWebViewBridge(this);
293
- b.setInjectedObjectJson(obj);
294
- }
351
+ this.injectedJavaScriptObject = obj;
352
+ injectJavascriptObject();
295
353
  }
296
354
 
297
- public void onMessage(String message) {
355
+ public void onMessage(String message, String sourceUrl) {
298
356
  ThemedReactContext reactContext = getThemedReactContext();
299
357
  RNCWebView mWebView = this;
300
358
 
@@ -306,7 +364,7 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
306
364
  if (mRNCWebViewClient == null) {
307
365
  return;
308
366
  }
309
- WritableMap data = mRNCWebViewClient.createWebViewEvent(webView, webView.getUrl());
367
+ WritableMap data = mRNCWebViewClient.createWebViewEvent(webView, sourceUrl);
310
368
  data.putString("data", message);
311
369
 
312
370
  if (mMessagingJSModule != null) {
@@ -408,16 +466,11 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
408
466
  protected class RNCWebViewBridge {
409
467
  private String TAG = "RNCWebViewBridge";
410
468
  RNCWebView mWebView;
411
- String injectedObjectJson;
412
469
 
413
470
  RNCWebViewBridge(RNCWebView c) {
414
471
  mWebView = c;
415
472
  }
416
473
 
417
- public void setInjectedObjectJson(String s) {
418
- injectedObjectJson = s;
419
- }
420
-
421
474
  /**
422
475
  * This method is called whenever JavaScript running within the web view calls:
423
476
  * - window[JAVASCRIPT_INTERFACE].postMessage
@@ -425,14 +478,12 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
425
478
  @JavascriptInterface
426
479
  public void postMessage(String message) {
427
480
  if (mWebView.getMessagingEnabled()) {
428
- mWebView.onMessage(message);
481
+ // Post to main thread because `mWebView.getUrl()` requires to be executed on main.
482
+ mWebView.post(() -> mWebView.onMessage(message, mWebView.getUrl()));
429
483
  } else {
430
484
  FLog.w(TAG, "ReactNativeWebView.postMessage method was called but messaging is disabled. Pass an onMessage handler to the WebView.");
431
485
  }
432
486
  }
433
-
434
- @JavascriptInterface
435
- public String injectedObjectJson() { return injectedObjectJson; }
436
487
  }
437
488
 
438
489
 
@@ -2,6 +2,7 @@ package com.reactnativecommunity.webview;
2
2
 
3
3
  import android.annotation.TargetApi;
4
4
  import android.graphics.Bitmap;
5
+ import android.net.Uri;
5
6
  import android.net.http.SslError;
6
7
  import android.os.Build;
7
8
  import android.os.SystemClock;
@@ -25,6 +26,7 @@ import com.facebook.react.bridge.ReadableArray;
25
26
  import com.facebook.react.bridge.WritableMap;
26
27
  import com.facebook.react.uimanager.ThemedReactContext;
27
28
  import com.facebook.react.uimanager.UIManagerHelper;
29
+ import com.reactnativecommunity.webview.events.SubResourceErrorEvent;
28
30
  import com.reactnativecommunity.webview.events.TopHttpErrorEvent;
29
31
  import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
30
32
  import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
@@ -34,6 +36,9 @@ import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEven
34
36
  import android.webkit.CookieManager;
35
37
  import android.webkit.CookieSyncManager;
36
38
 
39
+ import java.io.ByteArrayInputStream;
40
+ import java.util.HashMap;
41
+ import java.util.Map;
37
42
  import java.util.concurrent.atomic.AtomicReference;
38
43
 
39
44
  public class RNCWebViewClient extends WebViewClient {
@@ -42,13 +47,8 @@ public class RNCWebViewClient extends WebViewClient {
42
47
 
43
48
  protected boolean mLastLoadFailed = false;
44
49
  protected RNCWebView.ProgressChangedFilter progressChangedFilter = null;
45
- protected @Nullable String ignoreErrFailedForThisURL = null;
46
50
  protected @Nullable RNCBasicAuthCredential basicAuthCredential = null;
47
51
 
48
- public void setIgnoreErrFailedForThisURL(@Nullable String url) {
49
- ignoreErrFailedForThisURL = url;
50
- }
51
-
52
52
  public void setBasicAuthCredential(@Nullable RNCBasicAuthCredential credential) {
53
53
  basicAuthCredential = credential;
54
54
  }
@@ -78,6 +78,10 @@ public class RNCWebViewClient extends WebViewClient {
78
78
  public void doUpdateVisitedHistory (WebView webView, String url, boolean isReload) {
79
79
  super.doUpdateVisitedHistory(webView, url, isReload);
80
80
 
81
+ // Capture the top-frame URL on the UI thread for off-thread use in
82
+ // shouldInterceptRequest (also fires on SPA pushState navigations).
83
+ ((RNCWebView) webView).setLastTopUrl(url);
84
+
81
85
  ((RNCWebView) webView).dispatchEvent(
82
86
  webView,
83
87
  new TopLoadingStartEvent(
@@ -91,6 +95,7 @@ public class RNCWebViewClient extends WebViewClient {
91
95
  mLastLoadFailed = false;
92
96
 
93
97
  RNCWebView reactWebView = (RNCWebView) webView;
98
+ reactWebView.setLastTopUrl(url);
94
99
  reactWebView.callInjectedJavaScriptBeforeContentLoaded();
95
100
  }
96
101
 
@@ -150,6 +155,138 @@ public class RNCWebViewClient extends WebViewClient {
150
155
  return this.shouldOverrideUrlLoading(view, url);
151
156
  }
152
157
 
158
+ // ------------------------------------------------------------------
159
+ // Content filter: subresource request interception.
160
+ // shouldInterceptRequest runs OFF the UI thread and is invoked for every
161
+ // subresource. Matching goes through the native content filter and fails
162
+ // open (returns super => request proceeds) whenever anything is unclear.
163
+ // ------------------------------------------------------------------
164
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
165
+ @Override
166
+ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
167
+ if (!(view instanceof RNCWebView) || request == null) {
168
+ return super.shouldInterceptRequest(view, request);
169
+ }
170
+ RNCWebView rncWebView = (RNCWebView) view;
171
+ if (!rncWebView.getContentBlockerEnabled()) {
172
+ return super.shouldInterceptRequest(view, request);
173
+ }
174
+ // Never block top-frame navigations — only subresources.
175
+ if (request.isForMainFrame()) {
176
+ return super.shouldInterceptRequest(view, request);
177
+ }
178
+ Uri uri = request.getUrl();
179
+ if (uri == null) {
180
+ return super.shouldInterceptRequest(view, request);
181
+ }
182
+ String scheme = uri.getScheme();
183
+ if (scheme == null || !(scheme.equals("http") || scheme.equals("https"))) {
184
+ return super.shouldInterceptRequest(view, request);
185
+ }
186
+
187
+ String url = uri.toString();
188
+ String sourceUrl = rncWebView.getLastTopUrl();
189
+ String requestType = inferRequestType(request);
190
+
191
+ try {
192
+ if (ContentFilter.INSTANCE.shouldBlock(url, sourceUrl, requestType)) {
193
+ return buildBlockedResponse(requestType);
194
+ }
195
+ } catch (Throwable t) {
196
+ // Fail open on any unexpected error.
197
+ }
198
+ return super.shouldInterceptRequest(view, request);
199
+ }
200
+
201
+ private static String headerCaseInsensitive(Map<String, String> headers, String name) {
202
+ if (headers == null) return null;
203
+ String v = headers.get(name);
204
+ if (v != null) return v;
205
+ for (Map.Entry<String, String> e : headers.entrySet()) {
206
+ if (name.equalsIgnoreCase(e.getKey())) return e.getValue();
207
+ }
208
+ return null;
209
+ }
210
+
211
+ // Android does not expose the resource type on WebResourceRequest, so infer
212
+ // a request-type string from Sec-Fetch-Dest / Accept / extension.
213
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
214
+ private static String inferRequestType(WebResourceRequest request) {
215
+ Map<String, String> headers = request.getRequestHeaders();
216
+
217
+ String dest = headerCaseInsensitive(headers, "Sec-Fetch-Dest");
218
+ if (dest != null) {
219
+ switch (dest) {
220
+ case "script": return "script";
221
+ case "image": return "image";
222
+ case "style": return "stylesheet";
223
+ case "font": return "font";
224
+ case "iframe":
225
+ case "frame": return "sub_frame";
226
+ case "audio":
227
+ case "video":
228
+ case "track": return "media";
229
+ case "document": return "document";
230
+ default: break;
231
+ }
232
+ }
233
+ String mode = headerCaseInsensitive(headers, "Sec-Fetch-Mode");
234
+ if ("empty".equals(dest) && ("cors".equals(mode) || "no-cors".equals(mode))) {
235
+ return "xmlhttprequest";
236
+ }
237
+
238
+ String accept = headerCaseInsensitive(headers, "Accept");
239
+ if (accept != null) {
240
+ String a = accept.toLowerCase();
241
+ if (a.startsWith("text/css")) return "stylesheet";
242
+ if (a.startsWith("image/")) return "image";
243
+ if (a.contains("javascript") || a.contains("ecmascript")) return "script";
244
+ if (a.startsWith("text/html")) return "sub_frame";
245
+ }
246
+ String requestedWith = headerCaseInsensitive(headers, "X-Requested-With");
247
+ if (requestedWith != null && requestedWith.toLowerCase().contains("xmlhttprequest")) {
248
+ return "xmlhttprequest";
249
+ }
250
+
251
+ String path = request.getUrl().getPath();
252
+ if (path != null) {
253
+ String p = path.toLowerCase();
254
+ if (p.endsWith(".js") || p.endsWith(".mjs")) return "script";
255
+ if (p.endsWith(".css")) return "stylesheet";
256
+ if (p.endsWith(".png") || p.endsWith(".jpg") || p.endsWith(".jpeg")
257
+ || p.endsWith(".webp") || p.endsWith(".gif") || p.endsWith(".svg")
258
+ || p.endsWith(".ico") || p.endsWith(".bmp")) return "image";
259
+ if (p.endsWith(".woff") || p.endsWith(".woff2") || p.endsWith(".ttf")
260
+ || p.endsWith(".otf") || p.endsWith(".eot")) return "font";
261
+ if (p.endsWith(".mp4") || p.endsWith(".webm") || p.endsWith(".m3u8")
262
+ || p.endsWith(".ts") || p.endsWith(".mp3") || p.endsWith(".m4a")
263
+ || p.endsWith(".aac") || p.endsWith(".ogg")) return "media";
264
+ }
265
+ return "other";
266
+ }
267
+
268
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
269
+ private WebResourceResponse buildBlockedResponse(String requestType) {
270
+ String mime;
271
+ switch (requestType) {
272
+ case "script": mime = "application/javascript"; break;
273
+ case "stylesheet": mime = "text/css"; break;
274
+ case "image": mime = "image/png"; break;
275
+ case "font": mime = "font/woff2"; break;
276
+ case "media": mime = "video/mp4"; break;
277
+ default: mime = "text/plain"; break;
278
+ }
279
+ WebResourceResponse response =
280
+ new WebResourceResponse(mime, "utf-8", new ByteArrayInputStream(new byte[0]));
281
+ // Set an explicit status so the response headers below are honored
282
+ // (a status-0 response can have its headers ignored on some WebViews).
283
+ response.setStatusCodeAndReasonPhrase(200, "OK");
284
+ Map<String, String> headers = new HashMap<>();
285
+ headers.put("Access-Control-Allow-Origin", "*");
286
+ response.setResponseHeaders(headers);
287
+ return response;
288
+ }
289
+
153
290
  @Override
154
291
  public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
155
292
  if (basicAuthCredential != null) {
@@ -173,12 +310,6 @@ public class RNCWebViewClient extends WebViewClient {
173
310
  // Undesired behavior: Return value of WebView.getUrl() may be the current URL instead of the failing URL.
174
311
  handler.cancel();
175
312
 
176
- if (!topWindowUrl.equalsIgnoreCase(failingUrl)) {
177
- // If error is not due to top-level navigation, then do not call onReceivedError()
178
- Log.w(TAG, "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl);
179
- return;
180
- }
181
-
182
313
  int code = error.getPrimaryError();
183
314
  String description = "";
184
315
  String descriptionPrefix = "SSL error: ";
@@ -210,6 +341,18 @@ public class RNCWebViewClient extends WebViewClient {
210
341
 
211
342
  description = descriptionPrefix + description;
212
343
 
344
+ if (!topWindowUrl.equalsIgnoreCase(failingUrl)) {
345
+ // If error is not due to top-level navigation, then do not call onReceivedError()
346
+ Log.w(TAG, "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl);
347
+ this.onReceivedSubResourceSslError(
348
+ webView,
349
+ code,
350
+ description,
351
+ failingUrl
352
+ );
353
+ return;
354
+ }
355
+
213
356
  this.onReceivedError(
214
357
  webView,
215
358
  code,
@@ -218,6 +361,20 @@ public class RNCWebViewClient extends WebViewClient {
218
361
  );
219
362
  }
220
363
 
364
+ public void onReceivedSubResourceSslError(
365
+ WebView webView,
366
+ int errorCode,
367
+ String description,
368
+ String failingUrl) {
369
+
370
+ WritableMap eventData = createWebViewEvent(webView, failingUrl);
371
+ eventData.putDouble("code", errorCode);
372
+ eventData.putString("description", description);
373
+
374
+ int reactTag = RNCWebViewWrapper.getReactTagFromWebView(webView);
375
+ UIManagerHelper.getEventDispatcherForReactTag((ReactContext) webView.getContext(), reactTag).dispatchEvent(new SubResourceErrorEvent(reactTag, eventData));
376
+ }
377
+
221
378
  @Override
222
379
  public void onReceivedError(
223
380
  WebView webView,
@@ -225,20 +382,6 @@ public class RNCWebViewClient extends WebViewClient {
225
382
  String description,
226
383
  String failingUrl) {
227
384
 
228
- if (ignoreErrFailedForThisURL != null
229
- && failingUrl.equals(ignoreErrFailedForThisURL)
230
- && errorCode == -1
231
- && description.equals("net::ERR_FAILED")) {
232
-
233
- // This is a workaround for a bug in the WebView.
234
- // See these chromium issues for more context:
235
- // https://bugs.chromium.org/p/chromium/issues/detail?id=1023678
236
- // https://bugs.chromium.org/p/chromium/issues/detail?id=1050635
237
- // This entire commit should be reverted once this bug is resolved in chromium.
238
- setIgnoreErrFailedForThisURL(null);
239
- return;
240
- }
241
-
242
385
  super.onReceivedError(webView, errorCode, description, failingUrl);
243
386
  mLastLoadFailed = true;
244
387
 
@@ -92,7 +92,6 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) {
92
92
  WebView.setWebContentsDebuggingEnabled(true)
93
93
  }
94
94
  webView.setDownloadListener(DownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
95
- webView.setIgnoreErrFailedForThisURL(url)
96
95
  val module = webView.reactApplicationContext.getNativeModule(RNCWebViewModule::class.java) ?: return@DownloadListener
97
96
  val request: DownloadManager.Request = try {
98
97
  DownloadManager.Request(Uri.parse(url))
@@ -341,11 +340,9 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) {
341
340
  }
342
341
  "injectJavaScript" -> webView.evaluateJavascriptWithFallback(args.getString(0))
343
342
  "loadUrl" -> {
344
- if (args == null) {
345
- throw RuntimeException("Arguments for loading an url are null!")
346
- }
343
+ val url = args?.getString(0) ?: throw RuntimeException("Arguments for loading an url are null!")
347
344
  webView.progressChangedFilter.setWaitingForCommandLoadUrl(false)
348
- args.getString(0)?.let { webView.loadUrl(it) }
345
+ webView.loadUrl(url)
349
346
  }
350
347
  "requestFocus" -> webView.requestFocus()
351
348
  "clearFormData" -> webView.clearFormData()
@@ -541,7 +538,11 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) {
541
538
  }
542
539
 
543
540
  fun setContentBlockerEnabled(viewWrapper: RNCWebViewWrapper, value: Boolean) {
544
- //
541
+ viewWrapper.webView.setContentBlockerEnabled(value)
542
+ if (value) {
543
+ // Lazily warm up the native engine only when filtering is actually used.
544
+ ContentFilter.ensureInitialized(viewWrapper.webView.context)
545
+ }
545
546
  }
546
547
 
547
548
  fun setHasOnScroll(viewWrapper: RNCWebViewWrapper, value: Boolean) {
@@ -721,4 +722,11 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) {
721
722
  fun setWebviewDebuggingEnabled(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
722
723
  RNCWebView.setWebContentsDebuggingEnabled(enabled)
723
724
  }
724
- }
725
+
726
+ fun setPaymentRequestEnabled(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
727
+ val view = viewWrapper.webView
728
+ if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) {
729
+ WebSettingsCompat.setPaymentRequestEnabled(view.settings, enabled)
730
+ }
731
+ }
732
+ }
@@ -0,0 +1,25 @@
1
+ package com.reactnativecommunity.webview.events
2
+
3
+ import com.facebook.react.bridge.WritableMap
4
+ import com.facebook.react.uimanager.events.Event
5
+ import com.facebook.react.uimanager.events.RCTEventEmitter
6
+
7
+ /**
8
+ * Event emitted when there is an error in loading a subresource
9
+ */
10
+ class SubResourceErrorEvent(viewId: Int, private val mEventData: WritableMap) :
11
+ Event<SubResourceErrorEvent>(viewId) {
12
+ companion object {
13
+ const val EVENT_NAME = "topLoadingSubResourceError"
14
+ }
15
+
16
+ override fun getEventName(): String = EVENT_NAME
17
+
18
+ override fun canCoalesce(): Boolean = false
19
+
20
+ override fun getCoalescingKey(): Short = 0
21
+
22
+ override fun dispatch(rctEventEmitter: RCTEventEmitter) =
23
+ rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
24
+
25
+ }
@@ -15,6 +15,7 @@ import com.facebook.react.viewmanagers.RNCWebViewManagerDelegate;
15
15
  import com.facebook.react.viewmanagers.RNCWebViewManagerInterface;
16
16
  import com.facebook.react.views.scroll.ScrollEventType;
17
17
  import com.reactnativecommunity.webview.events.TopCustomMenuSelectionEvent;
18
+ import com.reactnativecommunity.webview.events.SubResourceErrorEvent;
18
19
  import com.reactnativecommunity.webview.events.TopHttpErrorEvent;
19
20
  import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
20
21
  import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
@@ -232,7 +233,7 @@ public class RNCWebViewManager extends ViewGroupManager<RNCWebViewWrapper>
232
233
  }
233
234
 
234
235
  @Override
235
- @ReactProp(name = "suppressMenuItems ")
236
+ @ReactProp(name = "suppressMenuItems")
236
237
  public void setSuppressMenuItems(RNCWebViewWrapper view, @Nullable ReadableArray items) {}
237
238
 
238
239
  @Override
@@ -337,6 +338,12 @@ public class RNCWebViewManager extends ViewGroupManager<RNCWebViewWrapper>
337
338
  mRNCWebViewManagerImpl.setWebviewDebuggingEnabled(view, value);
338
339
  }
339
340
 
341
+ @Override
342
+ @ReactProp(name = "paymentRequestEnabled")
343
+ public void setPaymentRequestEnabled(RNCWebViewWrapper view, boolean value) {
344
+ mRNCWebViewManagerImpl.setPaymentRequestEnabled(view, value);
345
+ }
346
+
340
347
  /* iOS PROPS - no implemented here */
341
348
  @Override
342
349
  public void setAllowingReadAccessToURL(RNCWebViewWrapper view, @Nullable String value) {}
@@ -401,6 +408,9 @@ public class RNCWebViewManager extends ViewGroupManager<RNCWebViewWrapper>
401
408
  @Override
402
409
  public void setRefreshControlLightMode(RNCWebViewWrapper view, boolean value) {}
403
410
 
411
+ @Override
412
+ public void setIndicatorStyle(RNCWebViewWrapper view, @Nullable String value) {}
413
+
404
414
  @Override
405
415
  public void setScrollEnabled(RNCWebViewWrapper view, boolean value) {}
406
416
 
@@ -521,6 +531,7 @@ public class RNCWebViewManager extends ViewGroupManager<RNCWebViewWrapper>
521
531
  export.put(TopLoadingStartEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingStart"));
522
532
  export.put(TopLoadingFinishEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingFinish"));
523
533
  export.put(TopLoadingErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingError"));
534
+ export.put(SubResourceErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingSubResourceError"));
524
535
  export.put(TopMessageEvent.EVENT_NAME, MapBuilder.of("registrationName", "onMessage"));
525
536
  // !Default events but adding them here explicitly for clarity
526
537