Skip to content

Commit

Permalink
feat: refactor webview (#1)
Browse files Browse the repository at this point in the history
* feat: add loadUrl

* feat: add onnewwindow

commit e33cf02
Author: Yannick Chiron <yannick.chiron@gmail.com>
Date:   Sat Oct 29 17:48:53 2022 +0200

    fix: Store mHasOnOpenWindowEvent state in RNCWebViewManager

commit 0c8ad2e
Author: Yannick Chiron <yannick.chiron@gmail.com>
Date:   Mon Aug 15 21:46:54 2022 +0200

    feat(ios): Call `onOpenWindow` only if react-native props is set

    On iOS, `onOpenWindow` event should be called only if the
    react-native prop is defined

    If not defined, then the default behavior should be run (navigate the
    current WebView to the target link)

commit 3f0b008
Author: Yannick Chiron <yannick.chiron@gmail.com>
Date:   Thu Sep 8 17:47:44 2022 +0200

    feat(android): Call `onOpenWindow` only if react-native props is set

    On Android, `onOpenWindow` event should be called only if the
    react-native prop is defined

    If not defined, then the default behavior should be run (opening the
    device's browser)

commit baedc99
Author: Yannick Chiron <yannick.chiron@gmail.com>
Date:   Wed Sep 7 11:56:13 2022 +0200

    feat(ios): Add `hasTargetFrame` prop on `onShouldStartLoadWithRequest`

    On iOS, current implementation of `onShouldStartLoadWithRequest` does
    not allow to differentiate links that target the current WebView from
    ones that target a new WebView (i.e. `target="_blank"` links).

    In some scenario we want to have this info to prevent the WebView
    navigation

    This info can be useful if `onOpenWindow` event is not registered.
    Otherwise this event will be called instead of
    `onShouldStartLoadWithRequest`

commit 04e7387
Author: Yannick Chiron <yannick.chiron@gmail.com>
Date:   Wed Sep 7 12:13:07 2022 +0200

    chore: Add documentation for `onOpenWindow`

commit 539ba07
Author: Yannick Chiron <yannick.chiron@gmail.com>
Date:   Thu Sep 8 17:46:44 2022 +0200

    chore: Add examples for `onOpenWindow` event

commit 85fa577
Author: Yannick Chiron <yannick.chiron@gmail.com>
Date:   Thu Sep 8 18:24:27 2022 +0200

    feat(ios): Call `onOpenWindow` for links with `target="_blank"`

    On iOS, default behavior is to navigate the WebView to the target URL
    when a link with `target="_blank"` is clicked

    In some scenario we want to be able to intercept this from react-native
    and to handle the link by ourselves (i.e. to open it into an InApp
    Browser)

    The `onOpenWindow` event is now called in this scenario. Then the
    default "open browser" scenario does not occur

    Note that previous implementation only worked in apparence, but the
    WebView ref url would still be updated with new URL even if
    `return nil` is done in `createWebViewWithConfiguration`. So we want to
    intercept this earlier in `decidePolicyForNavigationAction` when the
    WebView did not update its ref yet

commit eb6e3ef
Author: Yannick Chiron <yannick.chiron@gmail.com>
Date:   Thu Sep 8 14:31:07 2022 +0200

    feat(iOS): Add `onOpenWindow` event

    On iOS, default behavior is to navigate the WebView to the target URL
    when `window.open('http://someurl, '_blank')` is called from JS

    In some scenario we want to be able to intercept this from react-native
    and to handle the link by ourselves (i.e. to open it into an InApp
    Browser)

    The `onOpenWindow` event is now called in this scenario. Then the
    default "open browser" scenario does not occur

commit 84fd919
Author: Yannick Chiron <yannick.chiron@gmail.com>
Date:   Mon Sep 5 15:51:56 2022 +0200

    feat(android): Add `onOpenWindow` event

    On Android, default behavior is to open the device's browser when
    `window.open('http://someurl, '_blank')` is called from JS or when a
    link with `target="_blank"` is clicked

    In some scenario we want to be able to intercept this from react-native
    and to handle the link by ourselves (i.e. to open it into an InApp
    Browser)

    The `onOpenWindow` event is now called on those scenario. In this case,
    the default "open browser" scenario does not occur

commit f2fc2c6
Author: Yannick Chiron <yannick.chiron@gmail.com>
Date:   Mon Aug 15 18:56:18 2022 +0200

    chore: Fix iOS example's build instructions

* fix: typo

* feat(ios): Cookie sync improvements (react-native-webview#2535)

commit 70e84de
Author: semantic-release-bot <semantic-release-bot@martynus.net>
Date:   Wed Nov 23 01:04:51 2022 +0000

    chore(release): 11.24.0 [skip ci]

    # [11.24.0](react-native-webview/react-native-webview@v11.23.1...v11.24.0) (2022-11-23)

    ### Features

    * **ios:** Cookie sync improvements ([react-native-webview#2535](react-native-webview#2535)) ([4ac0d74](react-native-webview@4ac0d74))

commit 4ac0d74
Author: Matias Korhonen <matias.korhonen@kaikuhealth.com>
Date:   Wed Nov 23 03:03:21 2022 +0200

    feat(ios): Cookie sync improvements (react-native-webview#2535)

commit a5e2a9f
Author: Handschrift <privacy.mtehw@aleeas.com>
Date:   Wed Nov 23 00:11:43 2022 +0000

    chore(docs): Add information that custom menu items are only available for iOS (react-native-webview#2748)

commit d6af98f
Author: Caleb Clarke <TheAlmightyBob@users.noreply.github.com>
Date:   Wed Nov 9 12:39:52 2022 -0800

    chore(ci): Update React Native to fix Android build (react-native-webview#2734)

    See facebook/react-native#35210

* chore: ensure webviewRef

* feat: fraudulentWebsiteWarningEnabled

* fix: missing type

* feat: add ios13 check

* fix: doc
  • Loading branch information
sunnylqm authored Jul 19, 2023
1 parent c14dcc2 commit 825169e
Show file tree
Hide file tree
Showing 17 changed files with 451 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
import com.reactnativecommunity.webview.events.TopMessageEvent;
import com.reactnativecommunity.webview.events.TopOpenWindowEvent;
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
import com.reactnativecommunity.webview.events.TopRenderProcessGoneEvent;

Expand Down Expand Up @@ -161,6 +162,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {

protected RNCWebChromeClient mWebChromeClient = null;
protected boolean mAllowsFullscreenVideo = false;
protected boolean mHasOnOpenWindowEvent = false;
protected @Nullable String mUserAgent = null;
protected @Nullable String mUserAgentWithApplicationName = null;
protected @Nullable String mDownloadingMessage = null;
Expand Down Expand Up @@ -311,6 +313,17 @@ public void setLackPermissionToDownlaodMessage(WebView view, String message) {
mLackPermissionToDownloadMessage = message;
}

@ReactProp(name = "hasOnOpenWindowEvent")
public void setHasOnOpenWindowEvent(WebView view, boolean hasEvent) {
mHasOnOpenWindowEvent = hasEvent;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
WebChromeClient client = view.getWebChromeClient();
if (client instanceof RNCWebChromeClient) {
((RNCWebChromeClient) client).setHasOnOpenWindowEvent(hasEvent);
}
}
}

@ReactProp(name = "cacheEnabled")
public void setCacheEnabled(WebView view, boolean enabled) {
view.getSettings().setCacheMode(enabled ? WebSettings.LOAD_DEFAULT : WebSettings.LOAD_NO_CACHE);
Expand Down Expand Up @@ -694,6 +707,7 @@ public Map getExportedCustomDirectEventTypeConstants() {
export.put(ScrollEventType.getJSEventName(ScrollEventType.SCROLL), MapBuilder.of("registrationName", "onScroll"));
export.put(TopHttpErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onHttpError"));
export.put(TopRenderProcessGoneEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRenderProcessGone"));
export.put(TopOpenWindowEvent.EVENT_NAME, MapBuilder.of("registrationName", "onOpenWindow"));
return export;
}

Expand Down Expand Up @@ -876,6 +890,7 @@ public void onHideCustomView() {
}
};

mWebChromeClient.setHasOnOpenWindowEvent(mHasOnOpenWindowEvent);
webView.setWebChromeClient(mWebChromeClient);
} else {
if (mWebChromeClient != null) {
Expand All @@ -889,6 +904,7 @@ public Bitmap getDefaultVideoPoster() {
}
};

mWebChromeClient.setHasOnOpenWindowEvent(mHasOnOpenWindowEvent);
webView.setWebChromeClient(mWebChromeClient);
}
}
Expand Down Expand Up @@ -1227,6 +1243,8 @@ protected static class RNCWebChromeClient extends WebChromeClient implements Lif

protected RNCWebView.ProgressChangedFilter progressChangedFilter = null;

protected boolean hasOnOpenWindowEvent = false;

public RNCWebChromeClient(ReactContext reactContext, WebView webView) {
this.mReactContext = reactContext;
this.mWebView = webView;
Expand All @@ -1236,6 +1254,24 @@ public RNCWebChromeClient(ReactContext reactContext, WebView webView) {
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {

final WebView newWebView = new WebView(view.getContext());

if(hasOnOpenWindowEvent) {
newWebView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading (WebView subview, String url) {
WritableMap event = Arguments.createMap();
event.putString("targetUrl", url);

((RNCWebView) view).dispatchEvent(
view,
new TopOpenWindowEvent(view.getId(), event)
);

return true;
}
});
}

final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(newWebView);
resultMsg.sendToTarget();
Expand Down Expand Up @@ -1480,6 +1516,10 @@ protected ViewGroup getRootView() {
public void setProgressChangedFilter(RNCWebView.ProgressChangedFilter filter) {
progressChangedFilter = filter;
}

public void setHasOnOpenWindowEvent(boolean hasEvent) {
hasOnOpenWindowEvent = hasEvent;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.reactnativecommunity.webview.events

import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter

/**
* Event emitted when the WebView opens a new Window (i.e: target=_blank)
*/
class TopOpenWindowEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopOpenWindowEvent>(viewId) {
companion object {
const val EVENT_NAME = "topOpenWindow"
}

override fun getEventName(): String = EVENT_NAME

override fun canCoalesce(): Boolean = false

override fun getCoalescingKey(): Short = 0

override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)

}
4 changes: 4 additions & 0 deletions apple/RNCWebView.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *_Nonnull)request
@property (nonatomic, assign) RNCWebViewPermissionGrantType mediaCapturePermissionGrantType;
#endif

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */
@property (nonatomic, assign) BOOL fraudulentWebsiteWarningEnabled;
#endif

+ (void)setClientAuthenticationCredential:(nullable NSURLCredential*)credential;
+ (void)setCustomCertificatesForHost:(nullable NSDictionary *)certificates;
- (void)postMessage:(NSString *_Nullable)message;
Expand Down
71 changes: 56 additions & 15 deletions apple/RNCWebView.m
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ - (void)scrollWheel:(NSEvent *)theEvent {
@end
#endif // TARGET_OS_OSX

@interface RNCWebView () <WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler,
@interface RNCWebView () <WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler, WKHTTPCookieStoreObserver,
#if !TARGET_OS_OSX
UIScrollViewDelegate,
#endif // !TARGET_OS_OSX
Expand All @@ -80,6 +80,7 @@ @interface RNCWebView () <WKUIDelegate, WKNavigationDelegate, WKScriptMessageHan
@property (nonatomic, copy) RCTDirectEventBlock onMessage;
@property (nonatomic, copy) RCTDirectEventBlock onScroll;
@property (nonatomic, copy) RCTDirectEventBlock onContentProcessDidTerminate;
@property (nonatomic, copy) RCTDirectEventBlock onOpenWindow;
#if !TARGET_OS_OSX
@property (nonatomic, copy) WKWebView *webView;
#else
Expand Down Expand Up @@ -234,6 +235,9 @@ - (void)startLongPress:(UILongPressGestureRecognizer *)pressSender
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (@available(iOS 11.0, *)) {
[self.webView.configuration.websiteDataStore.httpCookieStore removeObserver:self];
}
}

- (void)tappedMenuItem:(NSString *)eventType
Expand Down Expand Up @@ -310,7 +314,15 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
NSURL *url = navigationAction.request.URL;

if (_onOpenWindow) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{@"targetUrl": url.absoluteString}];
_onOpenWindow(event);
} else {
[webView loadRequest:navigationAction.request];
}
}
return nil;
}
Expand All @@ -324,6 +336,14 @@ - (WKWebViewConfiguration *)setUpWkWebViewConfig
prefs.javaScriptEnabled = NO;
_prefsUsed = YES;
}
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */
if (@available(iOS 13.0, *)) {
if (!_fraudulentWebsiteWarningEnabled) {
prefs.fraudulentWebsiteWarningEnabled = NO;
_prefsUsed = YES;
}
}
#endif
if (_allowUniversalAccessFromFileURLs) {
[wkWebViewConfig setValue:@TRUE forKey:@"allowUniversalAccessFromFileURLs"];
}
Expand Down Expand Up @@ -712,7 +732,7 @@ - (void)visitSource
[_webView loadHTMLString:@"" baseURL:nil];
return;
}
if (request.URL.host) {
if (request.URL.host || [request.URL.absoluteString isEqualToString:@"about:blank"]) {
[_webView loadRequest:request];
}
else {
Expand Down Expand Up @@ -1140,7 +1160,22 @@ - (void) webView:(WKWebView *)webView
WKNavigationType navigationType = navigationAction.navigationType;
NSURLRequest *request = navigationAction.request;
BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];

BOOL hasTargetFrame = navigationAction.targetFrame != nil;

if (_onOpenWindow && !hasTargetFrame) {
// When OnOpenWindow should be called, we want to prevent the navigation
// If not prevented, the `decisionHandler` is called first and after that `createWebViewWithConfiguration` is called
// In that order the WebView's ref would be updated with the target URL even if `createWebViewWithConfiguration` does not call `loadRequest`
// So the WebView's document stays on the current URL, but the WebView's ref is replaced by the target URL
// By preventing the navigation here, we also prevent the WebView's ref mutation
// The counterpart is that we have to manually call `_onOpenWindow` here, because no navigation means no call to `createWebViewWithConfiguration`
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{@"targetUrl": request.URL.absoluteString}];
decisionHandler(WKNavigationActionPolicyCancel);
_onOpenWindow(event);
return;
}

if (_onShouldStartLoadWithRequest) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
if (request.mainDocumentURL) {
Expand All @@ -1151,7 +1186,8 @@ - (void) webView:(WKWebView *)webView
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(navigationType)],
@"isTopFrame": @(isTopFrame)
@"isTopFrame": @(isTopFrame),
@"hasTargetFrame": @(hasTargetFrame),
}];
if (![self.delegate webView:self
shouldStartLoadForRequest:event
Expand Down Expand Up @@ -1326,23 +1362,26 @@ -(void)appWillResignActive
*/
- (void)webView:(WKWebView *)webView
didFinishNavigation:(WKNavigation *)navigation
{
if (_ignoreSilentHardwareSwitch) {
[self forceIgnoreSilentHardwareSwitch:true];
}

if (_onLoadingFinish) {
_onLoadingFinish([self baseEvent]);
}
}

- (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore
{
if(_sharedCookiesEnabled && @available(iOS 11.0, *)) {
// Write all cookies from WKWebView back to sharedHTTPCookieStorage
[webView.configuration.websiteDataStore.httpCookieStore getAllCookies:^(NSArray* cookies) {
[cookieStore getAllCookies:^(NSArray* cookies) {
for (NSHTTPCookie *cookie in cookies) {
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
}
}];
}

if (_ignoreSilentHardwareSwitch) {
[self forceIgnoreSilentHardwareSwitch:true];
}

if (_onLoadingFinish) {
_onLoadingFinish([self baseEvent]);
}
}

- (void)injectJavaScript:(NSString *)script
Expand Down Expand Up @@ -1559,7 +1598,9 @@ - (void)resetupScripts:(WKWebViewConfiguration *)wkWebViewConfig {
if(!_incognito && !_cacheEnabled) {
wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
}
[self syncCookiesToWebView:nil];
[self syncCookiesToWebView:^{
[wkWebViewConfig.websiteDataStore.httpCookieStore addObserver:self];
}];
} else {
NSMutableString *script = [NSMutableString string];

Expand Down
20 changes: 20 additions & 0 deletions apple/RNCWebViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ - (RCTUIView *)view
RCT_EXPORT_VIEW_PROPERTY(onHttpError, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onContentProcessDidTerminate, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onOpenWindow, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptBeforeContentLoaded, NSString)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptForMainFrameOnly, BOOL)
Expand Down Expand Up @@ -112,6 +113,10 @@ - (RCTUIView *)view
RCT_EXPORT_VIEW_PROPERTY(mediaCapturePermissionGrantType, RNCWebViewPermissionGrantType)
#endif

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */
RCT_EXPORT_VIEW_PROPERTY(fraudulentWebsiteWarningEnabled, BOOL)
#endif

/**
* Expose methods to enable messaging the webview.
*/
Expand Down Expand Up @@ -252,6 +257,21 @@ - (RCTUIView *)view
}];
}


RCT_EXPORT_METHOD(loadUrl:(nonnull NSNumber *)reactTag url:(NSString *)url)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RNCWebView *> *viewRegistry) {
RNCWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RNCWebView class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RNCWebView, got: %@", view);
} else {
NSDictionary *source = [NSDictionary dictionaryWithObjectsAndKeys:url,@"uri",nil];
[view setSource:source];
}
}];
}


#pragma mark - Exported synchronous methods

- (BOOL) webView:(RNCWebView *)webView
Expand Down
2 changes: 1 addition & 1 deletion docs/Contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The Android example app will built, the Metro Bundler will launch, and the examp
#### For iOS:

```sh
pod install --project-directory=ios
pod install --project-directory=example/ios
yarn ios
```

Expand Down
Loading

0 comments on commit 825169e

Please sign in to comment.