diff --git a/apple/RNCAssetSchemeHandler.h b/apple/RNCAssetSchemeHandler.h new file mode 100644 index 000000000..c7ebb423d --- /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 000000000..4adc5cc88 --- /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/RNCWebView.mm b/apple/RNCWebView.mm index 95c0fcc91..e6e3b9133 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 1f6bbfd69..8d7d95be8 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 7f5c24d6e..70c9fb3be 100644 --- a/apple/RNCWebViewImpl.m +++ b/apple/RNCWebViewImpl.m @@ -6,6 +6,7 @@ */ #import "RNCWebViewImpl.h" +#import "RNCAssetSchemeHandler.h" #import #import #import "RNCWKProcessPoolManager.h" @@ -181,6 +182,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; @@ -509,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; } @@ -545,6 +553,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 +749,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 +1120,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 f8f375f23..5e23a253f 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 a3eef1390..a112f17e1 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 7f4e6c4f7..2e9fc2770 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 deb179bc7..ff5806fa9 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 eca81415a..f1aa423e7 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 {