From 010b9de233009bab389da689869aac90799f4a51 Mon Sep 17 00:00:00 2001 From: Artem Litchmanov Date: Sun, 8 Feb 2026 12:31:29 -0800 Subject: [PATCH 1/2] Re-add Readwise custom features on v13.16.0 with Fabric support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-applies 4 custom Readwise features on top of upstream v13.16.0: 1. setTintColor command - controls WebView selection/tint color via RGBA doubles. Fixed Fabric signature to match codegen protocol (4 doubles instead of UIColor), preventing SIGABRT on New Architecture. 2. scrollsToTop prop - controls scroll-to-top on status bar tap. 3. dragInteractionEnabled prop - controls drag interactions on the webview. 4. forceLightScrollIndicators is NOT re-added — upstream v13.14.0+ has indicatorStyle prop which supersedes it with more flexibility. Co-Authored-By: Claude Opus 4.6 --- apple/RNCWebView.mm | 10 ++++++++++ apple/RNCWebViewImpl.h | 3 +++ apple/RNCWebViewImpl.m | 27 +++++++++++++++++++++++++++ apple/RNCWebViewManager.mm | 24 ++++++++++++++++++++++++ index.d.ts | 6 ++++++ src/RNCWebViewNativeComponent.ts | 12 ++++++++++++ src/WebView.ios.tsx | 3 +++ src/WebViewTypes.ts | 19 ++++++++++++++++++- 8 files changed, 103 insertions(+), 1 deletion(-) diff --git a/apple/RNCWebView.mm b/apple/RNCWebView.mm index 95c0fcc91b..e6e3b91334 100644 --- a/apple/RNCWebView.mm +++ b/apple/RNCWebView.mm @@ -312,6 +312,8 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & REMAP_WEBVIEW_PROP(showsHorizontalScrollIndicator) REMAP_WEBVIEW_PROP(showsVerticalScrollIndicator) REMAP_WEBVIEW_PROP(keyboardDisplayRequiresUserAction) + REMAP_WEBVIEW_PROP(scrollsToTop) + REMAP_WEBVIEW_PROP(dragInteractionEnabled) #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */ REMAP_WEBVIEW_PROP(automaticallyAdjustContentInsets) @@ -551,5 +553,13 @@ - (void)clearHistory { // android only } +- (void)setTintColor:(double)red green:(double)green blue:(double)blue alpha:(double)alpha { + UIColor *color = [UIColor colorWithRed:red / 255.0 + green:green / 255.0 + blue:blue / 255.0 + alpha:alpha]; + [_view setTintColor:color]; +} + @end #endif diff --git a/apple/RNCWebViewImpl.h b/apple/RNCWebViewImpl.h index 1f6bbfd697..8d7d95be82 100644 --- a/apple/RNCWebViewImpl.h +++ b/apple/RNCWebViewImpl.h @@ -110,6 +110,8 @@ shouldStartLoadForRequest:(NSMutableDictionary *)request @property (nonatomic, assign) BOOL pullToRefreshEnabled; @property (nonatomic, assign) BOOL refreshControlLightMode; @property (nonatomic, assign) BOOL enableApplePay; +@property (nonatomic, assign) BOOL scrollsToTop; +@property (nonatomic, assign) BOOL dragInteractionEnabled; @property (nonatomic, copy) NSArray * _Nullable menuItems; @property (nonatomic, copy) NSArray * _Nullable suppressMenuItems; @property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection; @@ -149,6 +151,7 @@ shouldStartLoadForRequest:(NSMutableDictionary *)request - (void)stopLoading; - (void)requestFocus; - (void)clearCache:(BOOL)includeDiskFiles; +- (void)setTintColor:(UIColor *)tintColor; #ifdef RCT_NEW_ARCH_ENABLED - (void)destroyWebView; #endif diff --git a/apple/RNCWebViewImpl.m b/apple/RNCWebViewImpl.m index 7f5c24d6e2..888942cc12 100644 --- a/apple/RNCWebViewImpl.m +++ b/apple/RNCWebViewImpl.m @@ -181,6 +181,8 @@ - (instancetype)initWithFrame:(CGRect)frame _injectedJavaScriptBeforeContentLoaded = nil; _injectedJavaScriptBeforeContentLoadedForMainFrameOnly = YES; _enableApplePay = NO; + _scrollsToTop = YES; + _dragInteractionEnabled = YES; #if TARGET_OS_IOS _savedStatusBarStyle = RCTSharedApplication().statusBarStyle; _savedStatusBarHidden = RCTSharedApplication().statusBarHidden; @@ -545,6 +547,7 @@ - (void)didMoveToWindow } _webView.scrollView.directionalLockEnabled = _directionalLockEnabled; + _webView.scrollView.scrollsToTop = _scrollsToTop; #endif // !TARGET_OS_OSX _webView.allowsLinkPreview = _allowsLinkPreview; [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil]; @@ -740,6 +743,11 @@ - (void)setBackgroundColor:(RCTUIColor *)backgroundColor #endif // !TARGET_OS_OSX } +- (void)setTintColor:(UIColor *)tintColor +{ + _webView.tintColor = tintColor; +} + #if !TARGET_OS_OSX - (void)setContentInsetAdjustmentBehavior:(UIScrollViewContentInsetAdjustmentBehavior)behavior { @@ -1106,6 +1114,25 @@ - (void)setIndicatorStyle:(NSString *)indicatorStyle _webView.scrollView.indicatorStyle = UIScrollViewIndicatorStyleDefault; } } + +- (void)setScrollsToTop:(BOOL)scrollsToTop +{ + _scrollsToTop = scrollsToTop; + _webView.scrollView.scrollsToTop = scrollsToTop; +} + +- (void)setDragInteractionEnabled:(BOOL)dragInteractionEnabled +{ + _dragInteractionEnabled = dragInteractionEnabled; + if (@available(iOS 11.0, *)) { + for (id interaction in _webView.scrollView.interactions) { + if ([interaction isKindOfClass:[UIDragInteraction class]]) { + ((UIDragInteraction *)interaction).enabled = dragInteractionEnabled; + } + } + } +} + #endif // !TARGET_OS_OSX - (void)postMessage:(NSString *)message diff --git a/apple/RNCWebViewManager.mm b/apple/RNCWebViewManager.mm index f8f375f23b..5e23a253fe 100644 --- a/apple/RNCWebViewManager.mm +++ b/apple/RNCWebViewManager.mm @@ -175,6 +175,14 @@ - (RNCView *)view view.keyboardDisplayRequiresUserAction = json == nil ? true : [RCTConvert BOOL: json]; } +RCT_CUSTOM_VIEW_PROPERTY(scrollsToTop, BOOL, RNCWebViewImpl) { + view.scrollsToTop = json == nil ? true : [RCTConvert BOOL: json]; +} + +RCT_CUSTOM_VIEW_PROPERTY(dragInteractionEnabled, BOOL, RNCWebViewImpl) { + view.dragInteractionEnabled = json == nil ? true : [RCTConvert BOOL: json]; +} + #if !TARGET_OS_OSX #define BASE_VIEW_PER_OS() UIView #else @@ -216,4 +224,20 @@ - (RNCView *)view QUICK_RCT_EXPORT_COMMAND_METHOD_PARAMS(injectJavaScript, script:(NSString *)script, script) QUICK_RCT_EXPORT_COMMAND_METHOD_PARAMS(clearCache, includeDiskFiles:(BOOL)includeDiskFiles, includeDiskFiles) +RCT_EXPORT_METHOD(setTintColor:(nonnull NSNumber *)reactTag red:(double)red green:(double)green blue:(double)blue alpha:(double)alpha) +{ +[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RNCWebViewImpl *view = (RNCWebViewImpl *)viewRegistry[reactTag]; + if (![view isKindOfClass:[RNCWebViewImpl class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RNCWebView, got: %@", view); + } else { + UIColor *color = [UIColor colorWithRed:red / 255.0 + green:green / 255.0 + blue:blue / 255.0 + alpha:alpha]; + [view setTintColor:color]; + } + }]; +} + @end diff --git a/index.d.ts b/index.d.ts index a3eef1390c..a112f17e10 100644 --- a/index.d.ts +++ b/index.d.ts @@ -58,6 +58,12 @@ declare class WebView

