Skip to content

Commit

Permalink
feat(Windows): Add support for custom headers, POST requests and `onO…
Browse files Browse the repository at this point in the history
…penWindow` to Windows

* Add WindowsWebViewCommands and implement releaseFocus

* Fixes to setup request methos, added helpers class to visual studio project and fixed missing FocusManager namespace

* Add LinkHandlingEnabled property to allow application to handle request by the webview to open new windows

* Refactor cookie handling in ReactWebView2.cpp

* Fix WebView source handling and add new commands

* Add linkHandlingEnabled property to nativeProps

* Add postMessage and loadUrl commands to command list

* Add loadUrl function to WebViewComponent

* Refactor string manipulation functions in ReactWebViewHelpers

* Refactor cookie handling in ReactWebView2.cpp

* Update RNCWebViewUIManagerWindows type in WebViewTypes.ts

* Fix WebView messaging functionality

* Add useWebView2 prop to WebView component

* Update test to include alert support in WebView2

* Create Windows specific example components for testing webview scenarios

* Update documentation

---------

Co-authored-by: Kennedy Mumo <kemumo@microsoft.com>
  • Loading branch information
jfkm69 and Kennedy Mumo committed Jan 31, 2024
1 parent 6960a19 commit 9e2794e
Show file tree
Hide file tree
Showing 21 changed files with 902 additions and 192 deletions.
3 changes: 2 additions & 1 deletion __tests__/Alert.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ describe('Alert Tests', () => {
const showAlertButton = By2.nativeName('Show alert');
await showAlertButton.click();
await By2.nativeName('Hello! I am an alert box!');
// await By2.nativeName('OK').click(); All alerts will be automatically dismissed as Windows Webview does not have support for Alerts https://github.com/MicrosoftDocs/winrt-api/blob/docs/windows.ui.xaml.controls/webview.md#use-of-alert
const okButton = By2.nativeXpath('//Button[@Name="OK"]');
await okButton.click();
const dismissMessage = By2.nativeName('Alert dismissed!');
expect(dismissMessage).not.toBeNull();
await driver.quit();
Expand Down
2 changes: 2 additions & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1771,6 +1771,8 @@ Clears the resource cache. Note that the cache is per-application, so this will

In iOS, includeDiskFiles will also remove data from the web storages and databases.[developer.apple.com reference](https://developer.apple.com/documentation/webkit/wkwebsitedatastore/1532936-removedata)

In Windows, this has been set to clear cookies, since there is no way to clear the cache in WebView2 because it is shared with Edge. The best we can do is clear the cookies, because we cannot access history or local storage.

### `clearHistory()`[](#methods-index)

(android only)
Expand Down
73 changes: 73 additions & 0 deletions example/examples/Alerts.windows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, {Component} from 'react';
import {Text, View} from 'react-native';

import WebView from 'react-native-webview';

const HTML = `
<!DOCTYPE html>\n
<html>
<head>
<title>Alerts</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=320, user-scalable=no">
<style type="text/css">
body {
margin: 0;
padding: 0;
font: 62.5% arial, sans-serif;
background: #ccc;
}
</style>
</head>
<body>
<button onclick="showAlert()">Show alert</button>
<button onclick="showConfirm()">Show confirm</button>
<button onclick="showPrompt()">Show prompt</button>
<p id="demo"></p>
<script>
function showAlert() {
alert("Hello! I am an alert box!");
document.getElementById("demo").innerHTML = "Alert dismissed!";
}
function showConfirm() {
var response;
if (confirm("Press a button!")) {
response = "You pressed OK on confirm!";
} else {
response = "You pressed Cancel on confirm!";
}
document.getElementById("demo").innerHTML = response;
}
function showPrompt() {
var message;
const name = prompt("Please enter your name", "Name");
if (name !== null) {
message = "Hello " + name;
} else {
message = "You pressed Cancel on prompt!";
}
document.getElementById("demo").innerHTML = message;
}
</script>
</body>
</html>
`;

type Props = {};
type State = {};

export default class Alerts extends Component<Props, State> {
state = {};

render() {
return (
<View style={{ height: 120 }}>
<WebView
source={{html: HTML}}
automaticallyAdjustContentInsets={false}
useWebView2
/>
</View>
);
}
}
74 changes: 74 additions & 0 deletions example/examples/Messaging.windows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, {Component} from 'react';
import {View, Alert, TextInput} from 'react-native';

import WebView from 'react-native-webview';

const HTML = `<!DOCTYPE html>\n
<html>
<head>
<title>Messaging</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=320, user-scalable=no">
<style type="text/css">
body {
margin: 0;
padding: 0;
font: 62.5% arial, sans-serif;
background: #ccc;
}
</style>
</head>
<body>
<button onclick="sendPostMessage()">Send post message from JS to WebView</button>
<p id="demo"></p>
<p id="test">Nothing received yet</p>
<script>
function sendPostMessage() {
window.postMessage('Message from JS');
}
window.addEventListener('message',function(event){
document.getElementById('test').innerHTML = event.data;
console.log("Message received from RN: ",event.data);
},false);
document.addEventListener('message',function(event){
document.getElementById('test').innerHTML = event.data;
console.log("Message received from RN: ",event.data);
},false);
</script>
</body>
</html>`;

type Props = {};
type State = {};

export default class Messaging extends Component<Props, State> {
state = {};

constructor(props) {
super(props);
this.webView = React.createRef();
}

render() {
return (
<View style={{height: 120}}>
<TextInput onSubmitEditing={(e) => {
this.webView.current.postMessage(e.nativeEvent.text);
}}/>
<WebView
ref={this.webView}
source={{html: HTML}}
onLoadEnd={()=>{this.webView.current.postMessage('Hello from RN');}}
automaticallyAdjustContentInsets={false}
onMessage={(e: {nativeEvent: {data?: string}}) => {
Alert.alert('Message received from JS: ', e.nativeEvent.data);
}}
useWebView2
/>
</View>
);
}
}
133 changes: 133 additions & 0 deletions example/examples/OpenWindow.windows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React, {Component} from 'react';
import {Button, Switch, StyleSheet, Text, View} from 'react-native';

import WebView from 'react-native-webview';

const HTML = `
<!DOCTYPE html>\n
<html>
<head>
<title>OnOpenWindow</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=320, user-scalable=no">
<style type="text/css">
body {
margin: 0;
padding: 0;
font: 62.5% arial, sans-serif;
background: #ccc;
}
a {
display: block;
margin-top: 20px;
}
</style>
</head>
<body>
<button onclick="openWindowWithoutParam()">Call window.open() from JS</button>
<button onclick="openWindowBlank()">Call window.open('_blank') from JS</button>
<button onclick="openWindowSelf()">Call window.open('_self') from JS</button>
<button onclick="openWindowParent()">Call window.open('_parent') from JS</button>
<button onclick="openWindowTop()">Call window.open('_top') from JS</button>
<a href="https://example.com" target="_blank">Call target=_blank link from DOM</a>
<script>
function openWindowWithoutParam() {
window.open('https://example.com')
}
function openWindowBlank() {
window.open('https://example.com', '_blank')
}
function openWindowSelf() {
window.open('https://example.com', '_self')
}
function openWindowParent() {
window.open('https://example.com', '_parent')
}
function openWindowTop() {
window.open('https://example.com', '_top')
}
</script>
</body>
</html>
`;

type Props = {};
type State = {
shouldInterceptOpenWindow: boolean,
text: string,
webViewKey: number
};

export default class OpenWindow extends Component<Props, State> {
state = {
shouldInterceptOpenWindow: true,
text: 'No OpenWindow event intercepted yet',
webViewKey: 1
};

interceptOpenWindow = (syntheticEvent) => {
const { nativeEvent } = syntheticEvent
const { targetUrl } = nativeEvent
this.setState({
text: `Intercepted OpenWindow event for ${targetUrl} at ${Date.now()}`
})
};

toggleShouldInterceptOpenWindow = () => {
this.setState(prevState => ({
shouldInterceptOpenWindow: !prevState.shouldInterceptOpenWindow
}))
};

resetWebView = () => {
this.setState(prevState => ({
webViewKey: prevState.webViewKey + 1
}))
};

render() {
const onOpenWindow = this.state.shouldInterceptOpenWindow
? this.interceptOpenWindow
: undefined

return (
<View style={styles.container}>
<View style={styles.interceptSection}>
<Text style={styles.text}>
Intercept OpenWindow event
</Text>
<Switch
onValueChange={this.toggleShouldInterceptOpenWindow}
value={this.state.shouldInterceptOpenWindow}
/>
</View>
<WebView
key={this.state.webViewKey}
source={{html: HTML}}
automaticallyAdjustContentInsets={false}
useWebView2
onOpenWindow={onOpenWindow}
/>
<Text style={styles.text}>
{this.state.text}
</Text>
<Button title="Reset webview" onPress={this.resetWebView} />
</View>
);
}
}

const styles = StyleSheet.create({
container: {
height: 300
},
interceptSection: {
alignItems: 'center',
flexDirection: "row",
justifyContent: 'space-between',
marginBottom: 20
},
text: {
color: 'black'
}
})
14 changes: 11 additions & 3 deletions src/WebView.windows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
import styles from './WebView.styles';

const Commands = codegenNativeCommands({
supportedCommands: ['goBack', 'goForward', 'reload', 'stopLoading', 'injectJavaScript', 'requestFocus', 'postMessage', 'loadUrl'],
supportedCommands: ['goBack', 'goForward', 'reload', 'stopLoading', 'injectJavaScript', 'requestFocus', 'clearCache', 'postMessage', 'loadUrl'],
});
const { resolveAssetSource } = Image;

Expand All @@ -43,6 +43,8 @@ const WebViewComponent = forwardRef<{}, WindowsWebViewProps>(({
onLoad,
onLoadEnd,
onLoadProgress,
onOpenWindow: onOpenWindowProp,
onSourceChanged,
onHttpError: onHttpErrorProp,
onMessage: onMessageProp,
renderLoading,
Expand Down Expand Up @@ -71,7 +73,7 @@ const WebViewComponent = forwardRef<{}, WindowsWebViewProps>(({
}
}, [RCTWebViewString]);

const { onLoadingStart, onShouldStartLoadWithRequest, onMessage, viewState, setViewState, lastErrorEvent, onHttpError, onLoadingError, onLoadingFinish, onLoadingProgress } = useWebViewLogic({
const { onLoadingStart, onShouldStartLoadWithRequest, onMessage, viewState, setViewState, lastErrorEvent, onHttpError, onLoadingError, onLoadingFinish, onLoadingProgress, onOpenWindow } = useWebViewLogic({
onNavigationStateChange,
onLoad,
onError,
Expand All @@ -84,6 +86,7 @@ const WebViewComponent = forwardRef<{}, WindowsWebViewProps>(({
originWhitelist,
onShouldStartLoadWithRequestProp,
onShouldStartLoadWithRequestCallback,
onOpenWindowProp,
})

useImperativeHandle(ref, () => ({
Expand All @@ -98,6 +101,8 @@ const WebViewComponent = forwardRef<{}, WindowsWebViewProps>(({
postMessage: (data: string) => Commands.postMessage(webViewRef.current, data),
injectJavaScript: (data: string) => Commands.injectJavaScript(webViewRef.current, data),
requestFocus: () => Commands.requestFocus(webViewRef.current),
clearCache: () => Commands.clearCache(webViewRef.current),
loadUrl: (url: string) => Commands.loadUrl(webViewRef.current, url),
}), [setViewState, webViewRef]);

let otherView = null;
Expand All @@ -118,19 +123,22 @@ const WebViewComponent = forwardRef<{}, WindowsWebViewProps>(({
const webViewContainerStyle = [styles.container, containerStyle];

const NativeWebView
= useWebView2? RCTWebView2 : RCTWebView;
= useWebView2 ? RCTWebView2 : RCTWebView;

const webView = <NativeWebView
key="webViewKey"
{...otherProps}
messagingEnabled={typeof onMessageProp === 'function'}
linkHandlingEnabled={typeof onOpenWindowProp === 'function'}
onLoadingError={onLoadingError}
onLoadingFinish={onLoadingFinish}
onLoadingProgress={onLoadingProgress}
onLoadingStart={onLoadingStart}
onHttpError={onHttpError}
onMessage={onMessage}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
onOpenWindow={onOpenWindow}
onSourceChanged={onSourceChanged}
ref={webViewRef}
// TODO: find a better way to type this.
source={resolveAssetSource(source as ImageSourcePropType)}
Expand Down
2 changes: 0 additions & 2 deletions src/WebViewShared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,9 @@ export const useWebViewLogic = ({
)
, [originWhitelist, onShouldStartLoadWithRequestProp, onShouldStartLoadWithRequestCallback])

// Android and iOS Only
const onOpenWindow = useCallback((event: WebViewOpenWindowEvent) => {
onOpenWindowProp?.(event);
}, [onOpenWindowProp]);
// !Android and iOS Only

return {
onShouldStartLoadWithRequest,
Expand Down
Loading

0 comments on commit 9e2794e

Please sign in to comment.