From 9dc542af1466ae40abef9a810e3419d42f8f49bf Mon Sep 17 00:00:00 2001 From: Darius Costolas <10818970+meltingice1337@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:18:03 +0300 Subject: [PATCH 1/2] feat: add additionalMessageHandlers prop to iOS WebView --- CHANGELOG.md | 4 ++ apple/RNCWebView.mm | 9 +++ apple/RNCWebViewImpl.h | 1 + apple/RNCWebViewImpl.m | 51 +++++++++++++++- apple/RNCWebViewManager.mm | 1 + docs/Reference.md | 21 +++++++ example/App.tsx | 19 +++++- .../examples/AdditionalMessageHandlers.tsx | 60 +++++++++++++++++++ src/RNCWebViewNativeComponent.ts | 1 + src/WebViewTypes.ts | 13 ++++ 10 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 example/examples/AdditionalMessageHandlers.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 421eaaf5c3..15ea3a8254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- add `additionalMessageHandlerNames` prop (iOS) to register extra native script message handlers that forward to `onMessage`, including in Apple Pay mode + ## [14.6.0] ### Added diff --git a/apple/RNCWebView.mm b/apple/RNCWebView.mm index e7ca571f05..3af1f5fd2b 100644 --- a/apple/RNCWebView.mm +++ b/apple/RNCWebView.mm @@ -402,6 +402,15 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & [_view setSuppressMenuItems:suppressMenuItems]; } + if (oldViewProps.additionalMessageHandlerNames != newViewProps.additionalMessageHandlerNames) { + NSMutableArray *handlerNames = [NSMutableArray array]; + + for (const auto &handlerName: newViewProps.additionalMessageHandlerNames) { + [handlerNames addObject: RCTNSStringFromString(handlerName)]; + } + + [_view setAdditionalMessageHandlerNames:handlerNames]; + } if (oldViewProps.hasOnFileDownload != newViewProps.hasOnFileDownload) { if (newViewProps.hasOnFileDownload) { _view.onFileDownload = [self](NSDictionary* dictionary) { diff --git a/apple/RNCWebViewImpl.h b/apple/RNCWebViewImpl.h index 49bbb36940..a5daadd277 100644 --- a/apple/RNCWebViewImpl.h +++ b/apple/RNCWebViewImpl.h @@ -112,6 +112,7 @@ shouldStartLoadForRequest:(NSMutableDictionary *)request @property (nonatomic, assign) BOOL enableApplePay; @property (nonatomic, copy) NSArray * _Nullable menuItems; @property (nonatomic, copy) NSArray * _Nullable suppressMenuItems; +@property (nonatomic, copy) NSArray * _Nullable additionalMessageHandlerNames; @property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection; #if !TARGET_OS_OSX @property (nonatomic, assign) WKDataDetectorTypes dataDetectorTypes; diff --git a/apple/RNCWebViewImpl.m b/apple/RNCWebViewImpl.m index 46c6ebfefd..a631557cc1 100644 --- a/apple/RNCWebViewImpl.m +++ b/apple/RNCWebViewImpl.m @@ -53,6 +53,7 @@ @interface RNCWKWebView : WKWebView #if !TARGET_OS_OSX @property (nonatomic, copy) NSArray * _Nullable menuItems; @property (nonatomic, copy) NSArray * _Nullable suppressMenuItems; +@property (nonatomic, copy) NSArray * _Nullable additionalMessageHandlerNames; #endif // !TARGET_OS_OSX @end @implementation RNCWKWebView @@ -618,6 +619,7 @@ - (void)removeFromSuperview if (_webView) { [_webView.configuration.userContentController removeScriptMessageHandlerForName:HistoryShimName]; [_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHandlerName]; + [self removeAdditionalMessageHandlers:_webView.configuration]; [_webView removeObserver:self forKeyPath:@"estimatedProgress"]; [_webView removeFromSuperview]; if (@available(iOS 15.0, macOS 12.0, *)) { @@ -788,16 +790,58 @@ - (void)userContentController:(WKUserContentController *)userContentController _onLoadingFinish(event); _disablePromptDuringLoading = NO; } - } else if ([message.name isEqualToString:MessageHandlerName]) { + } else if ([message.name isEqualToString:MessageHandlerName] || [self isAdditionalMessageHandlerName:message.name]) { if (_onMessage && message.frameInfo.mainFrame) { NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary: @{@"data": message.body}]; + id body = message.body; + if (![body isKindOfClass:[NSString class]]) { + // Third-party pages may post objects; deliver a JSON string so + // onMessage always receives a string like it does for ReactNativeWebView. + NSData *json = [NSJSONSerialization isValidJSONObject:body] + ? [NSJSONSerialization dataWithJSONObject:body options:0 error:nil] + : nil; + body = json ? [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding] : [body description]; + } + [event addEntriesFromDictionary: @{@"data": body}]; [event addEntriesFromDictionary: @{@"url": message.frameInfo.request.URL.absoluteString}]; _onMessage(event); } } } +- (BOOL)isAdditionalMessageHandlerName:(NSString *)name { + return name != nil && [_additionalMessageHandlerNames containsObject:name]; +} + +- (void)removeAdditionalMessageHandlers:(WKWebViewConfiguration *)wkWebViewConfig { + for (NSString *name in _additionalMessageHandlerNames) { + if ([name isEqualToString:MessageHandlerName]) { + continue; + } + [wkWebViewConfig.userContentController removeScriptMessageHandlerForName:name]; + } +} + +- (void)addAdditionalMessageHandlers:(WKWebViewConfiguration *)wkWebViewConfig { + for (NSString *name in _additionalMessageHandlerNames) { + if ([name isEqualToString:MessageHandlerName]) { + continue; + } + [wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate:self] + name:name]; + } +} + +- (void)setAdditionalMessageHandlerNames:(NSArray *)additionalMessageHandlerNames { + if (_webView != nil) { + [self removeAdditionalMessageHandlers:_webView.configuration]; + } + _additionalMessageHandlerNames = [additionalMessageHandlerNames copy]; + if (_webView != nil) { + [self resetupScripts:_webView.configuration]; + } +} + - (void)setSource:(NSDictionary *)source { if (![_source isEqualToDictionary:source]) { @@ -1949,10 +1993,12 @@ - (void)syncCookiesToWebView:(void (^)(void))completion { - (void)resetupScripts:(WKWebViewConfiguration *)wkWebViewConfig { [wkWebViewConfig.userContentController removeAllUserScripts]; [wkWebViewConfig.userContentController removeScriptMessageHandlerForName:MessageHandlerName]; + [self removeAdditionalMessageHandlers:wkWebViewConfig]; if(self.enableApplePay){ if (self.postMessageScript){ [wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate:self] name:MessageHandlerName]; + [self addAdditionalMessageHandlers:wkWebViewConfig]; } return; } @@ -2083,6 +2129,7 @@ - (void)resetupScripts:(WKWebViewConfiguration *)wkWebViewConfig { if (self.postMessageScript){ [wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate:self] name:MessageHandlerName]; + [self addAdditionalMessageHandlers:wkWebViewConfig]; [wkWebViewConfig.userContentController addUserScript:self.postMessageScript]; } if (self.atEndScript) { diff --git a/apple/RNCWebViewManager.mm b/apple/RNCWebViewManager.mm index 96855c8578..c410c7ee1f 100644 --- a/apple/RNCWebViewManager.mm +++ b/apple/RNCWebViewManager.mm @@ -117,6 +117,7 @@ - (RNCView *)view RCT_EXPORT_VIEW_PROPERTY(enableApplePay, BOOL) RCT_EXPORT_VIEW_PROPERTY(menuItems, NSArray); RCT_EXPORT_VIEW_PROPERTY(suppressMenuItems, NSArray); +RCT_EXPORT_VIEW_PROPERTY(additionalMessageHandlerNames, NSArray); // New arch only RCT_CUSTOM_VIEW_PROPERTY(hasOnFileDownload, BOOL, RNCWebViewImpl) {} diff --git a/docs/Reference.md b/docs/Reference.md index ec934cb7ea..3717be65be 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -87,6 +87,7 @@ This document lays out the current public properties and methods for the React N - [`setSupportMultipleWindows`](Reference.md#setSupportMultipleWindows) - [`basicAuthCredential`](Reference.md#basicAuthCredential) - [`enableApplePay`](Reference.md#enableApplePay) +- [`additionalMessageHandlerNames`](Reference.md#additionalMessageHandlerNames) - [`forceDarkOn`](Reference.md#forceDarkOn) - [`useWebView2`](Reference.md#useWebView2) - [`minimumFontSize`](Reference.md#minimumFontSize) @@ -1599,6 +1600,26 @@ Example: ``` +### `additionalMessageHandlerNames`[⬆](#props-index) + +Extra names to register as native `WKScriptMessageHandler`s next to the built-in `ReactNativeWebView` one. A page can call `window.webkit.messageHandlers..postMessage(...)` and the message arrives on [`onMessage`](Reference.md#onmessage) exactly like a `ReactNativeWebView` message. Non-string bodies are serialized to JSON so `event.nativeEvent.data` is always a string. + +Native handlers keep working when [`enableApplePay`](Reference.md#enableApplePay) is `true`, which removes every injected script, so this is the way to receive events from third-party checkout pages that post to their own handler name. Only messages from the main frame are delivered. The handlers are registered when the WebView is created, so pass the prop on mount. + +| Type | Required | Default | Platform | +| -------- | -------- | ------- | -------- | +| string[] | No | none | iOS | + +Example: + +```javascript + console.log(event.nativeEvent.data)} +/> +``` + ### `forceDarkOn`[⬆](#props-index) Configuring Dark Theme diff --git a/example/App.tsx b/example/App.tsx index 03843b9984..09ec223ea3 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -21,6 +21,7 @@ import Messaging from './examples/Messaging'; import MultiMessaging from './examples/MultiMessaging'; import NativeWebpage from './examples/NativeWebpage'; import ApplePay from './examples/ApplePay'; +import AdditionalMessageHandlers from './examples/AdditionalMessageHandlers'; import GooglePay from './examples/GooglePay'; import CustomMenu from './examples/CustomMenu'; import OpenWindow from './examples/OpenWindow'; @@ -124,6 +125,15 @@ const TESTS = { return ; }, }, + AdditionalMessageHandlers: { + title: 'Additional message handlers ', + testId: 'AdditionalMessageHandlers', + description: + 'Native message handler names forwarded to onMessage while Apple Pay is enabled', + render() { + return ; + }, + }, GooglePay: { title: 'Google Pay ', testId: 'GooglePay', @@ -172,7 +182,7 @@ export default class App extends Component { _simulateRestart = () => { this.setState({ restarting: true }, () => - this.setState({ restarting: false }), + this.setState({ restarting: false }) ); }; @@ -259,6 +269,13 @@ export default class App extends Component { onPress={() => this._changeTest('ApplePay')} /> )} + {Platform.OS === 'ios' && ( + + + + +`; + +export default class AdditionalMessageHandlers extends Component { + state: State = { messages: [] }; + + render() { + return ( + + + + this.setState((state) => ({ + messages: [...state.messages, event.nativeEvent.data], + })) + } + /> + + Received on onMessage: + {this.state.messages.map((message, index) => ( + + {message} + + ))} + + ); + } +} diff --git a/src/RNCWebViewNativeComponent.ts b/src/RNCWebViewNativeComponent.ts index 6fe64b469f..1646cbeec4 100644 --- a/src/RNCWebViewNativeComponent.ts +++ b/src/RNCWebViewNativeComponent.ts @@ -244,6 +244,7 @@ export interface NativeProps extends ViewProps { menuItems?: ReadonlyArray>; suppressMenuItems?: Readonly[]; + additionalMessageHandlerNames?: Readonly[]; // Workaround to watch if listener if defined hasOnFileDownload?: boolean; fraudulentWebsiteWarningEnabled?: WithDefault; diff --git a/src/WebViewTypes.ts b/src/WebViewTypes.ts index 31d7aaef5a..8167aa5935 100644 --- a/src/WebViewTypes.ts +++ b/src/WebViewTypes.ts @@ -729,6 +729,19 @@ export interface IOSWebViewProps extends WebViewSharedProps { */ enableApplePay?: boolean; + /** + * Extra names to register as native WKScriptMessageHandlers next to the + * built-in `ReactNativeWebView` one. A page can then call + * `window.webkit.messageHandlers..postMessage(...)` and the message + * arrives on `onMessage` exactly like a `ReactNativeWebView` message + * (non-string bodies are serialized to JSON). Native handlers keep working + * when `enableApplePay` is true, which removes every injected script, so + * this is the way to receive events from third-party checkout pages that + * post to their own handler name (for example Coinbase Onramp's `cbOnramp`). + * @platform ios + */ + additionalMessageHandlerNames?: string[]; + /** * An array of objects which will be shown when selecting text. An empty array will suppress the menu. * These will appear after a long press to select text. From 12e72ebfe65c426666d2f0743edbe10c14859e0d Mon Sep 17 00:00:00 2001 From: Darius Costolas <10818970+meltingice1337@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:28:13 +0300 Subject: [PATCH 2/2] feat: add setAdditionalMessageHandlerNames method to RNCWebViewManager --- .../com/reactnativecommunity/webview/RNCWebViewManager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java index 6e21a020da..a00f297fab 100644 --- a/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -398,6 +398,9 @@ public void setDirectionalLockEnabled(RNCWebViewWrapper view, boolean value) {} @Override public void setEnableApplePay(RNCWebViewWrapper view, boolean value) {} + @Override + public void setAdditionalMessageHandlerNames(RNCWebViewWrapper view, @Nullable ReadableArray value) {} + @Override public void setHideKeyboardAccessoryView(RNCWebViewWrapper view, boolean value) {}