diff --git a/apple/RNCWebView.h b/apple/RNCWebView.h index 0ce544db3..30c93d4ac 100644 --- a/apple/RNCWebView.h +++ b/apple/RNCWebView.h @@ -19,14 +19,6 @@ typedef enum RNCWebViewPermissionGrantType : NSUInteger { @class RNCWebView; -@protocol RNCWebViewDelegate - -- (BOOL)webView:(RNCWebView *_Nonnull)webView -shouldStartLoadForRequest:(NSMutableDictionary *_Nonnull)request - withCallback:(RCTDirectEventBlock _Nonnull)callback; - -@end - @interface RNCWeakScriptMessageDelegate : NSObject @property (nonatomic, weak, nullable) id scriptDelegate; @@ -37,7 +29,6 @@ shouldStartLoadForRequest:(NSMutableDictionary *_Nonnull)request @interface RNCWebView : RCTView -@property (nonatomic, weak) id _Nullable delegate; @property (nonatomic, copy) NSDictionary * _Nullable source; @property (nonatomic, assign) BOOL messagingEnabled; @property (nonatomic, copy) NSString * _Nullable injectedJavaScript; diff --git a/apple/RNCWebView.m b/apple/RNCWebView.m index 702dcd3f2..9bad01463 100644 --- a/apple/RNCWebView.m +++ b/apple/RNCWebView.m @@ -9,6 +9,7 @@ #import #import #import "RNCWKProcessPoolManager.h" +#import "RNCWebViewDecisionManager.h" #if !TARGET_OS_OSX #import #else @@ -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 *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(); + }); + }]; + NSMutableDictionary *event = [self baseEvent]; if (request.mainDocumentURL) { [event addEntriesFromDictionary: @{ @@ -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 *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(); } /** diff --git a/apple/RNCWebViewDecisionManager.h b/apple/RNCWebViewDecisionManager.h new file mode 100644 index 000000000..730d83228 --- /dev/null +++ b/apple/RNCWebViewDecisionManager.h @@ -0,0 +1,18 @@ +#import + +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 diff --git a/apple/RNCWebViewDecisionManager.m b/apple/RNCWebViewDecisionManager.m new file mode 100644 index 000000000..268b9c354 --- /dev/null +++ b/apple/RNCWebViewDecisionManager.m @@ -0,0 +1,73 @@ +#import "RNCWebViewDecisionManager.h" +#import + +/** + * 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 diff --git a/apple/RNCWebViewManager.m b/apple/RNCWebViewManager.m index b5b76cbd3..accf031ac 100644 --- a/apple/RNCWebViewManager.m +++ b/apple/RNCWebViewManager.m @@ -10,8 +10,9 @@ #import #import #import "RNCWebView.h" +#import "RNCWebViewDecisionManager.h" -@interface RNCWebViewManager () +@interface RNCWebViewManager () @end @implementation RCTConvert (WKWebView) @@ -35,10 +36,6 @@ @implementation RCTConvert (WKWebView) @end @implementation RNCWebViewManager -{ - NSConditionLock *_shouldStartLoadLock; - BOOL _shouldStartLoad; -} RCT_EXPORT_MODULE() @@ -49,7 +46,6 @@ - (RCTUIView *)view #endif // !TARGET_OS_OSX { RNCWebView *webView = [RNCWebView new]; - webView.delegate = self; return webView; } @@ -243,38 +239,9 @@ - (RCTUIView *)view }]; } -#pragma mark - Exported synchronous methods - -- (BOOL) webView:(RNCWebView *)webView -shouldStartLoadForRequest:(NSMutableDictionary *)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