Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions apple/RNCWebView.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@ typedef enum RNCWebViewPermissionGrantType : NSUInteger {

@class RNCWebView;

@protocol RNCWebViewDelegate <NSObject>

- (BOOL)webView:(RNCWebView *_Nonnull)webView
shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *_Nonnull)request
withCallback:(RCTDirectEventBlock _Nonnull)callback;

@end

@interface RNCWeakScriptMessageDelegate : NSObject<WKScriptMessageHandler>

@property (nonatomic, weak, nullable) id<WKScriptMessageHandler> scriptDelegate;
Expand All @@ -37,7 +29,6 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *_Nonnull)request

@interface RNCWebView : RCTView

@property (nonatomic, weak) id<RNCWebViewDelegate> _Nullable delegate;
@property (nonatomic, copy) NSDictionary * _Nullable source;
@property (nonatomic, assign) BOOL messagingEnabled;
@property (nonatomic, copy) NSString * _Nullable injectedJavaScript;
Expand Down
51 changes: 31 additions & 20 deletions apple/RNCWebView.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#import <React/RCTConvert.h>
#import <React/RCTAutoInsetsProtocol.h>
#import "RNCWKProcessPoolManager.h"
#import "RNCWebViewDecisionManager.h"
#if !TARGET_OS_OSX
#import <UIKit/UIKit.h>
#else
Expand Down Expand Up @@ -1106,7 +1107,32 @@ - (void) webView:(WKWebView *)webView
NSURLRequest *request = navigationAction.request;
BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];

void (^allowNavigation)(void) = ^{
if (self->_onLoadingStart) {
// We have this check to filter out iframe requests and whatnot
if (isTopFrame) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(navigationType)]
}];
self->_onLoadingStart(event);
}
}
decisionHandler(WKNavigationActionPolicyAllow);
};

if (_onShouldStartLoadWithRequest) {
int lockIdentifier = [[RNCWebViewDecisionManager getInstance] setDecisionHandler:^(BOOL shouldStart) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!shouldStart) {
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
allowNavigation();
});
}];

Comment on lines +1126 to +1135
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reference: https://github.com/react-native-webview/react-native-webview/blob/5bc526fce5b9d6225df183bdf3d8cf542007d90a/apple/RNCWebViewImpl.m#L1366-L1386

The allowing code is moved into the allowNavigation helper to be reused below to keep the old fallback behavior

NSMutableDictionary<NSString *, id> *event = [self baseEvent];
if (request.mainDocumentURL) {
[event addEntriesFromDictionary: @{
Expand All @@ -1116,30 +1142,15 @@ - (void) webView:(WKWebView *)webView
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(navigationType)],
@"isTopFrame": @(isTopFrame)
@"isTopFrame": @(isTopFrame),
@"lockIdentifier": @(lockIdentifier)
}];
if (![self.delegate webView:self
shouldStartLoadForRequest:event
withCallback:_onShouldStartLoadWithRequest]) {
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}

if (_onLoadingStart) {
// We have this check to filter out iframe requests and whatnot
if (isTopFrame) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(navigationType)]
}];
_onLoadingStart(event);
}
_onShouldStartLoadWithRequest(event);
return;
}

// Allow all navigation by default
decisionHandler(WKNavigationActionPolicyAllow);
allowNavigation();
}

/**
Expand Down
18 changes: 18 additions & 0 deletions apple/RNCWebViewDecisionManager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#import <Foundation/Foundation.h>

typedef void (^DecisionBlock)(BOOL);

@interface RNCWebViewDecisionManager : NSObject {
int nextLockIdentifier;
NSMutableDictionary *decisionHandlers;
}

@property (nonatomic) int nextLockIdentifier;
@property (nonatomic, retain) NSMutableDictionary *decisionHandlers;

+ (id) getInstance;

- (int)setDecisionHandler:(DecisionBlock)handler;
- (void) setResult:(BOOL)shouldStart
forLockIdentifier:(int)lockIdentifier;
@end
73 changes: 73 additions & 0 deletions apple/RNCWebViewDecisionManager.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#import "RNCWebViewDecisionManager.h"
#import <React/RCTLog.h>

/**
* Thread-safe singleton that manages navigation decision handlers for WKWebView.
*
* This class bridges async navigation decisions between:
* - WKWebView delegate (main thread) - stores decision handlers
* - React Native bridge (background thread) - resolves decisions from JS
*
* All public methods use @synchronized for thread safety since they access
* shared state (nextLockIdentifier and decisionHandlers) from different threads.
*/
@implementation RNCWebViewDecisionManager