extends Component { * Tells this WebView to clear its internal back/forward list. */ clearHistory?: () => void; + + /** + * (iOS only) + * Sets the tint color (selection color) of the WebView. + */ + setTintColor: (red: number, green: number, blue: number, alpha: number) => void; } export {WebView}; diff --git a/src/RNCWebViewNativeComponent.ts b/src/RNCWebViewNativeComponent.ts index 7f4e6c4f73..2e9fc2770f 100644 --- a/src/RNCWebViewNativeComponent.ts +++ b/src/RNCWebViewNativeComponent.ts @@ -234,7 +234,9 @@ export interface NativeProps extends ViewProps { pullToRefreshEnabled?: boolean; refreshControlLightMode?: boolean; scrollEnabled?: WithDefault; + scrollsToTop?: WithDefault; sharedCookiesEnabled?: boolean; + dragInteractionEnabled?: WithDefault; textInteractionEnabled?: WithDefault; useSharedProcessPool?: WithDefault; onContentProcessDidTerminate?: DirectEventHandler; @@ -325,6 +327,15 @@ export interface NativeCommands { ) => void; clearHistory: (viewRef: React.ElementRef>) => void; // !Android Only + + // iOS Only (Readwise custom) + setTintColor: ( + viewRef: React.ElementRef>, + red: Double, + green: Double, + blue: Double, + alpha: Double + ) => void; } export const Commands = codegenNativeCommands({ @@ -340,6 +351,7 @@ export const Commands = codegenNativeCommands({ 'clearFormData', 'clearCache', 'clearHistory', + 'setTintColor', ], }); diff --git a/src/WebView.ios.tsx b/src/WebView.ios.tsx index deb179bc7c..ff5806fa90 100644 --- a/src/WebView.ios.tsx +++ b/src/WebView.ios.tsx @@ -159,6 +159,9 @@ const WebViewComponent = forwardRef<{}, IOSWebViewProps>( clearCache: (includeDiskFiles: boolean) => webViewRef.current && Commands.clearCache(webViewRef.current, includeDiskFiles), + setTintColor: (red: number, green: number, blue: number, alpha: number) => + webViewRef.current && + Commands.setTintColor(webViewRef.current, red, green, blue, alpha), }), [setViewState, webViewRef] ); diff --git a/src/WebViewTypes.ts b/src/WebViewTypes.ts index eca81415ab..f1aa423e70 100644 --- a/src/WebViewTypes.ts +++ b/src/WebViewTypes.ts @@ -24,6 +24,8 @@ type WebViewCommands = type AndroidWebViewCommands = 'clearHistory' | 'clearFormData'; +type IOSWebViewCommands = 'setTintColor'; + interface RNCWebViewUIManager extends UIManagerStatic { getViewManagerConfig: (name: string) => { Commands: { [key in Commands]: number }; @@ -33,7 +35,7 @@ interface RNCWebViewUIManager extends UIManagerStatic { export type RNCWebViewUIManagerAndroid = RNCWebViewUIManager< WebViewCommands | AndroidWebViewCommands >; -export type RNCWebViewUIManagerIOS = RNCWebViewUIManager; +export type RNCWebViewUIManagerIOS = RNCWebViewUIManager; export type RNCWebViewUIManagerMacOS = RNCWebViewUIManager; export type RNCWebViewUIManagerWindows = RNCWebViewUIManager; @@ -777,6 +779,21 @@ export interface IOSWebViewProps extends WebViewSharedProps { * @platform ios */ fraudulentWebsiteWarningEnabled?: boolean; + + /** + * A Boolean value that controls whether the web view scrolls to the top of + * the content when the user taps the status bar. + * The default value is `true`. + * @platform ios + */ + scrollsToTop?: boolean; + + /** + * A Boolean value that determines whether drag interactions are enabled + * on the web view. The default value is `true`. + * @platform ios + */ + dragInteractionEnabled?: boolean; } export interface MacOSWebViewProps extends WebViewSharedProps { From e5614188387c5a8d62897ddce6d7836d66bd3562 Mon Sep 17 00:00:00 2001 From: Artem Litchmanov Date: Sun, 8 Feb 2026 21:19:59 -0800 Subject: [PATCH 2/2] Add rw-asset:// URL scheme handler for serving app bundle assets WKWebView pages loaded with about:blank origin (via loadHTMLString:baseURL:) cannot access file:// URLs. This custom scheme handler serves files from the app bundle (fonts, etc.) via rw-asset:///filename URLs, bypassing the file:// security restriction. Co-Authored-By: Claude Opus 4.6 --- apple/RNCAssetSchemeHandler.h | 4 +++ apple/RNCAssetSchemeHandler.m | 66 +++++++++++++++++++++++++++++++++++ apple/RNCWebViewImpl.m | 6 ++++ 3 files changed, 76 insertions(+) create mode 100644 apple/RNCAssetSchemeHandler.h create mode 100644 apple/RNCAssetSchemeHandler.m diff --git a/apple/RNCAssetSchemeHandler.h b/apple/RNCAssetSchemeHandler.h new file mode 100644 index 0000000000..c7ebb423d9 --- /dev/null +++ b/apple/RNCAssetSchemeHandler.h @@ -0,0 +1,4 @@ +#import + +@interface RNCAssetSchemeHandler : NSObject +@end diff --git a/apple/RNCAssetSchemeHandler.m b/apple/RNCAssetSchemeHandler.m new file mode 100644 index 0000000000..4adc5cc88a --- /dev/null +++ b/apple/RNCAssetSchemeHandler.m @@ -0,0 +1,66 @@ +#import "RNCAssetSchemeHandler.h" + +@implementation RNCAssetSchemeHandler + +- (void)webView:(WKWebView *)webView startURLSchemeTask:(id)urlSchemeTask { + NSURL *url = urlSchemeTask.request.URL; + NSString *path = url.path; + + // Strip leading slash from path to get the filename + if ([path hasPrefix:@"/"]) { + path = [path substringFromIndex:1]; + } + + // URL-decode the path (handles brackets and other special chars) + NSString *fileName = [path stringByRemovingPercentEncoding]; + if (!fileName || fileName.length == 0) { + NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]; + [urlSchemeTask didFailWithError:error]; + return; + } + + // Find the file in the app bundle + NSString *bundleResourcePath = [[NSBundle mainBundle] resourcePath]; + NSString *fullPath = [bundleResourcePath stringByAppendingPathComponent:fileName]; + + if (![[NSFileManager defaultManager] fileExistsAtPath:fullPath]) { + NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]; + [urlSchemeTask didFailWithError:error]; + return; + } + + NSData *data = [NSData dataWithContentsOfFile:fullPath]; + if (!data) { + NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotOpenFile userInfo:nil]; + [urlSchemeTask didFailWithError:error]; + return; + } + + // Determine MIME type from extension + NSString *mimeType = @"application/octet-stream"; + NSString *ext = [fileName pathExtension]; + if ([ext isEqualToString:@"woff2"]) { + mimeType = @"font/woff2"; + } else if ([ext isEqualToString:@"ttf"]) { + mimeType = @"font/ttf"; + } else if ([ext isEqualToString:@"otf"]) { + mimeType = @"font/otf"; + } else if ([ext isEqualToString:@"woff"]) { + mimeType = @"font/woff"; + } + + NSURLResponse *response = [[NSURLResponse alloc] initWithURL:url + MIMEType:mimeType + expectedContentLength:data.length + textEncodingName:nil]; + + [urlSchemeTask didReceiveResponse:response]; + [urlSchemeTask didReceiveData:data]; + [urlSchemeTask didFinish]; +} + +- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id)urlSchemeTask { + // Requests complete synchronously, nothing to cancel +} + +@end diff --git a/apple/RNCWebViewImpl.m b/apple/RNCWebViewImpl.m index 888942cc12..70c9fb3bea 100644 --- a/apple/RNCWebViewImpl.m +++ b/apple/RNCWebViewImpl.m @@ -6,6 +6,7 @@ */ #import "RNCWebViewImpl.h" +#import "RNCAssetSchemeHandler.h" #import #import #import "RNCWKProcessPoolManager.h" @@ -511,6 +512,11 @@ - (WKWebViewConfiguration *)setUpWkWebViewConfig wkWebViewConfig.applicationNameForUserAgent = [NSString stringWithFormat:@"%@ %@", wkWebViewConfig.applicationNameForUserAgent, _applicationNameForUserAgent]; } + // Register custom URL scheme handler to serve app bundle assets (fonts, etc.) + // from WKWebView pages loaded with about:blank origin, which can't access file:// URLs. + RNCAssetSchemeHandler *assetHandler = [[RNCAssetSchemeHandler alloc] init]; + [wkWebViewConfig setURLSchemeHandler:assetHandler forURLScheme:@"rw-asset"]; + return wkWebViewConfig; }