Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

Expand Down
9 changes: 9 additions & 0 deletions apple/RNCWebView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions apple/RNCWebViewImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
@property (nonatomic, assign) BOOL enableApplePay;
@property (nonatomic, copy) NSArray<NSDictionary *> * _Nullable menuItems;
@property (nonatomic, copy) NSArray<NSString *> * _Nullable suppressMenuItems;
@property (nonatomic, copy) NSArray<NSString *> * _Nullable additionalMessageHandlerNames;
@property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection;
#if !TARGET_OS_OSX
@property (nonatomic, assign) WKDataDetectorTypes dataDetectorTypes;
Expand Down
51 changes: 49 additions & 2 deletions apple/RNCWebViewImpl.m
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ @interface RNCWKWebView : WKWebView
#if !TARGET_OS_OSX
@property (nonatomic, copy) NSArray<NSDictionary *> * _Nullable menuItems;
@property (nonatomic, copy) NSArray<NSString *> * _Nullable suppressMenuItems;
@property (nonatomic, copy) NSArray<NSString *> * _Nullable additionalMessageHandlerNames;
#endif // !TARGET_OS_OSX
@end
@implementation RNCWKWebView
Expand Down Expand Up @@ -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, *)) {
Expand Down Expand Up @@ -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<NSString *, id> *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<NSString *> *)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]) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions apple/RNCWebViewManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand Down
21 changes: 21 additions & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1599,6 +1600,26 @@ Example:
<WebView enableApplePay={true} />
```

### `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.<name>.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
<WebView
enableApplePay
additionalMessageHandlerNames={['cbOnramp']}
onMessage={(event) => console.log(event.nativeEvent.data)}
/>
```

### `forceDarkOn`[⬆](#props-index)

Configuring Dark Theme
Expand Down
19 changes: 18 additions & 1 deletion example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -124,6 +125,15 @@ const TESTS = {
return <ApplePay />;
},
},
AdditionalMessageHandlers: {
title: 'Additional message handlers ',
testId: 'AdditionalMessageHandlers',
description:
'Native message handler names forwarded to onMessage while Apple Pay is enabled',
render() {
return <AdditionalMessageHandlers />;
},
},
GooglePay: {
title: 'Google Pay ',
testId: 'GooglePay',
Expand Down Expand Up @@ -172,7 +182,7 @@ export default class App extends Component<Props, State> {

_simulateRestart = () => {
this.setState({ restarting: true }, () =>
this.setState({ restarting: false }),
this.setState({ restarting: false })
);
};

Expand Down Expand Up @@ -259,6 +269,13 @@ export default class App extends Component<Props, State> {
onPress={() => this._changeTest('ApplePay')}
/>
)}
{Platform.OS === 'ios' && (
<Button
testID="testType_additionalMessageHandlers"
title="MessageHandlers"
onPress={() => this._changeTest('AdditionalMessageHandlers')}
/>
)}
{Platform.OS === 'android' && (
<Button
testID="testType_googlePay"
Expand Down
60 changes: 60 additions & 0 deletions example/examples/AdditionalMessageHandlers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { Component } from 'react';
import { Text, View } from 'react-native';

import WebView from '@metamask/react-native-webview';

type Props = {};
type State = { messages: string[] };

// A page that posts to its own handler name instead of ReactNativeWebView,
// the way third-party checkout pages do. With enableApplePay the WebView
// removes every injected script, so only a native handler can receive it.
const HTML = `
<!doctype html>
<html>
<body style="font-family: -apple-system; padding: 16px">
<h3>additionalMessageHandlerNames</h3>
<button id="send" style="font-size: 18px; padding: 12px">
Post to cbOnramp
</button>
<script>
document.getElementById('send').onclick = function () {
var handlers = window.webkit && window.webkit.messageHandlers;
if (handlers && handlers.cbOnramp) {
handlers.cbOnramp.postMessage(JSON.stringify({ eventName: 'example.string' }));
handlers.cbOnramp.postMessage({ eventName: 'example.object' });
}
};
</script>
</body>
</html>
`;

export default class AdditionalMessageHandlers extends Component<Props, State> {
state: State = { messages: [] };

render() {
return (
<View style={{ flex: 1 }}>
<View style={{ height: 200 }}>
<WebView
enableApplePay={true}
additionalMessageHandlerNames={['cbOnramp']}
source={{ html: HTML }}
onMessage={(event) =>
this.setState((state) => ({
messages: [...state.messages, event.nativeEvent.data],
}))
}
/>
</View>
<Text style={{ padding: 16 }}>Received on onMessage:</Text>
{this.state.messages.map((message, index) => (
<Text key={index} style={{ paddingHorizontal: 16 }}>
{message}
</Text>
))}
</View>
);
}
}
1 change: 1 addition & 0 deletions src/RNCWebViewNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ export interface NativeProps extends ViewProps {

menuItems?: ReadonlyArray<Readonly<{ label: string; key: string }>>;
suppressMenuItems?: Readonly<string>[];
additionalMessageHandlerNames?: Readonly<string>[];
// Workaround to watch if listener if defined
hasOnFileDownload?: boolean;
fraudulentWebsiteWarningEnabled?: WithDefault<boolean, true>;
Expand Down
13 changes: 13 additions & 0 deletions src/WebViewTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.<name>.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.
Expand Down
Loading