@synthesize nextLockIdentifier;
@synthesize decisionHandlers;

+ (id)getInstance {
static RNCWebViewDecisionManager *lockManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lockManager = [[self alloc] init];
});
return lockManager;
}

/**
* Stores a decision handler and returns a unique identifier.
* Called from the main thread (WKNavigationDelegate).
* @synchronized ensures atomic increment + insertion.
*/
- (int)setDecisionHandler:(DecisionBlock)decisionHandler {
@synchronized (self) {
int lockIdentifier = self.nextLockIdentifier++;
[self.decisionHandlers setObject:decisionHandler forKey:@(lockIdentifier)];
return lockIdentifier;
}
}

/**
* Resolves a pending navigation decision.
* Called from the RN bridge thread (background) when JS responds.
*
* The handler is invoked OUTSIDE the @synchronized block to prevent deadlocks,
* since the handler dispatches to the main queue and could potentially
* trigger another navigation that re-enters this class.
*/
- (void) setResult:(BOOL)shouldStart
forLockIdentifier:(int)lockIdentifier {
DecisionBlock handler;
@synchronized (self) {
handler = [self.decisionHandlers objectForKey:@(lockIdentifier)];
if (handler == nil) {
RCTLogWarn(@"Lock not found");
return;
}
[self.decisionHandlers removeObjectForKey:@(lockIdentifier)];
}
handler(shouldStart);
}

- (id)init {
if (self = [super init]) {
self.nextLockIdentifier = 1;
self.decisionHandlers = [[NSMutableDictionary alloc] init];
}
return self;
}

- (void)dealloc {}

@end
41 changes: 4 additions & 37 deletions apple/RNCWebViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
#import <React/RCTUIManager.h>
#import <React/RCTDefines.h>
#import "RNCWebView.h"
#import "RNCWebViewDecisionManager.h"

@interface RNCWebViewManager () <RNCWebViewDelegate>
@interface RNCWebViewManager ()
@end

@implementation RCTConvert (WKWebView)
Expand All @@ -35,10 +36,6 @@ @implementation RCTConvert (WKWebView)
@end

@implementation RNCWebViewManager
{
NSConditionLock *_shouldStartLoadLock;
BOOL _shouldStartLoad;
}

RCT_EXPORT_MODULE()

Expand All @@ -49,7 +46,6 @@ - (RCTUIView *)view
#endif // !TARGET_OS_OSX
{
RNCWebView *webView = [RNCWebView new];
webView.delegate = self;
return webView;
}

Expand Down Expand Up @@ -243,38 +239,9 @@ - (RCTUIView *)view
}];
}

#pragma mark - Exported synchronous methods

- (BOOL) webView:(RNCWebView *)webView
shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
withCallback:(RCTDirectEventBlock)callback
{
_shouldStartLoadLock = [[NSConditionLock alloc] initWithCondition:arc4random()];
_shouldStartLoad = YES;
request[@"lockIdentifier"] = @(_shouldStartLoadLock.condition);
callback(request);

// Block the main thread for a maximum of 500ms until the JS thread returns
if ([_shouldStartLoadLock lockWhenCondition:0 beforeDate:[NSDate dateWithTimeIntervalSinceNow:.50]]) {
BOOL returnValue = _shouldStartLoad;
[_shouldStartLoadLock unlock];
_shouldStartLoadLock = nil;
return returnValue;
} else {
RCTLogWarn(@"Did not receive response to shouldStartLoad in time, defaulting to NO");
return NO;
}
}

RCT_EXPORT_METHOD(startLoadWithResult:(BOOL)result lockIdentifier:(NSInteger)lockIdentifier)
RCT_EXPORT_METHOD(startLoadWithResult:(BOOL)result lockIdentifier:(int)lockIdentifier)
{
if ([_shouldStartLoadLock tryLockWhenCondition:lockIdentifier]) {
_shouldStartLoad = result;
[_shouldStartLoadLock unlockWithCondition:0];
} else {
RCTLogWarn(@"startLoadWithResult invoked with invalid lockIdentifier: "
"got %lld, expected %lld", (long long)lockIdentifier, (long long)_shouldStartLoadLock.condition);
}
[[RNCWebViewDecisionManager getInstance] setResult:result forLockIdentifier:lockIdentifier];
}

@end
Loading