forked from Ivasoft/mattermost-mobile
IWA support for Android and iOS (#2654)
This commit is contained in:
committed by
Miguel Alatzar
parent
d852e0991f
commit
fc5f33c03c
693
native_modules/RNCWKWebView.m
Normal file
693
native_modules/RNCWKWebView.m
Normal file
@@ -0,0 +1,693 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#import "RNCWKWebView.h"
|
||||
#import <React/RCTConvert.h>
|
||||
#import <React/RCTAutoInsetsProtocol.h>
|
||||
#import "RNCWKProcessPoolManager.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "objc/runtime.h"
|
||||
|
||||
static NSTimer *keyboardTimer;
|
||||
static NSString *const MessageHandlerName = @"ReactNativeWebView";
|
||||
static NSURLCredential* clientAuthenticationCredential;
|
||||
|
||||
// runtime trick to remove WKWebView keyboard default toolbar
|
||||
// see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
|
||||
@interface _SwizzleHelperWK : NSObject @end
|
||||
@implementation _SwizzleHelperWK
|
||||
-(id)inputAccessoryView
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface RNCWKWebView () <WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler, UIScrollViewDelegate, RCTAutoInsetsProtocol>
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onLoadingProgress;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onMessage;
|
||||
@property (nonatomic, copy) WKWebView *webView;
|
||||
@end
|
||||
|
||||
@implementation RNCWKWebView
|
||||
{
|
||||
UIColor * _savedBackgroundColor;
|
||||
BOOL _savedHideKeyboardAccessoryView;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if ((self = [super initWithFrame:frame])) {
|
||||
super.backgroundColor = [UIColor clearColor];
|
||||
_bounces = YES;
|
||||
_scrollEnabled = YES;
|
||||
_showsHorizontalScrollIndicator = YES;
|
||||
_showsVerticalScrollIndicator = YES;
|
||||
_automaticallyAdjustContentInsets = YES;
|
||||
_contentInset = UIEdgeInsetsZero;
|
||||
}
|
||||
|
||||
// Workaround for a keyboard dismissal bug present in iOS 12
|
||||
// https://openradar.appspot.com/radar?id=5018321736957952
|
||||
if (@available(iOS 12.0, *)) {
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserver:self
|
||||
selector:@selector(keyboardWillHide)
|
||||
name:UIKeyboardWillHideNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserver:self
|
||||
selector:@selector(keyboardWillShow)
|
||||
name:UIKeyboardWillShowNotification object:nil];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://stackoverflow.com/questions/25713069/why-is-wkwebview-not-opening-links-with-target-blank/25853806#25853806 for details.
|
||||
*/
|
||||
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
|
||||
{
|
||||
if (!navigationAction.targetFrame.isMainFrame) {
|
||||
[webView loadRequest:navigationAction.request];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
if (self.window != nil && _webView == nil) {
|
||||
WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new];
|
||||
if (_incognito) {
|
||||
wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
|
||||
} else if (_cacheEnabled) {
|
||||
wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore defaultDataStore];
|
||||
}
|
||||
if(self.useSharedProcessPool) {
|
||||
wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
|
||||
}
|
||||
wkWebViewConfig.userContentController = [WKUserContentController new];
|
||||
|
||||
if (_messagingEnabled) {
|
||||
[wkWebViewConfig.userContentController addScriptMessageHandler:self name:MessageHandlerName];
|
||||
|
||||
NSString *source = [NSString stringWithFormat:
|
||||
@"window.%@ = {"
|
||||
" postMessage: function (data) {"
|
||||
" window.webkit.messageHandlers.%@.postMessage(String(data));"
|
||||
" }"
|
||||
"};", MessageHandlerName, MessageHandlerName
|
||||
];
|
||||
|
||||
WKUserScript *script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
|
||||
[wkWebViewConfig.userContentController addUserScript:script];
|
||||
}
|
||||
|
||||
wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
|
||||
#if WEBKIT_IOS_10_APIS_AVAILABLE
|
||||
wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction
|
||||
? WKAudiovisualMediaTypeAll
|
||||
: WKAudiovisualMediaTypeNone;
|
||||
wkWebViewConfig.dataDetectorTypes = _dataDetectorTypes;
|
||||
#else
|
||||
wkWebViewConfig.mediaPlaybackRequiresUserAction = _mediaPlaybackRequiresUserAction;
|
||||
#endif
|
||||
|
||||
_webView = [[WKWebView alloc] initWithFrame:self.bounds configuration: wkWebViewConfig];
|
||||
_webView.scrollView.delegate = self;
|
||||
_webView.UIDelegate = self;
|
||||
_webView.navigationDelegate = self;
|
||||
_webView.scrollView.scrollEnabled = _scrollEnabled;
|
||||
_webView.scrollView.pagingEnabled = _pagingEnabled;
|
||||
_webView.scrollView.bounces = _bounces;
|
||||
_webView.scrollView.showsHorizontalScrollIndicator = _showsHorizontalScrollIndicator;
|
||||
_webView.scrollView.showsVerticalScrollIndicator = _showsVerticalScrollIndicator;
|
||||
_webView.allowsLinkPreview = _allowsLinkPreview;
|
||||
[_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
|
||||
_webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures;
|
||||
|
||||
if (_userAgent) {
|
||||
_webView.customUserAgent = _userAgent;
|
||||
}
|
||||
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
|
||||
if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) {
|
||||
_webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
||||
}
|
||||
#endif
|
||||
|
||||
[self addSubview:_webView];
|
||||
[self setHideKeyboardAccessoryView: _savedHideKeyboardAccessoryView];
|
||||
[self visitSource];
|
||||
}
|
||||
}
|
||||
|
||||
// Update webview property when the component prop changes.
|
||||
- (void)setAllowsBackForwardNavigationGestures:(BOOL)allowsBackForwardNavigationGestures {
|
||||
_allowsBackForwardNavigationGestures = allowsBackForwardNavigationGestures;
|
||||
_webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures;
|
||||
}
|
||||
|
||||
|
||||
- (void)removeFromSuperview
|
||||
{
|
||||
if (_webView) {
|
||||
[_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHandlerName];
|
||||
[_webView removeObserver:self forKeyPath:@"estimatedProgress"];
|
||||
[_webView removeFromSuperview];
|
||||
_webView.scrollView.delegate = nil;
|
||||
_webView = nil;
|
||||
}
|
||||
|
||||
[super removeFromSuperview];
|
||||
}
|
||||
|
||||
-(void)keyboardWillHide
|
||||
{
|
||||
keyboardTimer = [NSTimer scheduledTimerWithTimeInterval:0 target:self selector:@selector(keyboardDisplacementFix) userInfo:nil repeats:false];
|
||||
[[NSRunLoop mainRunLoop] addTimer:keyboardTimer forMode:NSRunLoopCommonModes];
|
||||
}
|
||||
-(void)keyboardWillShow
|
||||
{
|
||||
if (keyboardTimer != nil) {
|
||||
[keyboardTimer invalidate];
|
||||
}
|
||||
}
|
||||
-(void)keyboardDisplacementFix
|
||||
{
|
||||
// Additional viewport checks to prevent unintentional scrolls
|
||||
UIScrollView *scrollView = self.webView.scrollView;
|
||||
double maxContentOffset = scrollView.contentSize.height - scrollView.frame.size.height;
|
||||
if (maxContentOffset < 0) {
|
||||
maxContentOffset = 0;
|
||||
}
|
||||
if (scrollView.contentOffset.y > maxContentOffset) {
|
||||
// https://stackoverflow.com/a/9637807/824966
|
||||
[UIView animateWithDuration:.25 animations:^{
|
||||
scrollView.contentOffset = CGPointMake(0, maxContentOffset);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
|
||||
if ([keyPath isEqual:@"estimatedProgress"] && object == self.webView) {
|
||||
if(_onLoadingProgress){
|
||||
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||
[event addEntriesFromDictionary:@{@"progress":[NSNumber numberWithDouble:self.webView.estimatedProgress]}];
|
||||
_onLoadingProgress(event);
|
||||
}
|
||||
}else{
|
||||
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
_savedBackgroundColor = backgroundColor;
|
||||
if (_webView == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor);
|
||||
self.opaque = _webView.opaque = (alpha == 1.0);
|
||||
_webView.scrollView.backgroundColor = backgroundColor;
|
||||
_webView.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called whenever JavaScript running within the web view calls:
|
||||
* - window.webkit.messageHandlers[MessageHandlerName].postMessage
|
||||
*/
|
||||
- (void)userContentController:(WKUserContentController *)userContentController
|
||||
didReceiveScriptMessage:(WKScriptMessage *)message
|
||||
{
|
||||
if (_onMessage != nil) {
|
||||
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||
[event addEntriesFromDictionary: @{@"data": message.body}];
|
||||
_onMessage(event);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSource:(NSDictionary *)source
|
||||
{
|
||||
if (![_source isEqualToDictionary:source]) {
|
||||
_source = [source copy];
|
||||
|
||||
if (_webView != nil) {
|
||||
[self visitSource];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setContentInset:(UIEdgeInsets)contentInset
|
||||
{
|
||||
_contentInset = contentInset;
|
||||
[RCTView autoAdjustInsetsForView:self
|
||||
withScrollView:_webView.scrollView
|
||||
updateOffset:NO];
|
||||
}
|
||||
|
||||
- (void)refreshContentInset
|
||||
{
|
||||
[RCTView autoAdjustInsetsForView:self
|
||||
withScrollView:_webView.scrollView
|
||||
updateOffset:YES];
|
||||
}
|
||||
|
||||
- (void)visitSource
|
||||
{
|
||||
// Check for a static html source first
|
||||
NSString *html = [RCTConvert NSString:_source[@"html"]];
|
||||
if (html) {
|
||||
NSURL *baseURL = [RCTConvert NSURL:_source[@"baseUrl"]];
|
||||
if (!baseURL) {
|
||||
baseURL = [NSURL URLWithString:@"about:blank"];
|
||||
}
|
||||
[_webView loadHTMLString:html baseURL:baseURL];
|
||||
return;
|
||||
}
|
||||
|
||||
NSURLRequest *request = [RCTConvert NSURLRequest:_source];
|
||||
// Because of the way React works, as pages redirect, we actually end up
|
||||
// passing the redirect urls back here, so we ignore them if trying to load
|
||||
// the same url. We'll expose a call to 'reload' to allow a user to load
|
||||
// the existing page.
|
||||
if ([request.URL isEqual:_webView.URL]) {
|
||||
return;
|
||||
}
|
||||
if (!request.URL) {
|
||||
// Clear the webview
|
||||
[_webView loadHTMLString:@"" baseURL:nil];
|
||||
return;
|
||||
}
|
||||
[_webView loadRequest:request];
|
||||
}
|
||||
|
||||
-(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
|
||||
{
|
||||
if (_webView == nil) {
|
||||
_savedHideKeyboardAccessoryView = hideKeyboardAccessoryView;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_savedHideKeyboardAccessoryView == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIView* subview;
|
||||
|
||||
for (UIView* view in _webView.scrollView.subviews) {
|
||||
if([[view.class description] hasPrefix:@"WK"])
|
||||
subview = view;
|
||||
}
|
||||
|
||||
if(subview == nil) return;
|
||||
|
||||
NSString* name = [NSString stringWithFormat:@"%@_SwizzleHelperWK", subview.class.superclass];
|
||||
Class newClass = NSClassFromString(name);
|
||||
|
||||
if(newClass == nil)
|
||||
{
|
||||
newClass = objc_allocateClassPair(subview.class, [name cStringUsingEncoding:NSASCIIStringEncoding], 0);
|
||||
if(!newClass) return;
|
||||
|
||||
Method method = class_getInstanceMethod([_SwizzleHelperWK class], @selector(inputAccessoryView));
|
||||
class_addMethod(newClass, @selector(inputAccessoryView), method_getImplementation(method), method_getTypeEncoding(method));
|
||||
|
||||
objc_registerClassPair(newClass);
|
||||
}
|
||||
|
||||
object_setClass(subview, newClass);
|
||||
}
|
||||
|
||||
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
|
||||
{
|
||||
scrollView.decelerationRate = _decelerationRate;
|
||||
}
|
||||
|
||||
- (void)setScrollEnabled:(BOOL)scrollEnabled
|
||||
{
|
||||
_scrollEnabled = scrollEnabled;
|
||||
_webView.scrollView.scrollEnabled = scrollEnabled;
|
||||
}
|
||||
|
||||
- (void)setShowsHorizontalScrollIndicator:(BOOL)showsHorizontalScrollIndicator
|
||||
{
|
||||
_showsHorizontalScrollIndicator = showsHorizontalScrollIndicator;
|
||||
_webView.scrollView.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator;
|
||||
}
|
||||
|
||||
- (void)setShowsVerticalScrollIndicator:(BOOL)showsVerticalScrollIndicator
|
||||
{
|
||||
_showsVerticalScrollIndicator = showsVerticalScrollIndicator;
|
||||
_webView.scrollView.showsVerticalScrollIndicator = showsVerticalScrollIndicator;
|
||||
}
|
||||
|
||||
- (void)postMessage:(NSString *)message
|
||||
{
|
||||
NSDictionary *eventInitDict = @{@"data": message};
|
||||
NSString *source = [NSString
|
||||
stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
|
||||
RCTJSONStringify(eventInitDict, NULL)
|
||||
];
|
||||
[self injectJavaScript: source];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
// Ensure webview takes the position and dimensions of RNCWKWebView
|
||||
_webView.frame = self.bounds;
|
||||
}
|
||||
|
||||
- (NSMutableDictionary<NSString *, id> *)baseEvent
|
||||
{
|
||||
NSDictionary *event = @{
|
||||
@"url": _webView.URL.absoluteString ?: @"",
|
||||
@"title": _webView.title,
|
||||
@"loading" : @(_webView.loading),
|
||||
@"canGoBack": @(_webView.canGoBack),
|
||||
@"canGoForward" : @(_webView.canGoForward)
|
||||
};
|
||||
return [[NSMutableDictionary alloc] initWithDictionary: event];
|
||||
}
|
||||
|
||||
+ (void)setClientAuthenticationCredential:(nullable NSURLCredential*)credential {
|
||||
clientAuthenticationCredential = credential;
|
||||
}
|
||||
|
||||
#pragma mark - WKNavigationDelegate methods
|
||||
|
||||
/**
|
||||
* alert
|
||||
*/
|
||||
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
|
||||
{
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
completionHandler();
|
||||
}]];
|
||||
[[self topViewController] presentViewController:alert animated:YES completion:NULL];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* confirm
|
||||
*/
|
||||
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
completionHandler(YES);
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
|
||||
completionHandler(NO);
|
||||
}]];
|
||||
[[self topViewController] presentViewController:alert animated:YES completion:NULL];
|
||||
}
|
||||
|
||||
/**
|
||||
* prompt
|
||||
*/
|
||||
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler{
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.textColor = [UIColor lightGrayColor];
|
||||
textField.placeholder = defaultText;
|
||||
}];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
completionHandler([[alert.textFields lastObject] text]);
|
||||
}]];
|
||||
[[self topViewController] presentViewController:alert animated:YES completion:NULL];
|
||||
}
|
||||
|
||||
/**
|
||||
* topViewController
|
||||
*/
|
||||
-(UIViewController *)topViewController{
|
||||
UIViewController *controller = [self topViewControllerWithRootViewController:[self getCurrentWindow].rootViewController];
|
||||
return controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* topViewControllerWithRootViewController
|
||||
*/
|
||||
-(UIViewController *)topViewControllerWithRootViewController:(UIViewController *)viewController{
|
||||
if (viewController==nil) return nil;
|
||||
if (viewController.presentedViewController!=nil) {
|
||||
return [self topViewControllerWithRootViewController:viewController.presentedViewController];
|
||||
} else if ([viewController isKindOfClass:[UITabBarController class]]){
|
||||
return [self topViewControllerWithRootViewController:[(UITabBarController *)viewController selectedViewController]];
|
||||
} else if ([viewController isKindOfClass:[UINavigationController class]]){
|
||||
return [self topViewControllerWithRootViewController:[(UINavigationController *)viewController visibleViewController]];
|
||||
} else {
|
||||
return viewController;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* getCurrentWindow
|
||||
*/
|
||||
-(UIWindow *)getCurrentWindow{
|
||||
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
||||
if (window.windowLevel!=UIWindowLevelNormal) {
|
||||
for (UIWindow *wid in [UIApplication sharedApplication].windows) {
|
||||
if (window.windowLevel==UIWindowLevelNormal) {
|
||||
window = wid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decides whether to allow or cancel a navigation.
|
||||
* @see https://fburl.com/42r9fxob
|
||||
*/
|
||||
- (void) webView:(WKWebView *)webView
|
||||
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
|
||||
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
|
||||
{
|
||||
static NSDictionary<NSNumber *, NSString *> *navigationTypes;
|
||||
static dispatch_once_t onceToken;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
navigationTypes = @{
|
||||
@(WKNavigationTypeLinkActivated): @"click",
|
||||
@(WKNavigationTypeFormSubmitted): @"formsubmit",
|
||||
@(WKNavigationTypeBackForward): @"backforward",
|
||||
@(WKNavigationTypeReload): @"reload",
|
||||
@(WKNavigationTypeFormResubmitted): @"formresubmit",
|
||||
@(WKNavigationTypeOther): @"other",
|
||||
};
|
||||
});
|
||||
|
||||
WKNavigationType navigationType = navigationAction.navigationType;
|
||||
NSURLRequest *request = navigationAction.request;
|
||||
|
||||
if (_onShouldStartLoadWithRequest) {
|
||||
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||
[event addEntriesFromDictionary: @{
|
||||
@"url": (request.URL).absoluteString,
|
||||
@"navigationType": navigationTypes[@(navigationType)]
|
||||
}];
|
||||
if (![self.delegate webView:self
|
||||
shouldStartLoadForRequest:event
|
||||
withCallback:_onShouldStartLoadWithRequest]) {
|
||||
decisionHandler(WKNavigationResponsePolicyCancel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_onLoadingStart) {
|
||||
// We have this check to filter out iframe requests and whatnot
|
||||
BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];
|
||||
if (isTopFrame) {
|
||||
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||
[event addEntriesFromDictionary: @{
|
||||
@"url": (request.URL).absoluteString,
|
||||
@"navigationType": navigationTypes[@(navigationType)]
|
||||
}];
|
||||
_onLoadingStart(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow all navigation by default
|
||||
decisionHandler(WKNavigationResponsePolicyAllow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an error occurs while the web view is loading content.
|
||||
* @see https://fburl.com/km6vqenw
|
||||
*/
|
||||
- (void) webView:(WKWebView *)webView
|
||||
didFailProvisionalNavigation:(WKNavigation *)navigation
|
||||
withError:(NSError *)error
|
||||
{
|
||||
if (_onLoadingError) {
|
||||
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
|
||||
// NSURLErrorCancelled is reported when a page has a redirect OR if you load
|
||||
// a new URL in the WebView before the previous one came back. We can just
|
||||
// ignore these since they aren't real errors.
|
||||
// http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os
|
||||
return;
|
||||
}
|
||||
|
||||
if ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102) {
|
||||
// Error code 102 "Frame load interrupted" is raised by the WKWebView
|
||||
// when the URL is from an http redirect. This is a common pattern when
|
||||
// implementing OAuth with a WebView.
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||
[event addEntriesFromDictionary:@{
|
||||
@"didFailProvisionalNavigation": @YES,
|
||||
@"domain": error.domain,
|
||||
@"code": @(error.code),
|
||||
@"description": error.localizedDescription,
|
||||
}];
|
||||
_onLoadingError(event);
|
||||
}
|
||||
|
||||
[self setBackgroundColor: _savedBackgroundColor];
|
||||
}
|
||||
|
||||
- (void)evaluateJS:(NSString *)js
|
||||
thenCall: (void (^)(NSString*)) callback
|
||||
{
|
||||
[self.webView evaluateJavaScript: js completionHandler: ^(id result, NSError *error) {
|
||||
if (error == nil) {
|
||||
if (callback != nil) {
|
||||
callback([NSString stringWithFormat:@"%@", result]);
|
||||
}
|
||||
} else {
|
||||
RCTLogError(@"Error evaluating injectedJavaScript: This is possibly due to an unsupported return type. Try adding true to the end of your injectedJavaScript string.");
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
-(void) webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
|
||||
NSString *authenticationMethod = [[challenge protectionSpace] authenticationMethod];
|
||||
|
||||
if (authenticationMethod == NSURLAuthenticationMethodNTLM || authenticationMethod == NSURLAuthenticationMethodNegotiate) {
|
||||
NSString *hostName = webView.URL.host;
|
||||
NSString *title = @"Authentication Challenge";
|
||||
NSString *message = [NSString stringWithFormat:@"%@ requires user name and password", hostName];
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.placeholder = @"User";
|
||||
}];
|
||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.placeholder = @"Password";
|
||||
textField.secureTextEntry = YES;
|
||||
}];
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
|
||||
NSString *userName = ((UITextField *)alertController.textFields[0]).text;
|
||||
NSString *password = ((UITextField *)alertController.textFields[1]).text;
|
||||
|
||||
NSURLCredential *credential = [[NSURLCredential alloc] initWithUser:userName password:password persistence:NSURLCredentialPersistenceNone];
|
||||
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
|
||||
|
||||
}]];
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
|
||||
}]];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIViewController *rootVC = UIApplication.sharedApplication.delegate.window.rootViewController;
|
||||
|
||||
while (rootVC.presentedViewController != nil) {
|
||||
rootVC = rootVC.presentedViewController;
|
||||
}
|
||||
[rootVC presentViewController:alertController animated:YES completion:^{}];
|
||||
});
|
||||
} else if (!clientAuthenticationCredential) {
|
||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
||||
return;
|
||||
} else if (authenticationMethod == NSURLAuthenticationMethodClientCertificate) {
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential, clientAuthenticationCredential);
|
||||
} else {
|
||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the navigation is complete.
|
||||
* @see https://fburl.com/rtys6jlb
|
||||
*/
|
||||
- (void) webView:(WKWebView *)webView
|
||||
didFinishNavigation:(WKNavigation *)navigation
|
||||
{
|
||||
if (_injectedJavaScript) {
|
||||
[self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
|
||||
NSMutableDictionary *event = [self baseEvent];
|
||||
event[@"jsEvaluationValue"] = jsEvaluationValue;
|
||||
|
||||
if (self.onLoadingFinish) {
|
||||
self.onLoadingFinish(event);
|
||||
}
|
||||
}];
|
||||
} else if (_onLoadingFinish) {
|
||||
_onLoadingFinish([self baseEvent]);
|
||||
}
|
||||
|
||||
[self setBackgroundColor: _savedBackgroundColor];
|
||||
}
|
||||
|
||||
- (void)injectJavaScript:(NSString *)script
|
||||
{
|
||||
[self evaluateJS: script thenCall: nil];
|
||||
}
|
||||
|
||||
- (void)goForward
|
||||
{
|
||||
[_webView goForward];
|
||||
}
|
||||
|
||||
- (void)goBack
|
||||
{
|
||||
[_webView goBack];
|
||||
}
|
||||
|
||||
- (void)reload
|
||||
{
|
||||
/**
|
||||
* When the initial load fails due to network connectivity issues,
|
||||
* [_webView reload] doesn't reload the webpage. Therefore, we must
|
||||
* manually call [_webView loadRequest:request].
|
||||
*/
|
||||
NSURLRequest *request = [RCTConvert NSURLRequest:self.source];
|
||||
if (request.URL && !_webView.URL.absoluteString.length) {
|
||||
[_webView loadRequest:request];
|
||||
}
|
||||
else {
|
||||
[_webView reload];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopLoading
|
||||
{
|
||||
[_webView stopLoading];
|
||||
}
|
||||
|
||||
- (void)setBounces:(BOOL)bounces
|
||||
{
|
||||
_bounces = bounces;
|
||||
_webView.scrollView.bounces = bounces;
|
||||
}
|
||||
@end
|
||||
877
native_modules/RNCWebViewManager.java
Normal file
877
native_modules/RNCWebViewManager.java
Normal file
@@ -0,0 +1,877 @@
|
||||
package com.reactnativecommunity.webview;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import android.text.InputType;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.webkit.ConsoleMessage;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.DownloadListener;
|
||||
import android.webkit.GeolocationPermissions;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.URLUtil;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.webkit.HttpAuthHandler;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.LifecycleEventListener;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.build.ReactBuildConfig;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.facebook.react.uimanager.SimpleViewManager;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
|
||||
import com.facebook.react.uimanager.events.Event;
|
||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
|
||||
import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
|
||||
import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
|
||||
import com.reactnativecommunity.webview.events.TopMessageEvent;
|
||||
import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
|
||||
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Manages instances of {@link WebView}
|
||||
*
|
||||
* Can accept following commands:
|
||||
* - GO_BACK
|
||||
* - GO_FORWARD
|
||||
* - RELOAD
|
||||
* - LOAD_URL
|
||||
*
|
||||
* {@link WebView} instances could emit following direct events:
|
||||
* - topLoadingFinish
|
||||
* - topLoadingStart
|
||||
* - topLoadingStart
|
||||
* - topLoadingProgress
|
||||
* - topShouldStartLoadWithRequest
|
||||
*
|
||||
* Each event will carry the following properties:
|
||||
* - target - view's react tag
|
||||
* - url - url set for the webview
|
||||
* - loading - whether webview is in a loading state
|
||||
* - title - title of the current page
|
||||
* - canGoBack - boolean, whether there is anything on a history stack to go back
|
||||
* - canGoForward - boolean, whether it is possible to request GO_FORWARD command
|
||||
*/
|
||||
@ReactModule(name = RNCWebViewManager.REACT_CLASS)
|
||||
public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
||||
|
||||
protected static final String REACT_CLASS = "RNCWebView";
|
||||
private RNCWebViewPackage aPackage;
|
||||
|
||||
protected static final String HTML_ENCODING = "UTF-8";
|
||||
protected static final String HTML_MIME_TYPE = "text/html";
|
||||
protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView";
|
||||
|
||||
protected static final String HTTP_METHOD_POST = "POST";
|
||||
|
||||
public static final int COMMAND_GO_BACK = 1;
|
||||
public static final int COMMAND_GO_FORWARD = 2;
|
||||
public static final int COMMAND_RELOAD = 3;
|
||||
public static final int COMMAND_STOP_LOADING = 4;
|
||||
public static final int COMMAND_POST_MESSAGE = 5;
|
||||
public static final int COMMAND_INJECT_JAVASCRIPT = 6;
|
||||
public static final int COMMAND_LOAD_URL = 7;
|
||||
|
||||
// Use `webView.loadUrl("about:blank")` to reliably reset the view
|
||||
// state and release page resources (including any running JavaScript).
|
||||
protected static final String BLANK_URL = "about:blank";
|
||||
|
||||
protected WebViewConfig mWebViewConfig;
|
||||
|
||||
protected static class RNCWebViewClient extends WebViewClient {
|
||||
|
||||
protected boolean mLastLoadFailed = false;
|
||||
protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
|
||||
protected Activity mCurrentActivity;
|
||||
|
||||
public void setCurrentActivity(Activity mCurrentActivity) {
|
||||
this.mCurrentActivity = mCurrentActivity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView webView, String url) {
|
||||
super.onPageFinished(webView, url);
|
||||
|
||||
if (!mLastLoadFailed) {
|
||||
RNCWebView reactWebView = (RNCWebView) webView;
|
||||
|
||||
reactWebView.callInjectedJavaScript();
|
||||
|
||||
emitFinishEvent(webView, url);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted(WebView webView, String url, Bitmap favicon) {
|
||||
super.onPageStarted(webView, url, favicon);
|
||||
mLastLoadFailed = false;
|
||||
|
||||
dispatchEvent(
|
||||
webView,
|
||||
new TopLoadingStartEvent(
|
||||
webView.getId(),
|
||||
createWebViewEvent(webView, url)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
dispatchEvent(view, new TopShouldStartLoadWithRequestEvent(view.getId(), url));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
|
||||
dispatchEvent(view, new TopShouldStartLoadWithRequestEvent(view.getId(), request.getUrl().toString()));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(
|
||||
WebView webView,
|
||||
int errorCode,
|
||||
String description,
|
||||
String failingUrl) {
|
||||
super.onReceivedError(webView, errorCode, description, failingUrl);
|
||||
mLastLoadFailed = true;
|
||||
|
||||
// In case of an error JS side expect to get a finish event first, and then get an error event
|
||||
// Android WebView does it in the opposite way, so we need to simulate that behavior
|
||||
emitFinishEvent(webView, failingUrl);
|
||||
|
||||
WritableMap eventData = createWebViewEvent(webView, failingUrl);
|
||||
eventData.putDouble("code", errorCode);
|
||||
eventData.putString("description", description);
|
||||
|
||||
dispatchEvent(
|
||||
webView,
|
||||
new TopLoadingErrorEvent(webView.getId(), eventData));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedHttpAuthRequest(WebView view,
|
||||
final HttpAuthHandler handler, String host, String realm)
|
||||
{
|
||||
Log.d("ReactNative", "host = " + host + " realm = " + realm);
|
||||
if (this.mCurrentActivity != null) {
|
||||
final EditText usernameInput = new EditText(this.mCurrentActivity);
|
||||
usernameInput.setHint("Username");
|
||||
|
||||
final EditText passwordInput = new EditText(this.mCurrentActivity);
|
||||
passwordInput.setHint("Password");
|
||||
passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
|
||||
LinearLayout layout = new LinearLayout(this.mCurrentActivity);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
layout.addView(usernameInput);
|
||||
layout.addView(passwordInput);
|
||||
|
||||
AlertDialog.Builder authDialog = new AlertDialog.Builder(this.mCurrentActivity)
|
||||
.setTitle("Authentication Challenge")
|
||||
.setMessage(host + " requires user name and password ")
|
||||
.setView(layout)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
handler.proceed(usernameInput.getText().toString(), passwordInput.getText().toString());
|
||||
dialogInterface.dismiss();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
dialogInterface.dismiss();
|
||||
handler.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
if (view != null) {
|
||||
authDialog.show();
|
||||
}
|
||||
} else {
|
||||
handler.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
protected void emitFinishEvent(WebView webView, String url) {
|
||||
dispatchEvent(
|
||||
webView,
|
||||
new TopLoadingFinishEvent(
|
||||
webView.getId(),
|
||||
createWebViewEvent(webView, url)));
|
||||
}
|
||||
|
||||
protected WritableMap createWebViewEvent(WebView webView, String url) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble("target", webView.getId());
|
||||
// Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks
|
||||
// like onPageFinished
|
||||
event.putString("url", url);
|
||||
event.putBoolean("loading", !mLastLoadFailed && webView.getProgress() != 100);
|
||||
event.putString("title", webView.getTitle());
|
||||
event.putBoolean("canGoBack", webView.canGoBack());
|
||||
event.putBoolean("canGoForward", webView.canGoForward());
|
||||
return event;
|
||||
}
|
||||
|
||||
public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
|
||||
mUrlPrefixesForDefaultIntent = specialUrls;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclass of {@link WebView} that implements {@link LifecycleEventListener} interface in order
|
||||
* to call {@link WebView#destroy} on activity destroy event and also to clear the client
|
||||
*/
|
||||
protected static class RNCWebView extends WebView implements LifecycleEventListener {
|
||||
protected @Nullable String injectedJS;
|
||||
protected boolean messagingEnabled = false;
|
||||
protected @Nullable RNCWebViewClient mRNCWebViewClient;
|
||||
protected boolean sendContentSizeChangeEvents = false;
|
||||
protected ReactContext reactContext;
|
||||
|
||||
public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) {
|
||||
this.sendContentSizeChangeEvents = sendContentSizeChangeEvents;
|
||||
}
|
||||
|
||||
|
||||
protected class RNCWebViewBridge {
|
||||
RNCWebView mContext;
|
||||
|
||||
RNCWebViewBridge(RNCWebView c) {
|
||||
mContext = c;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called whenever JavaScript running within the web view calls:
|
||||
* - window[JAVASCRIPT_INTERFACE].postMessage
|
||||
*/
|
||||
@JavascriptInterface
|
||||
public void postMessage(String message) {
|
||||
mContext.onMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebView must be created with an context of the current activity
|
||||
*
|
||||
* Activity Context is required for creation of dialogs internally by WebView
|
||||
* Reactive Native needed for access to ReactNative internal system functionality
|
||||
*
|
||||
*/
|
||||
public RNCWebView(ThemedReactContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostResume() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostPause() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostDestroy() {
|
||||
cleanupCallbacksAndDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int ow, int oh) {
|
||||
super.onSizeChanged(w, h, ow, oh);
|
||||
|
||||
if (sendContentSizeChangeEvents) {
|
||||
dispatchEvent(
|
||||
this,
|
||||
new ContentSizeChangeEvent(
|
||||
this.getId(),
|
||||
w,
|
||||
h
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWebViewClient(WebViewClient client) {
|
||||
super.setWebViewClient(client);
|
||||
Log.d("ReactNative", "SETTING THE WEBVIEW CLIENT");
|
||||
mRNCWebViewClient = (RNCWebViewClient)client;
|
||||
if (this.reactContext != null && this.reactContext.getCurrentActivity() != null && mRNCWebViewClient != null) {
|
||||
mRNCWebViewClient.setCurrentActivity(this.reactContext.getCurrentActivity());
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable RNCWebViewClient getRNCWebViewClient() {
|
||||
return mRNCWebViewClient;
|
||||
}
|
||||
|
||||
public void setInjectedJavaScript(@Nullable String js) {
|
||||
injectedJS = js;
|
||||
}
|
||||
|
||||
protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) {
|
||||
return new RNCWebViewBridge(webView);
|
||||
}
|
||||
|
||||
@SuppressLint("AddJavascriptInterface")
|
||||
public void setMessagingEnabled(boolean enabled) {
|
||||
if (messagingEnabled == enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
messagingEnabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
|
||||
} else {
|
||||
removeJavascriptInterface(JAVASCRIPT_INTERFACE);
|
||||
}
|
||||
}
|
||||
|
||||
protected void evaluateJavascriptWithFallback(String script) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
evaluateJavascript(script, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loadUrl("javascript:" + URLEncoder.encode(script, "UTF-8"));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// UTF-8 should always be supported
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void callInjectedJavaScript() {
|
||||
if (getSettings().getJavaScriptEnabled() &&
|
||||
injectedJS != null &&
|
||||
!TextUtils.isEmpty(injectedJS)) {
|
||||
evaluateJavascriptWithFallback("(function() {\n" + injectedJS + ";\n})();");
|
||||
}
|
||||
}
|
||||
|
||||
public void onMessage(String message) {
|
||||
dispatchEvent(this, new TopMessageEvent(this.getId(), message));
|
||||
}
|
||||
|
||||
protected void cleanupCallbacksAndDestroy() {
|
||||
setWebViewClient(null);
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public RNCWebViewManager() {
|
||||
mWebViewConfig = new WebViewConfig() {
|
||||
public void configWebView(WebView webView) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public RNCWebViewManager(WebViewConfig webViewConfig) {
|
||||
mWebViewConfig = webViewConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return REACT_CLASS;
|
||||
}
|
||||
|
||||
protected RNCWebView createRNCWebViewInstance(ThemedReactContext reactContext) {
|
||||
return new RNCWebView(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
protected WebView createViewInstance(ThemedReactContext reactContext) {
|
||||
RNCWebView webView = createRNCWebViewInstance(reactContext);
|
||||
webView.setWebChromeClient(new WebChromeClient() {
|
||||
@Override
|
||||
public boolean onConsoleMessage(ConsoleMessage message) {
|
||||
if (ReactBuildConfig.DEBUG) {
|
||||
return super.onConsoleMessage(message);
|
||||
}
|
||||
// Ignore console logs in non debug builds.
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(WebView webView, int newProgress) {
|
||||
super.onProgressChanged(webView, newProgress);
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble("target", webView.getId());
|
||||
event.putString("title", webView.getTitle());
|
||||
event.putBoolean("canGoBack", webView.canGoBack());
|
||||
event.putBoolean("canGoForward", webView.canGoForward());
|
||||
event.putDouble("progress", (float)newProgress/100);
|
||||
dispatchEvent(
|
||||
webView,
|
||||
new TopLoadingProgressEvent(
|
||||
webView.getId(),
|
||||
event));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
|
||||
callback.invoke(origin, true, false);
|
||||
}
|
||||
|
||||
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType) {
|
||||
getModule().startPhotoPickerIntent(filePathCallback, acceptType);
|
||||
}
|
||||
protected void openFileChooser(ValueCallback<Uri> filePathCallback) {
|
||||
getModule().startPhotoPickerIntent(filePathCallback, "");
|
||||
}
|
||||
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType, String capture) {
|
||||
getModule().startPhotoPickerIntent(filePathCallback, acceptType);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
|
||||
String[] acceptTypes = fileChooserParams.getAcceptTypes();
|
||||
boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE;
|
||||
Intent intent = fileChooserParams.createIntent();
|
||||
return getModule().startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple);
|
||||
}
|
||||
});
|
||||
reactContext.addLifecycleEventListener(webView);
|
||||
mWebViewConfig.configWebView(webView);
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setBuiltInZoomControls(true);
|
||||
settings.setDisplayZoomControls(false);
|
||||
settings.setDomStorageEnabled(true);
|
||||
|
||||
settings.setAllowFileAccess(false);
|
||||
settings.setAllowContentAccess(false);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
settings.setAllowFileAccessFromFileURLs(false);
|
||||
setAllowUniversalAccessFromFileURLs(webView, false);
|
||||
}
|
||||
setMixedContentMode(webView, "never");
|
||||
|
||||
// Fixes broken full-screen modals/galleries due to body height being 0.
|
||||
webView.setLayoutParams(
|
||||
new LayoutParams(LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT));
|
||||
|
||||
setGeolocationEnabled(webView, false);
|
||||
if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
|
||||
webView.setDownloadListener(new DownloadListener() {
|
||||
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
|
||||
RNCWebViewModule module = getModule();
|
||||
|
||||
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
|
||||
|
||||
String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype);
|
||||
String downloadMessage = "Downloading " + fileName;
|
||||
|
||||
//Attempt to add cookie, if it exists
|
||||
URL urlObj = null;
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost();
|
||||
String cookie = CookieManager.getInstance().getCookie(baseUrl);
|
||||
request.addRequestHeader("Cookie", cookie);
|
||||
System.out.println("Got cookie for DownloadManager: " + cookie);
|
||||
} catch (MalformedURLException e) {
|
||||
System.out.println("Error getting cookie for DownloadManager: " + e.toString());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
//Finish setting up request
|
||||
request.addRequestHeader("User-Agent", userAgent);
|
||||
request.setTitle(fileName);
|
||||
request.setDescription(downloadMessage);
|
||||
request.allowScanningByMediaScanner();
|
||||
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
|
||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
|
||||
|
||||
module.setDownloadRequest(request);
|
||||
|
||||
if (module.grantFileDownloaderPermissions()) {
|
||||
module.downloadFile();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return webView;
|
||||
}
|
||||
|
||||
@ReactProp(name = "javaScriptEnabled")
|
||||
public void setJavaScriptEnabled(WebView view, boolean enabled) {
|
||||
view.getSettings().setJavaScriptEnabled(enabled);
|
||||
}
|
||||
|
||||
@ReactProp(name = "showsHorizontalScrollIndicator")
|
||||
public void setShowsHorizontalScrollIndicator(WebView view, boolean enabled) {
|
||||
view.setHorizontalScrollBarEnabled(enabled);
|
||||
}
|
||||
|
||||
@ReactProp(name = "showsVerticalScrollIndicator")
|
||||
public void setShowsVerticalScrollIndicator(WebView view, boolean enabled) {
|
||||
view.setVerticalScrollBarEnabled(enabled);
|
||||
}
|
||||
|
||||
@ReactProp(name = "cacheEnabled")
|
||||
public void setCacheEnabled(WebView view, boolean enabled) {
|
||||
if (enabled) {
|
||||
Context ctx = view.getContext();
|
||||
if (ctx != null) {
|
||||
view.getSettings().setAppCachePath(ctx.getCacheDir().getAbsolutePath());
|
||||
view.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
|
||||
view.getSettings().setAppCacheEnabled(true);
|
||||
}
|
||||
} else {
|
||||
view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
|
||||
view.getSettings().setAppCacheEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = "androidHardwareAccelerationDisabled")
|
||||
public void setHardwareAccelerationDisabled(WebView view, boolean disabled) {
|
||||
if (disabled) {
|
||||
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = "overScrollMode")
|
||||
public void setOverScrollMode(WebView view, String overScrollModeString) {
|
||||
Integer overScrollMode;
|
||||
switch (overScrollModeString) {
|
||||
case "never":
|
||||
overScrollMode = View.OVER_SCROLL_NEVER;
|
||||
break;
|
||||
case "content":
|
||||
overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS;
|
||||
break;
|
||||
case "always":
|
||||
default:
|
||||
overScrollMode = View.OVER_SCROLL_ALWAYS;
|
||||
break;
|
||||
}
|
||||
view.setOverScrollMode(overScrollMode);
|
||||
}
|
||||
|
||||
@ReactProp(name = "thirdPartyCookiesEnabled")
|
||||
public void setThirdPartyCookiesEnabled(WebView view, boolean enabled) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
CookieManager.getInstance().setAcceptThirdPartyCookies(view, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = "scalesPageToFit")
|
||||
public void setScalesPageToFit(WebView view, boolean enabled) {
|
||||
view.getSettings().setUseWideViewPort(!enabled);
|
||||
}
|
||||
|
||||
@ReactProp(name = "domStorageEnabled")
|
||||
public void setDomStorageEnabled(WebView view, boolean enabled) {
|
||||
view.getSettings().setDomStorageEnabled(enabled);
|
||||
}
|
||||
|
||||
@ReactProp(name = "userAgent")
|
||||
public void setUserAgent(WebView view, @Nullable String userAgent) {
|
||||
if (userAgent != null) {
|
||||
// TODO(8496850): Fix incorrect behavior when property is unset (uA == null)
|
||||
view.getSettings().setUserAgentString(userAgent);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
@ReactProp(name = "mediaPlaybackRequiresUserAction")
|
||||
public void setMediaPlaybackRequiresUserAction(WebView view, boolean requires) {
|
||||
view.getSettings().setMediaPlaybackRequiresUserGesture(requires);
|
||||
}
|
||||
|
||||
@ReactProp(name = "allowUniversalAccessFromFileURLs")
|
||||
public void setAllowUniversalAccessFromFileURLs(WebView view, boolean allow) {
|
||||
view.getSettings().setAllowUniversalAccessFromFileURLs(allow);
|
||||
}
|
||||
|
||||
@ReactProp(name = "saveFormDataDisabled")
|
||||
public void setSaveFormDataDisabled(WebView view, boolean disable) {
|
||||
view.getSettings().setSaveFormData(!disable);
|
||||
}
|
||||
|
||||
@ReactProp(name = "injectedJavaScript")
|
||||
public void setInjectedJavaScript(WebView view, @Nullable String injectedJavaScript) {
|
||||
((RNCWebView) view).setInjectedJavaScript(injectedJavaScript);
|
||||
}
|
||||
|
||||
@ReactProp(name = "messagingEnabled")
|
||||
public void setMessagingEnabled(WebView view, boolean enabled) {
|
||||
((RNCWebView) view).setMessagingEnabled(enabled);
|
||||
}
|
||||
|
||||
@ReactProp(name = "source")
|
||||
public void setSource(WebView view, @Nullable ReadableMap source) {
|
||||
if (source != null) {
|
||||
if (source.hasKey("html")) {
|
||||
String html = source.getString("html");
|
||||
if (source.hasKey("baseUrl")) {
|
||||
view.loadDataWithBaseURL(
|
||||
source.getString("baseUrl"), html, HTML_MIME_TYPE, HTML_ENCODING, null);
|
||||
} else {
|
||||
view.loadData(html, HTML_MIME_TYPE + "; charset=" + HTML_ENCODING, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (source.hasKey("uri")) {
|
||||
String url = source.getString("uri");
|
||||
String previousUrl = view.getUrl();
|
||||
if (previousUrl != null && previousUrl.equals(url)) {
|
||||
return;
|
||||
}
|
||||
if (source.hasKey("method")) {
|
||||
String method = source.getString("method");
|
||||
if (method.equalsIgnoreCase(HTTP_METHOD_POST)) {
|
||||
byte[] postData = null;
|
||||
if (source.hasKey("body")) {
|
||||
String body = source.getString("body");
|
||||
try {
|
||||
postData = body.getBytes("UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
postData = body.getBytes();
|
||||
}
|
||||
}
|
||||
if (postData == null) {
|
||||
postData = new byte[0];
|
||||
}
|
||||
view.postUrl(url, postData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
HashMap<String, String> headerMap = new HashMap<>();
|
||||
if (source.hasKey("headers")) {
|
||||
ReadableMap headers = source.getMap("headers");
|
||||
ReadableMapKeySetIterator iter = headers.keySetIterator();
|
||||
while (iter.hasNextKey()) {
|
||||
String key = iter.nextKey();
|
||||
if ("user-agent".equals(key.toLowerCase(Locale.ENGLISH))) {
|
||||
if (view.getSettings() != null) {
|
||||
view.getSettings().setUserAgentString(headers.getString(key));
|
||||
}
|
||||
} else {
|
||||
headerMap.put(key, headers.getString(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
view.loadUrl(url, headerMap);
|
||||
return;
|
||||
}
|
||||
}
|
||||
view.loadUrl(BLANK_URL);
|
||||
}
|
||||
|
||||
@ReactProp(name = "onContentSizeChange")
|
||||
public void setOnContentSizeChange(WebView view, boolean sendContentSizeChangeEvents) {
|
||||
((RNCWebView) view).setSendContentSizeChangeEvents(sendContentSizeChangeEvents);
|
||||
}
|
||||
|
||||
@ReactProp(name = "mixedContentMode")
|
||||
public void setMixedContentMode(WebView view, @Nullable String mixedContentMode) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (mixedContentMode == null || "never".equals(mixedContentMode)) {
|
||||
view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
|
||||
} else if ("always".equals(mixedContentMode)) {
|
||||
view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
||||
} else if ("compatibility".equals(mixedContentMode)) {
|
||||
view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = "urlPrefixesForDefaultIntent")
|
||||
public void setUrlPrefixesForDefaultIntent(
|
||||
WebView view,
|
||||
@Nullable ReadableArray urlPrefixesForDefaultIntent) {
|
||||
RNCWebViewClient client = ((RNCWebView) view).getRNCWebViewClient();
|
||||
if (client != null && urlPrefixesForDefaultIntent != null) {
|
||||
client.setUrlPrefixesForDefaultIntent(urlPrefixesForDefaultIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = "allowFileAccess")
|
||||
public void setAllowFileAccess(
|
||||
WebView view,
|
||||
@Nullable Boolean allowFileAccess) {
|
||||
view.getSettings().setAllowFileAccess(allowFileAccess != null && allowFileAccess);
|
||||
}
|
||||
|
||||
@ReactProp(name = "geolocationEnabled")
|
||||
public void setGeolocationEnabled(
|
||||
WebView view,
|
||||
@Nullable Boolean isGeolocationEnabled) {
|
||||
view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
|
||||
// Do not register default touch emitter and let WebView implementation handle touches
|
||||
view.setWebViewClient(new RNCWebViewClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map getExportedCustomDirectEventTypeConstants() {
|
||||
Map export = super.getExportedCustomDirectEventTypeConstants();
|
||||
if (export == null) {
|
||||
export = MapBuilder.newHashMap();
|
||||
}
|
||||
export.put(TopLoadingProgressEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingProgress"));
|
||||
export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest"));
|
||||
return export;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Map<String, Integer> getCommandsMap() {
|
||||
return MapBuilder.of(
|
||||
"goBack", COMMAND_GO_BACK,
|
||||
"goForward", COMMAND_GO_FORWARD,
|
||||
"reload", COMMAND_RELOAD,
|
||||
"stopLoading", COMMAND_STOP_LOADING,
|
||||
"postMessage", COMMAND_POST_MESSAGE,
|
||||
"injectJavaScript", COMMAND_INJECT_JAVASCRIPT,
|
||||
"loadUrl", COMMAND_LOAD_URL
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray args) {
|
||||
switch (commandId) {
|
||||
case COMMAND_GO_BACK:
|
||||
root.goBack();
|
||||
break;
|
||||
case COMMAND_GO_FORWARD:
|
||||
root.goForward();
|
||||
break;
|
||||
case COMMAND_RELOAD:
|
||||
root.reload();
|
||||
break;
|
||||
case COMMAND_STOP_LOADING:
|
||||
root.stopLoading();
|
||||
break;
|
||||
case COMMAND_POST_MESSAGE:
|
||||
try {
|
||||
RNCWebView reactWebView = (RNCWebView) root;
|
||||
JSONObject eventInitDict = new JSONObject();
|
||||
eventInitDict.put("data", args.getString(0));
|
||||
reactWebView.evaluateJavascriptWithFallback("(function () {" +
|
||||
"var event;" +
|
||||
"var data = " + eventInitDict.toString() + ";" +
|
||||
"try {" +
|
||||
"event = new MessageEvent('message', data);" +
|
||||
"} catch (e) {" +
|
||||
"event = document.createEvent('MessageEvent');" +
|
||||
"event.initMessageEvent('message', true, true, data.data, data.origin, data.lastEventId, data.source);" +
|
||||
"}" +
|
||||
"document.dispatchEvent(event);" +
|
||||
"})();");
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
break;
|
||||
case COMMAND_INJECT_JAVASCRIPT:
|
||||
RNCWebView reactWebView = (RNCWebView) root;
|
||||
reactWebView.evaluateJavascriptWithFallback(args.getString(0));
|
||||
break;
|
||||
case COMMAND_LOAD_URL:
|
||||
if (args == null) {
|
||||
throw new RuntimeException("Arguments for loading an url are null!");
|
||||
}
|
||||
root.loadUrl(args.getString(0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDropViewInstance(WebView webView) {
|
||||
super.onDropViewInstance(webView);
|
||||
((ThemedReactContext) webView.getContext()).removeLifecycleEventListener((RNCWebView) webView);
|
||||
((RNCWebView) webView).cleanupCallbacksAndDestroy();
|
||||
}
|
||||
|
||||
protected static void dispatchEvent(WebView webView, Event event) {
|
||||
ReactContext reactContext = (ReactContext) webView.getContext();
|
||||
EventDispatcher eventDispatcher =
|
||||
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
|
||||
eventDispatcher.dispatchEvent(event);
|
||||
}
|
||||
|
||||
public RNCWebViewPackage getPackage() {
|
||||
return this.aPackage;
|
||||
}
|
||||
|
||||
public void setPackage(RNCWebViewPackage aPackage) {
|
||||
this.aPackage = aPackage;
|
||||
}
|
||||
|
||||
public RNCWebViewModule getModule() {
|
||||
return this.aPackage.getModule();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user