From 0334cdaad66031e58fa11c9afea36d80731813f9 Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Thu, 17 Sep 2026 15:36:21 -0400 Subject: [PATCH 01/13] Introduce a "target" for a WebView to render at --- .../webview/RNCWebViewManager.java | 3 + .../webview/RNCWebViewMapManager.kt | 19 +++ .../webview/RNCWebViewPackage.kt | 3 +- .../webview/RNCWebViewTargetManager.kt | 148 ++++++++++++++++++ apple/RNCWebView.m | 11 ++ apple/RNCWebViewTarget.h | 19 +++ apple/RNCWebViewTarget.m | 120 ++++++++++++++ apple/RNCWebViewTargetManager.h | 4 + apple/RNCWebViewTargetManager.m | 18 +++ index.d.ts | 5 +- index.js | 3 +- lib/WebViewTarget.d.ts | 8 + lib/WebViewTarget.js | 6 + src/WebViewTarget.tsx | 29 ++++ src/index.ts | 4 +- 15 files changed, 396 insertions(+), 4 deletions(-) create mode 100644 android/src/main/java/com/reactnativecommunity/webview/RNCWebViewTargetManager.kt create mode 100644 apple/RNCWebViewTarget.h create mode 100644 apple/RNCWebViewTarget.m create mode 100644 apple/RNCWebViewTargetManager.h create mode 100644 apple/RNCWebViewTargetManager.m create mode 100644 lib/WebViewTarget.d.ts create mode 100644 lib/WebViewTarget.js create mode 100644 src/WebViewTarget.tsx diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index ef58458788..c8e8386f35 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -349,6 +349,9 @@ public void setWebViewKey(RNCWebViewContainer view, String webViewKey) { webView.setWebViewKey(webViewKey); RNCWebViewMapManager.INSTANCE.getViewIdMap().put(webView.getId(), view.getId()); rncWebViewMap.put(webViewKey, webView); + // Let any RNCWebViewTargetView waiting on this key borrow the view now that it's registered. Deferred to the + // next frame so the builder finishes attaching it before a target reparents it away. + view.post(() -> RNCWebViewMapManager.INSTANCE.notifyWebViewRegistered(webViewKey)); }); } diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewMapManager.kt b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewMapManager.kt index b1b20aa0a1..0d5e351e3a 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewMapManager.kt +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewMapManager.kt @@ -15,4 +15,23 @@ object RNCWebViewMapManager { // This map facilitates finding the viewId of the wrapper RNCWebView from the WebView // view Id. val viewIdMap = mutableMapOf(); + + // Listeners notified (with the webViewKey) when a WebView is registered, so a RNCWebViewTargetView already + // waiting on that key can borrow it as soon as it exists rather than racing the builder that creates it. + private val registrationListeners = mutableListOf<(String) -> Unit>() + + fun addRegistrationListener(listener: (String) -> Unit) { + registrationListeners.add(listener) + } + + fun removeRegistrationListener(listener: (String) -> Unit) { + registrationListeners.remove(listener) + } + + fun notifyWebViewRegistered(webViewKey: String) { + // Copy first: a listener may borrow synchronously and mutate the list. + for (listener in registrationListeners.toList()) { + listener(webViewKey) + } + } } diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.kt b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.kt index 2b74c74aa9..9bd21ba6ec 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.kt +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.kt @@ -10,6 +10,7 @@ class RNCWebViewPackage: ReactPackage { ) override fun createViewManagers(reactContext: ReactApplicationContext) = listOf( - RNCWebViewManager() + RNCWebViewManager(), + RNCWebViewTargetManager() ) } diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewTargetManager.kt b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewTargetManager.kt new file mode 100644 index 0000000000..e0e94fa3da --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewTargetManager.kt @@ -0,0 +1,148 @@ +package com.reactnativecommunity.webview + +import android.view.View +import android.view.ViewGroup +import android.webkit.WebView +import android.widget.FrameLayout +import com.facebook.react.bridge.ReactContext +import com.facebook.react.uimanager.SimpleViewManager +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerModule +import com.facebook.react.uimanager.annotations.ReactProp + +/** + * A lightweight display target for a WebView that already lives in [RNCWebViewMapManager]. Where RNCWebView + * builds, configures, and owns a WebView, this only borrows one by its webViewKey and shows it. It never + * re-points the WebChromeClient, never writes the viewId map or the shared map, so the RNCWebViewContainer that + * built the WebView stays its owner: onMessage and load callbacks keep routing to that builder while this target + * merely displays the view. On unmount it hands the WebView back to its temporary parent so it survives for the + * next target rather than being torn down. + */ +class RNCWebViewTargetView(context: ThemedReactContext) : FrameLayout(context) { + init { + // React adds the WebView to us without measuring or laying it out (facebook/react-native#17968), so on its own + // it keeps whatever size it had in the pool and leaves a blank gap. We fix that by laying out the CHILD to fill + // us, at our top-left. It has to be the child, never the parent: React owns our own frame and has already placed + // us below the channel header, so re-laying the parent out at (0,0) would yank the whole target up under the + // header and leave a blank strip at the bottom where it no longer reaches. + setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener { + override fun onChildViewAdded(parent: View, child: View) { + child.measure( + View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(parent.measuredHeight, View.MeasureSpec.EXACTLY), + ) + child.layout(0, 0, parent.measuredWidth, parent.measuredHeight) + } + + override fun onChildViewRemoved(parent: View, child: View) {} + }) + } + + var webViewKey: String? = null + set(value) { + field = value + borrow() + } + var temporaryParentNodeTag: Int = 0 + + // Borrow as soon as our builder registers the WebView, in case it mounted after us — the counterpart to the + // borrow() we run on attach for the case where the builder was already there. + private val registrationListener: (String) -> Unit = { key -> + if (key == webViewKey) { + borrow() + } + } + + private fun mappedWebView(): WebView? { + val key = webViewKey ?: return null + return RNCWebViewMapManager.rncWebViewMap[key] + } + + fun borrow() { + val webView = mappedWebView() + if (webView == null) { + return + } + if (webView.parent === this) { + return + } + // Detach from wherever it currently lives — the builder, another target, or the temporary parent — by + // reparenting. We deliberately leave the builder's ownership bookkeeping (viewId map, chrome client) alone; + // we're only moving the view, not taking it over. + (webView.parent as? ViewGroup)?.removeView(webView) + addView(webView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + RNCWebViewMapManager.addRegistrationListener(registrationListener) + // The builder may already have created the WebView; grab it now. If not, registrationListener catches it. + borrow() + } + + override fun onDetachedFromWindow() { + RNCWebViewMapManager.removeRegistrationListener(registrationListener) + super.onDetachedFromWindow() + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + // Our real size can arrive after the WebView was borrowed in (the onHierarchyChange pass may have run before + // we were measured), so re-lay it out to our current bounds here to keep the guest filling us exactly. + val webView = mappedWebView() ?: return + if (webView.parent === this) { + webView.measure( + View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY), + ) + webView.layout(0, 0, w, h) + } + } + + fun returnToHome() { + val webView = mappedWebView() ?: return + // Only return it if we're the one currently showing it — if another target already stole it, it owns the + // hand-off now. + if (webView.parent !== this) { + return + } + removeView(webView) + if (temporaryParentNodeTag == 0) { + return + } + val uiManager = (context as ReactContext).getNativeModule(UIManagerModule::class.java) ?: return + val home = uiManager.resolveView(temporaryParentNodeTag) as? ViewGroup ?: return + home.addView(webView) + // Resize to match the home, mirroring how RNCWebViewManager returns a view to its temporary parent. + webView.measure( + MeasureSpec.makeMeasureSpec(home.measuredWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(home.measuredHeight, MeasureSpec.EXACTLY) + ) + webView.layout(0, 0, webView.measuredWidth, webView.measuredHeight) + } +} + +class RNCWebViewTargetManager : SimpleViewManager() { + override fun getName() = REACT_CLASS + + public override fun createViewInstance(reactContext: ThemedReactContext) = RNCWebViewTargetView(reactContext) + + @ReactProp(name = "webViewKey") + fun setWebViewKey(view: RNCWebViewTargetView, webViewKey: String?) { + view.webViewKey = webViewKey + } + + @ReactProp(name = "temporaryParentNodeTag") + fun setTemporaryParentNodeTag(view: RNCWebViewTargetView, tag: Int) { + view.temporaryParentNodeTag = tag + } + + override fun onDropViewInstance(view: RNCWebViewTargetView) { + super.onDropViewInstance(view) + view.returnToHome() + } + + companion object { + const val REACT_CLASS = "RNCWebViewTarget" + } +} diff --git a/apple/RNCWebView.m b/apple/RNCWebView.m index a97e25c251..fd7bc6492e 100644 --- a/apple/RNCWebView.m +++ b/apple/RNCWebView.m @@ -12,6 +12,7 @@ #import #import "RNCWKProcessPoolManager.h" #import "RNCWKWebViewMapManager.h" +#import "RNCWebViewTarget.h" #import "RNCScriptMessageManager.h" #import "ScriptMessageEventEmitter.h" @@ -512,6 +513,16 @@ - (void)didMoveToWindow if (!reusedWebViewInstance) { [self visitSource]; } + + // Now that this WebView exists and is registered under its key, let any RNCWebViewTarget already waiting on + // that key borrow it. Deferred to the next runloop so we finish attaching it here before a target reparents + // it away. + if ([self shouldReuseWebView] && _webViewKey != nil) { + NSString *registeredKey = _webViewKey; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:RNCWebViewDidRegisterNotification object:registeredKey]; + }); + } } #if !TARGET_OS_OSX // Allow this object to recognize gestures diff --git a/apple/RNCWebViewTarget.h b/apple/RNCWebViewTarget.h new file mode 100644 index 0000000000..d3924e6ba2 --- /dev/null +++ b/apple/RNCWebViewTarget.h @@ -0,0 +1,19 @@ +/** + * A lightweight display target for a WebView that already exists in the shared map. Where RNCWebView builds, + * configures, and owns a WKWebView, RNCWebViewTarget only borrows one by its webViewKey and shows it here. It + * never sets a source, a user agent, a delegate, or any configuration, so the RNCWebView that built the view + * stays its owner — navigation callbacks and the message stream keep flowing there while this target merely + * displays it. On unmount it hands the view back to its temporary parent so it survives for the next target. + */ +#import +#import + +// Posted (object = webViewKey) by RNCWebView when it registers a WebView, so a target waiting on that key borrows +// it as soon as it exists rather than racing the builder. +extern NSString *const RNCWebViewDidRegisterNotification; + +@interface RNCWebViewTarget : RCTView +@property (nonatomic, copy) NSString * _Nullable webViewKey; +@property (nonatomic, copy) NSNumber * _Nullable temporaryParentNodeTag; +@property (nonatomic, weak) RCTBridge * _Nullable bridge; +@end diff --git a/apple/RNCWebViewTarget.m b/apple/RNCWebViewTarget.m new file mode 100644 index 0000000000..5cdfa37d8d --- /dev/null +++ b/apple/RNCWebViewTarget.m @@ -0,0 +1,120 @@ +#import "RNCWebViewTarget.h" +#import "RNCWKWebViewMapManager.h" + +#import +#import + +NSString *const RNCWebViewDidRegisterNotification = @"RNCWebViewDidRegister"; + +@implementation RNCWebViewTarget + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onWebViewRegistered:) + name:RNCWebViewDidRegisterNotification + object:nil]; + } + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)onWebViewRegistered:(NSNotification *)notification +{ + id key = notification.object; + if (_webViewKey != nil && [key isKindOfClass:[NSString class]] && [(NSString *)key isEqualToString:_webViewKey]) { + [self borrowWebView]; + } +} + +- (WKWebView * _Nullable)mappedWebView +{ + if (_webViewKey == nil) { + return nil; + } + NSMutableDictionary *sharedWKWebViewDictionary = [[RNCWKWebViewMapManager sharedManager] sharedWKWebViewDictionary]; + id webView = sharedWKWebViewDictionary[_webViewKey]; + return [webView isKindOfClass:[WKWebView class]] ? (WKWebView *)webView : nil; +} + +/** + * Borrow the shared WKWebView for our key and show it here. This only moves the view into our hierarchy; it never + * touches the WebView's configuration, its navigation/UI delegates, or its script message handlers. Those stay + * owned by the RNCWebView that built it, so the RPC socket and load callbacks keep flowing to that builder while + * we display the view. The autoresizing mask makes the WebView track our bounds, so it reflows to whatever size + * this target is laid out at — no fixed sizing anywhere. If no WebView exists for this key yet (its builder hasn't + * registered one), there's nothing to show; we pick it up from the registration notification once it appears. + */ +- (void)borrowWebView +{ + if (self.window == nil) { + return; + } + WKWebView *wkWebView = [self mappedWebView]; + if (wkWebView == nil) { + return; + } + if (wkWebView.superview == self) { + return; + } + // Detach from wherever it currently lives — the builder's RNCWebView, another target, or a temporary parent — + // by reparenting. addSubview removes it from its old superview for us; we deliberately don't call the builder's + // teardown, since it isn't going away, it's just no longer the one on screen. + wkWebView.frame = self.bounds; + wkWebView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:wkWebView]; +} + +- (void)setWebViewKey:(NSString *)webViewKey +{ + _webViewKey = webViewKey; + [self borrowWebView]; +} + +- (void)didMoveToWindow +{ + [super didMoveToWindow]; + [self borrowWebView]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + WKWebView *wkWebView = [self mappedWebView]; + if (wkWebView != nil && wkWebView.superview == self) { + wkWebView.frame = self.bounds; + } else { + // Not showing it yet — a common case is that our builder hadn't created the view when we first mounted. A + // layout pass is a cheap, natural point to try again (the registration notification is the primary path). + [self borrowWebView]; + } +} + +/** + * When this target unmounts, hand the WebView back to its home (its temporary parent) so it stays alive for the + * next target, rather than being torn down with us. Only do this if we're the one currently showing it — if + * another target already stole it, its own bookkeeping owns it now. + */ +- (void)removeFromSuperview +{ + WKWebView *wkWebView = [self mappedWebView]; + if (wkWebView != nil && wkWebView.superview == self) { + UIView *home = _temporaryParentNodeTag != nil + ? [self.bridge.uiManager viewForReactTag:_temporaryParentNodeTag] + : nil; + if (home != nil) { + wkWebView.frame = home.bounds; + [home addSubview:wkWebView]; + } else { + [wkWebView removeFromSuperview]; + } + } + [super removeFromSuperview]; +} + +@end diff --git a/apple/RNCWebViewTargetManager.h b/apple/RNCWebViewTargetManager.h new file mode 100644 index 0000000000..438c15b6c3 --- /dev/null +++ b/apple/RNCWebViewTargetManager.h @@ -0,0 +1,4 @@ +#import + +@interface RNCWebViewTargetManager : RCTViewManager +@end diff --git a/apple/RNCWebViewTargetManager.m b/apple/RNCWebViewTargetManager.m new file mode 100644 index 0000000000..7fb38a59d4 --- /dev/null +++ b/apple/RNCWebViewTargetManager.m @@ -0,0 +1,18 @@ +#import "RNCWebViewTargetManager.h" +#import "RNCWebViewTarget.h" + +@implementation RNCWebViewTargetManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + RNCWebViewTarget *target = [RNCWebViewTarget new]; + target.bridge = self.bridge; + return target; +} + +RCT_EXPORT_VIEW_PROPERTY(webViewKey, NSString) +RCT_EXPORT_VIEW_PROPERTY(temporaryParentNodeTag, NSNumber) + +@end diff --git a/index.d.ts b/index.d.ts index 0f346bd259..7888235fdd 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,9 +2,12 @@ import { Component } from 'react'; // eslint-disable-next-line import getWebViewProxy from './lib/getWebViewProxy'; +import WebViewTarget from './lib/WebViewTarget'; import { IOSWebViewProps, AndroidWebViewProps, WindowsWebViewProps } from './lib/WebViewTypes'; +export type { WebViewTargetProps } from './lib/WebViewTarget'; + export { FileDownload, WebViewMessageEvent, WebViewNavigation } from "./lib/WebViewTypes"; export type WebViewProps = IOSWebViewProps & AndroidWebViewProps & WindowsWebViewProps; @@ -64,5 +67,5 @@ declare class WebView

extends Component { clearHistory?: () => void; } -export {WebView, getWebViewProxy}; +export {WebView, WebViewTarget, getWebViewProxy}; export default WebView; diff --git a/index.js b/index.js index ec86109069..333b72804d 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ import WebView from './lib/WebView'; +import WebViewTarget from './lib/WebViewTarget'; import getWebViewProxy from './lib/getWebViewProxy'; -export { WebView, getWebViewProxy}; +export { WebView, WebViewTarget, getWebViewProxy}; export default WebView; diff --git a/lib/WebViewTarget.d.ts b/lib/WebViewTarget.d.ts new file mode 100644 index 0000000000..46942ff3a7 --- /dev/null +++ b/lib/WebViewTarget.d.ts @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { StyleProp, ViewStyle } from 'react-native'; +export interface WebViewTargetProps { + webViewKey: string; + temporaryParentNodeTag?: number; + style?: StyleProp; +} +export default function WebViewTarget(props: WebViewTargetProps): React.ReactElement; diff --git a/lib/WebViewTarget.js b/lib/WebViewTarget.js new file mode 100644 index 0000000000..6cc80830da --- /dev/null +++ b/lib/WebViewTarget.js @@ -0,0 +1,6 @@ +import * as React from 'react'; +import { requireNativeComponent } from "react-native"; +const RNCWebViewTarget = requireNativeComponent('RNCWebViewTarget'); +export default function WebViewTarget(props) { + return React.createElement(RNCWebViewTarget, props); +} diff --git a/src/WebViewTarget.tsx b/src/WebViewTarget.tsx new file mode 100644 index 0000000000..fe9adf2043 --- /dev/null +++ b/src/WebViewTarget.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { requireNativeComponent, StyleProp, ViewStyle } from 'react-native'; + +export interface WebViewTargetProps { + /** + * The webViewKey of a WebView that already exists — one built by a with the same key. This target + * borrows that native view and displays it here. + */ + webViewKey: string; + /** + * React tag of the view the WebView should be returned to when this target unmounts, so it survives for the + * next target instead of being torn down. Typically the shared temporary-parent placeholder. + */ + temporaryParentNodeTag?: number; + style?: StyleProp; +} + +const RNCWebViewTarget = requireNativeComponent('RNCWebViewTarget'); + +/** + * Displays a WebView that already exists and is owned elsewhere. Where builds, configures, and owns a + * native WebView (keyed by webViewKey), only borrows one by that key and reparents it here to + * show it. It never sets a source, configuration, delegates, or message handlers, so the owning keeps + * the configuration and the message/callback stream while this target merely displays the view. This lets a + * single pooled WebView be shown in different places over time — moved rather than rebuilt — with no reload. + */ +export default function WebViewTarget(props: WebViewTargetProps) { + return ; +} diff --git a/src/index.ts b/src/index.ts index f335a5234e..8531c66140 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ import WebView from './WebView'; +import WebViewTarget from './WebViewTarget'; import getWebViewProxy from './getWebViewProxy'; -export { WebView, getWebViewProxy}; +export type { WebViewTargetProps } from './WebViewTarget'; +export { WebView, WebViewTarget, getWebViewProxy}; export default WebView; From 3897e8317d60989aaeb11689fc32ce625df28d30 Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Wed, 30 Sep 2026 16:57:04 -0400 Subject: [PATCH 02/13] update CI scripts --- .github/workflows/android-ci.yml | 20 ++++++++++---------- .github/workflows/detox.yml | 8 ++++---- .github/workflows/ios-ci.yml | 10 +++++----- .github/workflows/macos-ci.yml | 10 +++++----- .github/workflows/stale.yml | 6 +++--- .github/workflows/windows-ci.yml | 12 ++++++------ 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index fbaba471ac..044f4a6ea0 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -12,28 +12,28 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Set up Node.js - uses: actions/setup-node@v2.5.1 + uses: actions/setup-node@v7 with: - node-version: 16 + node-version: 22 - name: Set up JDK - uses: actions/setup-java@v2.5.0 + uses: actions/setup-java@v6 with: distribution: temurin java-version: 11 - name: Cache /node_modules - uses: actions/cache@v2 + uses: actions/cache@v6 with: path: node_modules key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - name: Install npm dependencies run: yarn --frozen-lockfile shell: bash + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 - name: Build Android test app - uses: gradle/gradle-build-action@v2.1.3 - with: - gradle-version: wrapper - arguments: --no-daemon clean build check test - build-root-directory: example/android + run: ./gradlew --no-daemon clean build check test + shell: bash + working-directory: example/android timeout-minutes: 60 diff --git a/.github/workflows/detox.yml b/.github/workflows/detox.yml index a1b67f9b80..2479743f9b 100644 --- a/.github/workflows/detox.yml +++ b/.github/workflows/detox.yml @@ -4,12 +4,12 @@ on: [pull_request] jobs: tests: name: 'Running tests' - runs-on: 'macOS-latest' + runs-on: macos-15 steps: - - uses: actions/checkout@master - - uses: actions/setup-node@master + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: - node-version: 14 + node-version: 22 - name: Setup - Install Yarn run: YARN_GPG=NO curl -o- -L https://yarnpkg.com/install.sh | bash - name: Setup - Install NPM Dependencies diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index 997e05d607..9a8c6188a1 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -6,16 +6,16 @@ on: pull_request: jobs: build: - runs-on: macos-latest + runs-on: macos-15 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Set up Node.js - uses: actions/setup-node@v2.5.1 + uses: actions/setup-node@v7 with: - node-version: 16 + node-version: 22 - name: Cache /node_modules - uses: actions/cache@v2 + uses: actions/cache@v6 with: path: node_modules key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index d8f1c959ad..eec33ae2ba 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -6,16 +6,16 @@ on: pull_request: jobs: build: - runs-on: macos-latest + runs-on: macos-15 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Set up Node.js - uses: actions/setup-node@v2.5.1 + uses: actions/setup-node@v7 with: - node-version: 16 + node-version: 22 - name: Cache /node_modules - uses: actions/cache@v2 + uses: actions/cache@v6 with: path: node_modules key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ebee3eb173..b1559e5961 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -7,11 +7,11 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v3.0.14 + - uses: actions/stale@v11 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'Hello 👋, this issue has been opened for more than 2 months with no activity on it. If the issue is still here, please keep in mind that we need community support and help to fix it! Just comment something like _still searching for solutions_ and if you found one, please open a pull request! You have 7 days until this gets closed automatically' stale-pr-message: 'Hello 👋, this PR has been opened for more than 2 months with no activity on it. If you think this is a mistake please comment and ping a maintainer to get this merged ASAP! Thanks for contributing! You have 7 days until this gets closed automatically' - exempt-issue-label: 'Keep opened' - exempt-pr-label: 'Keep opened' + exempt-issue-labels: 'Keep opened' + exempt-pr-labels: 'Keep opened' remove-stale-when-updated: true diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index d56326a0a4..6497f13c67 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -4,22 +4,22 @@ on: [pull_request] jobs: run-windows-tests: name: Build & run tests - runs-on: windows-2019 + runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v7 name: Checkout Code - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v7 with: - node-version: '^14' + node-version: 22 - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1.0.2 + uses: microsoft/setup-msbuild@v3 - name: Check node modules cache - uses: actions/cache@v1 + uses: actions/cache@v6 id: yarn-cache with: path: ./node_modules From fee9ccc74343b94b90938b91809dceb7e564dfdb Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Wed, 30 Sep 2026 17:16:21 -0400 Subject: [PATCH 03/13] [ci] Fix lint, boost pod, Gradle wrapper, and Windows Node failures Allow underscore-prefixed unused args in eslint so the platform no-op stubs for releaseWebView and injectJavaScriptWithWebViewKey pass lint. Point the RN 0.68 boost podspec at archives.boost.io before pod install on iOS and macOS, since the JFrog URL it ships with no longer serves the tarball. Run setup-gradle before node_modules is installed so wrapper validation only covers our own wrapper, not unknown JARs bundled in dependencies. Pin Windows CI to Node 20.12.1, the last release before Node started rejecting .cmd spawns without a shell, which the RNW 0.68 CLI relies on. Generated on: M2M-AKeener Co-Authored-By: Claude Opus 5.5 --- .eslintrc.js | 2 +- .github/workflows/android-ci.yml | 7 +++++-- .github/workflows/ios-ci.yml | 4 ++++ .github/workflows/macos-ci.yml | 4 ++++ .github/workflows/windows-ci.yml | 5 ++++- 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 28ec2643ca..ba0ae7c59a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -62,7 +62,7 @@ module.exports = { ], '@typescript-eslint/no-unused-vars': [ 'error', - { ignoreRestSiblings: true}, + { ignoreRestSiblings: true, argsIgnorePattern: '^_' }, ], '@typescript-eslint/consistent-type-definitions': [ 'error', diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index 044f4a6ea0..d6ef407ccc 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -22,6 +22,11 @@ jobs: with: distribution: temurin java-version: 11 + # setup-gradle validates every Gradle wrapper JAR in the workspace. Running it + # before node_modules exists keeps it to our own wrapper, since some packages + # ship wrapper JARs that aren't on Gradle's list of known checksums. + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 - name: Cache /node_modules uses: actions/cache@v6 with: @@ -30,8 +35,6 @@ jobs: - name: Install npm dependencies run: yarn --frozen-lockfile shell: bash - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v6 - name: Build Android test app run: ./gradlew --no-daemon clean build check test shell: bash diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index 9a8c6188a1..e972b4016d 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -21,6 +21,10 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - name: Install npm dependencies run: yarn --frozen-lockfile + # Boost moved its release archives off JFrog, so the old URL in this podspec + # no longer serves the tarball and pod install fails the checksum check. + - name: Point boost podspec at archives.boost.io + run: sed -i '' 's#https://boostorg.jfrog.io/artifactory/main/release/#https://archives.boost.io/release/#' node_modules/react-native/third-party-podspecs/boost.podspec - name: Install Pods run: pod install working-directory: example/ios diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index eec33ae2ba..d4e2c9db4c 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -21,6 +21,10 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - name: Install npm dependencies run: yarn --frozen-lockfile + # Boost moved its release archives off JFrog, so the old URL in this podspec + # no longer serves the tarball and pod install fails the checksum check. + - name: Point boost podspec at archives.boost.io + run: sed -i '' 's#https://boostorg.jfrog.io/artifactory/main/release/#https://archives.boost.io/release/#' node_modules/react-native-macos/third-party-podspecs/boost.podspec - name: Install Pods run: pod install working-directory: example/macos diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 6497f13c67..4af6fb3a56 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -10,10 +10,13 @@ jobs: - uses: actions/checkout@v7 name: Checkout Code + # The react-native-windows 0.68 CLI spawns .cmd files without a shell, which Node + # rejects with EINVAL starting in 20.12.2 (CVE-2024-27980). 20.12.1 is the newest + # release that still allows it. - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: 22 + node-version: 20.12.1 - name: Setup MSBuild uses: microsoft/setup-msbuild@v3 From 6eee9a0196335eb2c948a685d422d46a94c1423d Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Wed, 30 Sep 2026 17:22:37 -0400 Subject: [PATCH 04/13] fix types --- src/addOnMessageListenerWithWebViewKey.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/addOnMessageListenerWithWebViewKey.ts b/src/addOnMessageListenerWithWebViewKey.ts index 715ef8d423..5cec9f0658 100644 --- a/src/addOnMessageListenerWithWebViewKey.ts +++ b/src/addOnMessageListenerWithWebViewKey.ts @@ -1,4 +1,9 @@ import { NativeModules, Platform, EmitterSubscription, NativeEventEmitter } from "react-native"; +import { WebViewMessage } from './WebViewTypes'; + +export interface WebViewMessageWithWebViewKey extends WebViewMessage { + webViewKey: string; +} const scriptMessageEmitter = new NativeEventEmitter( Platform.select({ @@ -7,8 +12,8 @@ const scriptMessageEmitter = new NativeEventEmitter( }) ); -export default function addOnMessageListenerWithWebViewKey(webViewKey: string, listener: (event: any) => void): EmitterSubscription { - return scriptMessageEmitter.addListener('ReactNativeWebViewOnMessageWithWebViewKey', (eventData) => { +export default function addOnMessageListenerWithWebViewKey(webViewKey: string, listener: (event: WebViewMessageWithWebViewKey) => void): EmitterSubscription { + return scriptMessageEmitter.addListener('ReactNativeWebViewOnMessageWithWebViewKey', (eventData: WebViewMessageWithWebViewKey) => { if (eventData.webViewKey === webViewKey) { listener(eventData); } From 9686096da5f5ef6a7f88346cc68430d738b1c2ea Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Thu, 1 Oct 2026 12:05:11 -0400 Subject: [PATCH 05/13] [ci] Fix Android compile, Yoga build, iOS destination, and Windows SDK Assign getEventDispatcher() to a typed EventDispatcher local before dispatching. RN 0.68 declares it as a generic T, so chaining the call inferred Object and failed to compile; newer RN returns EventDispatcher directly, so this works on both. Change the two boolean | operators in RN 0.68's Yoga to || before pod install on iOS and macOS. Current Xcode warns on them and Yoga builds with -Werror. Build iOS for the generic simulator destination instead of looking up an iPhone 13, which the macos-15 image doesn't ship. Pin Windows CI to windows-2022, which has Windows SDK 10.0.19041.0 that react-native-windows 0.68 targets. windows-latest only has 26100. Generated on: M2M-AKeener Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ios-ci.yml | 10 +++++----- .github/workflows/macos-ci.yml | 4 ++++ .github/workflows/windows-ci.yml | 2 +- .../webview/RNCWebViewManager.java | 5 ++++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index e972b4016d..3f158e807c 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -25,15 +25,15 @@ jobs: # no longer serves the tarball and pod install fails the checksum check. - name: Point boost podspec at archives.boost.io run: sed -i '' 's#https://boostorg.jfrog.io/artifactory/main/release/#https://archives.boost.io/release/#' node_modules/react-native/third-party-podspecs/boost.podspec + # Newer Xcode warns about bitwise | on booleans, and Yoga's podspec builds with + # -Werror, so two hadOverflow() lines in RN 0.68's Yoga break the build. + - name: Fix Yoga bitwise-or for current Xcode + run: sed -i '' 's/getLayout().hadOverflow() |$/getLayout().hadOverflow() ||/' node_modules/react-native/ReactCommon/yoga/yoga/Yoga.cpp - name: Install Pods run: pod install working-directory: example/ios - name: Build iOS test app run: | - device_name='iPhone 13' - device=$(xcrun simctl list devices "${device_name}" available | grep "${device_name} (") - re='\(([-0-9A-Fa-f]+)\)' - [[ $device =~ $re ]] || exit 1 - xcodebuild -workspace WebviewExample.xcworkspace -scheme ReactTestApp -destination "platform=iOS Simulator,id=${BASH_REMATCH[1]}" CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO build + xcodebuild -workspace WebviewExample.xcworkspace -scheme ReactTestApp -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO build working-directory: example/ios timeout-minutes: 60 diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index d4e2c9db4c..72d8510c67 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -25,6 +25,10 @@ jobs: # no longer serves the tarball and pod install fails the checksum check. - name: Point boost podspec at archives.boost.io run: sed -i '' 's#https://boostorg.jfrog.io/artifactory/main/release/#https://archives.boost.io/release/#' node_modules/react-native-macos/third-party-podspecs/boost.podspec + # Newer Xcode warns about bitwise | on booleans, and Yoga's podspec builds with + # -Werror, so two hadOverflow() lines in RN 0.68's Yoga break the build. + - name: Fix Yoga bitwise-or for current Xcode + run: sed -i '' 's/getLayout().hadOverflow() |$/getLayout().hadOverflow() ||/' node_modules/react-native-macos/ReactCommon/yoga/yoga/Yoga.cpp - name: Install Pods run: pod install working-directory: example/macos diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 4af6fb3a56..04c2d6a4b7 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -4,7 +4,7 @@ on: [pull_request] jobs: run-windows-tests: name: Build & run tests - runs-on: windows-latest + runs-on: windows-2022 steps: - uses: actions/checkout@v7 diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index c8e8386f35..2f55825cc9 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -2076,7 +2076,10 @@ protected void dispatchEvent(WebView webView, Event event) { return; } - uiManager.getEventDispatcher().dispatchEvent(event); + // Older React Native declares getEventDispatcher() as a generic T, so we + // assign it to a typed local to give Java something to infer T from. + EventDispatcher eventDispatcher = uiManager.getEventDispatcher(); + eventDispatcher.dispatchEvent(event); } protected void cleanupCallbacksAndDestroy() { From 6ebe1977e018ab326b19c934551798c73a6f73c7 Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Thu, 1 Oct 2026 12:43:53 -0400 Subject: [PATCH 06/13] [ci] Restore std::unary_function for boost and tag RN 0.68 workarounds Pass _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION to xcodebuild on iOS and macOS. Boost 1.76 uses std::unary_function, which Xcode 16's libc++ drops in C++17 mode, so RCT-Folly failed to compile. It goes on the xcodebuild command line because react-native-test-app owns the Podfile's post_install hook. Tag every workaround that only exists for the RN 0.68 example app with TODO(rn-upgrade) so they can be found and removed during the upgrade. Generated on: M2M-AKeener Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ios-ci.yml | 8 +++++++- .github/workflows/macos-ci.yml | 8 +++++++- .github/workflows/windows-ci.yml | 3 +++ .../reactnativecommunity/webview/RNCWebViewManager.java | 1 + 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index 3f158e807c..d3c326ac95 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -21,10 +21,12 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - name: Install npm dependencies run: yarn --frozen-lockfile + # TODO(rn-upgrade): Remove once the example app is on RN 0.86, which already fixes this. # Boost moved its release archives off JFrog, so the old URL in this podspec # no longer serves the tarball and pod install fails the checksum check. - name: Point boost podspec at archives.boost.io run: sed -i '' 's#https://boostorg.jfrog.io/artifactory/main/release/#https://archives.boost.io/release/#' node_modules/react-native/third-party-podspecs/boost.podspec + # TODO(rn-upgrade): Remove once the example app is on RN 0.86, which already fixes this. # Newer Xcode warns about bitwise | on booleans, and Yoga's podspec builds with # -Werror, so two hadOverflow() lines in RN 0.68's Yoga break the build. - name: Fix Yoga bitwise-or for current Xcode @@ -32,8 +34,12 @@ jobs: - name: Install Pods run: pod install working-directory: example/ios + # TODO(rn-upgrade): Remove once the example app is on RN 0.86, which already fixes this. + # Boost 1.76 uses std::unary_function, which Xcode 16's libc++ drops in C++17 + # mode. This define brings it back for every target, and $(inherited) keeps + # each target's own preprocessor definitions. - name: Build iOS test app run: | - xcodebuild -workspace WebviewExample.xcworkspace -scheme ReactTestApp -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO build + xcodebuild -workspace WebviewExample.xcworkspace -scheme ReactTestApp -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO GCC_PREPROCESSOR_DEFINITIONS='$(inherited) _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION' build working-directory: example/ios timeout-minutes: 60 diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index 72d8510c67..57e8b30abf 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -21,10 +21,12 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - name: Install npm dependencies run: yarn --frozen-lockfile + # TODO(rn-upgrade): Remove once the example app is on RN 0.86, which already fixes this. # Boost moved its release archives off JFrog, so the old URL in this podspec # no longer serves the tarball and pod install fails the checksum check. - name: Point boost podspec at archives.boost.io run: sed -i '' 's#https://boostorg.jfrog.io/artifactory/main/release/#https://archives.boost.io/release/#' node_modules/react-native-macos/third-party-podspecs/boost.podspec + # TODO(rn-upgrade): Remove once the example app is on RN 0.86, which already fixes this. # Newer Xcode warns about bitwise | on booleans, and Yoga's podspec builds with # -Werror, so two hadOverflow() lines in RN 0.68's Yoga break the build. - name: Fix Yoga bitwise-or for current Xcode @@ -32,8 +34,12 @@ jobs: - name: Install Pods run: pod install working-directory: example/macos + # TODO(rn-upgrade): Remove once the example app is on RN 0.86, which already fixes this. + # Boost 1.76 uses std::unary_function, which Xcode 16's libc++ drops in C++17 + # mode. This define brings it back for every target, and $(inherited) keeps + # each target's own preprocessor definitions. - name: Build macOS test app run: | - xcodebuild -workspace WebviewExample.xcworkspace -scheme ReactTestApp CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO build + xcodebuild -workspace WebviewExample.xcworkspace -scheme ReactTestApp CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO GCC_PREPROCESSOR_DEFINITIONS='$(inherited) _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION' build working-directory: example/macos timeout-minutes: 60 diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 04c2d6a4b7..b853c9b431 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -4,12 +4,15 @@ on: [pull_request] jobs: run-windows-tests: name: Build & run tests + # TODO(rn-upgrade): windows-2022 has the Windows SDK 10.0.19041.0 that react-native-windows + # 0.68 targets. Revisit once react-native-windows is upgraded. runs-on: windows-2022 steps: - uses: actions/checkout@v7 name: Checkout Code + # TODO(rn-upgrade): Remove once react-native-windows is upgraded. # The react-native-windows 0.68 CLI spawns .cmd files without a shell, which Node # rejects with EINVAL starting in 20.12.2 (CVE-2024-27980). 20.12.1 is the newest # release that still allows it. diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index 2f55825cc9..bd1268a430 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -2076,6 +2076,7 @@ protected void dispatchEvent(WebView webView, Event event) { return; } + // TODO(rn-upgrade): Once the example app is on RN 0.86, this can be chained again. // Older React Native declares getEventDispatcher() as a generic T, so we // assign it to a typed local to give Java something to infer T from. EventDispatcher eventDispatcher = uiManager.getEventDispatcher(); From a8dc25cb10f54d2a860b184c50898eb5392ac4d7 Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Thu, 1 Oct 2026 12:56:06 -0400 Subject: [PATCH 07/13] [macos] Make WebViewTarget a no-op on macOS and Windows RNCWebViewTarget and its manager use UIKit-only view APIs (UIView, didMoveToWindow, layoutSubviews, UIViewAutoresizing), so the macOS build failed to compile them. Guard both implementations with !TARGET_OS_OSX, keeping the RNCWebViewDidRegisterNotification constant outside the guard since RNCWebView posts it on every platform. Add WebViewTarget.macos.tsx and WebViewTarget.windows.tsx stubs that render nothing, matching the existing no-op stubs for the other webViewKey features, so requireNativeComponent isn't called for a native component those platforms don't register. Generated on: M2M-AKeener Co-Authored-By: Claude Opus 5.5 --- apple/RNCWebViewTarget.m | 5 +++++ apple/RNCWebViewTargetManager.m | 5 +++++ src/WebViewTarget.macos.tsx | 6 ++++++ src/WebViewTarget.windows.tsx | 6 ++++++ 4 files changed, 22 insertions(+) create mode 100644 src/WebViewTarget.macos.tsx create mode 100644 src/WebViewTarget.windows.tsx diff --git a/apple/RNCWebViewTarget.m b/apple/RNCWebViewTarget.m index 5cdfa37d8d..6d4004bea7 100644 --- a/apple/RNCWebViewTarget.m +++ b/apple/RNCWebViewTarget.m @@ -6,6 +6,9 @@ NSString *const RNCWebViewDidRegisterNotification = @"RNCWebViewDidRegister"; +// WebViewTarget relies on UIKit view APIs, so like the other webViewKey features it's a no-op on macOS. +#if !TARGET_OS_OSX + @implementation RNCWebViewTarget - (instancetype)initWithFrame:(CGRect)frame @@ -118,3 +121,5 @@ - (void)removeFromSuperview } @end + +#endif // !TARGET_OS_OSX diff --git a/apple/RNCWebViewTargetManager.m b/apple/RNCWebViewTargetManager.m index 7fb38a59d4..fb54193e98 100644 --- a/apple/RNCWebViewTargetManager.m +++ b/apple/RNCWebViewTargetManager.m @@ -1,6 +1,9 @@ #import "RNCWebViewTargetManager.h" #import "RNCWebViewTarget.h" +// WebViewTarget relies on UIKit view APIs, so like the other webViewKey features it's a no-op on macOS. +#if !TARGET_OS_OSX + @implementation RNCWebViewTargetManager RCT_EXPORT_MODULE() @@ -16,3 +19,5 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(temporaryParentNodeTag, NSNumber) @end + +#endif // !TARGET_OS_OSX diff --git a/src/WebViewTarget.macos.tsx b/src/WebViewTarget.macos.tsx new file mode 100644 index 0000000000..72e1752bcf --- /dev/null +++ b/src/WebViewTarget.macos.tsx @@ -0,0 +1,6 @@ +import { WebViewTargetProps } from './WebViewTarget'; + +export default function WebViewTarget(_props: WebViewTargetProps) { + // no-op + return null; +} diff --git a/src/WebViewTarget.windows.tsx b/src/WebViewTarget.windows.tsx new file mode 100644 index 0000000000..72e1752bcf --- /dev/null +++ b/src/WebViewTarget.windows.tsx @@ -0,0 +1,6 @@ +import { WebViewTargetProps } from './WebViewTarget'; + +export default function WebViewTarget(_props: WebViewTargetProps) { + // no-op + return null; +} From 14827b25252227c8bee13132a85d1023356781e1 Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Thu, 1 Oct 2026 13:07:58 -0400 Subject: [PATCH 08/13] [macos] Use NSView for releaseWebView's parent view on macOS releaseWebView typed the WKWebView's superview as UIView, which doesn't exist on macOS, so RNCWebViewManager.m failed to compile there. Switch on TARGET_OS_OSX like the manager's view method already does. Generated on: M2M-AKeener Co-Authored-By: Claude Opus 5.5 --- apple/RNCWebViewManager.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apple/RNCWebViewManager.m b/apple/RNCWebViewManager.m index 24bb78e1e3..25bb2490b1 100644 --- a/apple/RNCWebViewManager.m +++ b/apple/RNCWebViewManager.m @@ -322,7 +322,11 @@ - (BOOL) webView:(RNCWebView *)webView WKWebView *wkWebView = sharedWKWebViewDictionary[webViewKey]; if (wkWebView != nil) { +#if !TARGET_OS_OSX UIView *parentView = wkWebView.superview; +#else + NSView *parentView = wkWebView.superview; +#endif // !TARGET_OS_OSX if (parentView != nil) { if ([parentView isKindOfClass:[RNCWebView class]]) { [(RNCWebView*)parentView cleanUpWebView]; From 7d4f5a4e4f46319642c41f29ef360564f46bb7a6 Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Thu, 1 Oct 2026 13:20:29 -0400 Subject: [PATCH 09/13] Revert "[macos] Use NSView for releaseWebView's parent view on macOS" This reverts commit 14827b25252227c8bee13132a85d1023356781e1. --- apple/RNCWebViewManager.m | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apple/RNCWebViewManager.m b/apple/RNCWebViewManager.m index 25bb2490b1..24bb78e1e3 100644 --- a/apple/RNCWebViewManager.m +++ b/apple/RNCWebViewManager.m @@ -322,11 +322,7 @@ - (BOOL) webView:(RNCWebView *)webView WKWebView *wkWebView = sharedWKWebViewDictionary[webViewKey]; if (wkWebView != nil) { -#if !TARGET_OS_OSX UIView *parentView = wkWebView.superview; -#else - NSView *parentView = wkWebView.superview; -#endif // !TARGET_OS_OSX if (parentView != nil) { if ([parentView isKindOfClass:[RNCWebView class]]) { [(RNCWebView*)parentView cleanUpWebView]; From c51a1c651947f30d35a648ec599e22f05e4ba165 Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Thu, 1 Oct 2026 13:20:29 -0400 Subject: [PATCH 10/13] Revert "[macos] Make WebViewTarget a no-op on macOS and Windows" This reverts commit a8dc25cb10f54d2a860b184c50898eb5392ac4d7. --- apple/RNCWebViewTarget.m | 5 ----- apple/RNCWebViewTargetManager.m | 5 ----- src/WebViewTarget.macos.tsx | 6 ------ src/WebViewTarget.windows.tsx | 6 ------ 4 files changed, 22 deletions(-) delete mode 100644 src/WebViewTarget.macos.tsx delete mode 100644 src/WebViewTarget.windows.tsx diff --git a/apple/RNCWebViewTarget.m b/apple/RNCWebViewTarget.m index 6d4004bea7..5cdfa37d8d 100644 --- a/apple/RNCWebViewTarget.m +++ b/apple/RNCWebViewTarget.m @@ -6,9 +6,6 @@ NSString *const RNCWebViewDidRegisterNotification = @"RNCWebViewDidRegister"; -// WebViewTarget relies on UIKit view APIs, so like the other webViewKey features it's a no-op on macOS. -#if !TARGET_OS_OSX - @implementation RNCWebViewTarget - (instancetype)initWithFrame:(CGRect)frame @@ -121,5 +118,3 @@ - (void)removeFromSuperview } @end - -#endif // !TARGET_OS_OSX diff --git a/apple/RNCWebViewTargetManager.m b/apple/RNCWebViewTargetManager.m index fb54193e98..7fb38a59d4 100644 --- a/apple/RNCWebViewTargetManager.m +++ b/apple/RNCWebViewTargetManager.m @@ -1,9 +1,6 @@ #import "RNCWebViewTargetManager.h" #import "RNCWebViewTarget.h" -// WebViewTarget relies on UIKit view APIs, so like the other webViewKey features it's a no-op on macOS. -#if !TARGET_OS_OSX - @implementation RNCWebViewTargetManager RCT_EXPORT_MODULE() @@ -19,5 +16,3 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(temporaryParentNodeTag, NSNumber) @end - -#endif // !TARGET_OS_OSX diff --git a/src/WebViewTarget.macos.tsx b/src/WebViewTarget.macos.tsx deleted file mode 100644 index 72e1752bcf..0000000000 --- a/src/WebViewTarget.macos.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { WebViewTargetProps } from './WebViewTarget'; - -export default function WebViewTarget(_props: WebViewTargetProps) { - // no-op - return null; -} diff --git a/src/WebViewTarget.windows.tsx b/src/WebViewTarget.windows.tsx deleted file mode 100644 index 72e1752bcf..0000000000 --- a/src/WebViewTarget.windows.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { WebViewTargetProps } from './WebViewTarget'; - -export default function WebViewTarget(_props: WebViewTargetProps) { - // no-op - return null; -} From c5cc2a61e08fa152e02fc89b758e302ad40da886 Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Thu, 1 Oct 2026 13:20:38 -0400 Subject: [PATCH 11/13] [ci] Remove macOS CI workflow Discord doesn't ship this fork on macOS, and the macOS build hasn't compiled for a long time: several webViewKey code paths use UIKit-only APIs with no macOS branch. Rather than maintain macOS support nobody uses, drop the workflow so CI covers the platforms Discord ships. Generated on: M2M-AKeener Co-Authored-By: Claude Opus 5.5 --- .github/workflows/macos-ci.yml | 45 ---------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 .github/workflows/macos-ci.yml diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml deleted file mode 100644 index 57e8b30abf..0000000000 --- a/.github/workflows/macos-ci.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: macOS -on: - push: - branches: - - master - pull_request: -jobs: - build: - runs-on: macos-15 - steps: - - name: Checkout - uses: actions/checkout@v7 - - name: Set up Node.js - uses: actions/setup-node@v7 - with: - node-version: 22 - - name: Cache /node_modules - uses: actions/cache@v6 - with: - path: node_modules - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - - name: Install npm dependencies - run: yarn --frozen-lockfile - # TODO(rn-upgrade): Remove once the example app is on RN 0.86, which already fixes this. - # Boost moved its release archives off JFrog, so the old URL in this podspec - # no longer serves the tarball and pod install fails the checksum check. - - name: Point boost podspec at archives.boost.io - run: sed -i '' 's#https://boostorg.jfrog.io/artifactory/main/release/#https://archives.boost.io/release/#' node_modules/react-native-macos/third-party-podspecs/boost.podspec - # TODO(rn-upgrade): Remove once the example app is on RN 0.86, which already fixes this. - # Newer Xcode warns about bitwise | on booleans, and Yoga's podspec builds with - # -Werror, so two hadOverflow() lines in RN 0.68's Yoga break the build. - - name: Fix Yoga bitwise-or for current Xcode - run: sed -i '' 's/getLayout().hadOverflow() |$/getLayout().hadOverflow() ||/' node_modules/react-native-macos/ReactCommon/yoga/yoga/Yoga.cpp - - name: Install Pods - run: pod install - working-directory: example/macos - # TODO(rn-upgrade): Remove once the example app is on RN 0.86, which already fixes this. - # Boost 1.76 uses std::unary_function, which Xcode 16's libc++ drops in C++17 - # mode. This define brings it back for every target, and $(inherited) keeps - # each target's own preprocessor definitions. - - name: Build macOS test app - run: | - xcodebuild -workspace WebviewExample.xcworkspace -scheme ReactTestApp CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO GCC_PREPROCESSOR_DEFINITIONS='$(inherited) _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION' build - working-directory: example/macos - timeout-minutes: 60 From 0db1085b270b33833f2bfb6f93489d92375a6e1b Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Thu, 1 Oct 2026 13:22:24 -0400 Subject: [PATCH 12/13] Revert "[ci] Remove macOS CI workflow" This reverts commit c5cc2a61e08fa152e02fc89b758e302ad40da886. --- .github/workflows/macos-ci.yml | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/macos-ci.yml diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml new file mode 100644 index 0000000000..57e8b30abf --- /dev/null +++ b/.github/workflows/macos-ci.yml @@ -0,0 +1,45 @@ +name: macOS +on: + push: + branches: + - master + pull_request: +jobs: + build: + runs-on: macos-15 + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 22 + - name: Cache /node_modules + uses: actions/cache@v6 + with: + path: node_modules + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + - name: Install npm dependencies + run: yarn --frozen-lockfile + # TODO(rn-upgrade): Remove once the example app is on RN 0.86, which already fixes this. + # Boost moved its release archives off JFrog, so the old URL in this podspec + # no longer serves the tarball and pod install fails the checksum check. + - name: Point boost podspec at archives.boost.io + run: sed -i '' 's#https://boostorg.jfrog.io/artifactory/main/release/#https://archives.boost.io/release/#' node_modules/react-native-macos/third-party-podspecs/boost.podspec + # TODO(rn-upgrade): Remove once the example app is on RN 0.86, which already fixes this. + # Newer Xcode warns about bitwise | on booleans, and Yoga's podspec builds with + # -Werror, so two hadOverflow() lines in RN 0.68's Yoga break the build. + - name: Fix Yoga bitwise-or for current Xcode + run: sed -i '' 's/getLayout().hadOverflow() |$/getLayout().hadOverflow() ||/' node_modules/react-native-macos/ReactCommon/yoga/yoga/Yoga.cpp + - name: Install Pods + run: pod install + working-directory: example/macos + # TODO(rn-upgrade): Remove once the example app is on RN 0.86, which already fixes this. + # Boost 1.76 uses std::unary_function, which Xcode 16's libc++ drops in C++17 + # mode. This define brings it back for every target, and $(inherited) keeps + # each target's own preprocessor definitions. + - name: Build macOS test app + run: | + xcodebuild -workspace WebviewExample.xcworkspace -scheme ReactTestApp CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO GCC_PREPROCESSOR_DEFINITIONS='$(inherited) _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION' build + working-directory: example/macos + timeout-minutes: 60 From 9d113cbda320c3c1f73128f50bd3ee26ad8edc43 Mon Sep 17 00:00:00 2001 From: Austin Keener Date: Thu, 1 Oct 2026 13:22:34 -0400 Subject: [PATCH 13/13] [ci] Run macOS CI only on manual dispatch Keep the macOS workflow but stop it running on pushes and pull requests. Discord doesn't ship this fork on macOS, and the macOS build doesn't compile because several webViewKey code paths use UIKit-only APIs. It can still be run by hand from the Actions tab. Generated on: M2M-AKeener Co-Authored-By: Claude Opus 5.5 --- .github/workflows/macos-ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index 57e8b30abf..6bc365243f 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -1,9 +1,9 @@ name: macOS +# Discord doesn't ship this fork on macOS, and several webViewKey code paths use UIKit-only APIs +# with no macOS branch, so this build doesn't compile. It only runs when triggered by hand from +# the Actions tab until someone takes on macOS support. on: - push: - branches: - - master - pull_request: + workflow_dispatch: jobs: build: runs-on: macos-15