Skip to content

Commit b7f5c6f

Browse files
authored
fix(windows): Refactor JS Code for Windows (react-native-webview#2554)
* Refactor JS Code * Fix Typescript
1 parent a2a2d0a commit b7f5c6f

File tree

2 files changed

+130
-248
lines changed

2 files changed

+130
-248
lines changed

src/WebView.windows.tsx

Lines changed: 128 additions & 245 deletions
Original file line numberDiff line numberDiff line change
@@ -10,271 +10,154 @@
1010
* Licensed under the MIT License.
1111
*/
1212

13-
import React from 'react';
13+
import React, { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
1414
import {
15-
UIManager as NotTypedUIManager,
1615
View,
17-
StyleSheet,
1816
Image,
1917
ImageSourcePropType,
20-
findNodeHandle,
18+
NativeModules,
2119
} from 'react-native';
20+
// @ts-expect-error react-native doesn't have this type
21+
import codegenNativeCommandsUntyped from 'react-native/Libraries/Utilities/codegenNativeCommands';
22+
import invariant from 'invariant';
2223
import {RCTWebView, RCTWebView2} from "./WebViewNativeComponent.windows";
23-
import { createOnShouldStartLoadWithRequest } from './WebViewShared';
24+
import { useWebWiewLogic, defaultOriginWhitelist, defaultRenderError, defaultRenderLoading, } from './WebViewShared';
2425
import {
2526
NativeWebViewWindows,
2627
WindowsWebViewProps,
27-
WebViewProgressEvent,
28-
WebViewNavigationEvent,
29-
WebViewErrorEvent,
30-
WebViewHttpErrorEvent,
31-
WebViewMessageEvent,
32-
RNCWebViewUIManagerWindows,
33-
State,
3428
} from './WebViewTypes';
3529

30+
import styles from './WebView.styles';
31+
3632
const {version} = require('react-native/Libraries/Core/ReactNativeVersion');
3733

38-
const UIManager = NotTypedUIManager as RNCWebViewUIManagerWindows;
39-
const { resolveAssetSource } = Image;
34+
const codegenNativeCommands = codegenNativeCommandsUntyped as <T extends {}>(options: { supportedCommands: (keyof T)[] }) => T;
4035

41-
const styles = StyleSheet.create({
42-
container: {
43-
flex: 1,
44-
},
45-
hidden: {
46-
height: 0,
47-
flex: 0, // disable 'flex:1' when hiding a View
48-
},
49-
loadingView: {
50-
flex: 1,
51-
justifyContent: 'center',
52-
alignItems: 'center',
53-
},
54-
loadingProgressBar: {
55-
height: 20,
56-
},
36+
const Commands = codegenNativeCommands({
37+
supportedCommands: ['goBack', 'goForward', 'reload', 'stopLoading', 'injectJavaScript', 'requestFocus', 'postMessage', 'loadUrl'],
5738
});
39+
const { resolveAssetSource } = Image;
5840

59-
export default class WebView extends React.Component<WindowsWebViewProps, State> {
60-
61-
static defaultProps = {
62-
javaScriptEnabled: true,
63-
};
64-
65-
state: State = {
66-
viewState: this.props.startInLoadingState ? 'LOADING' : 'IDLE',
67-
lastErrorEvent: null,
68-
}
69-
70-
webViewRef = React.createRef<NativeWebViewWindows>();
71-
72-
RnwVersionSupportsWebView2 = (version.major>1 || version.minor>=68);
41+
const WebViewComponent = forwardRef<{}, WindowsWebViewProps>(({
42+
cacheEnabled = true,
43+
originWhitelist = defaultOriginWhitelist,
44+
startInLoadingState,
45+
onNavigationStateChange,
46+
onLoadStart,
47+
onError,
48+
onLoad,
49+
onLoadEnd,
50+
onLoadProgress,
51+
onHttpError: onHttpErrorProp,
52+
onMessage: onMessageProp,
53+
renderLoading,
54+
renderError,
55+
style,
56+
containerStyle,
57+
source,
58+
nativeConfig,
59+
onShouldStartLoadWithRequest: onShouldStartLoadWithRequestProp,
60+
useWebView2,
61+
...otherProps
62+
}, ref) => {
63+
const webViewRef = useRef<NativeWebViewWindows | null>(null);
64+
65+
const RnwVersionSupportsWebView2 = (version.major>1 || version.minor>=68);
7366

74-
RCTWebViewString = (this.RnwVersionSupportsWebView2 && this.props.useWebView2) ? 'RCTWebView2' : 'RCTWebView';
75-
76-
goForward = () => {
77-
UIManager.dispatchViewManagerCommand(
78-
this.getWebViewHandle(),
79-
UIManager.getViewManagerConfig(this.RCTWebViewString).Commands.goForward,
80-
undefined,
81-
);
82-
}
83-
84-
goBack = () => {
85-
UIManager.dispatchViewManagerCommand(
86-
this.getWebViewHandle(),
87-
UIManager.getViewManagerConfig(this.RCTWebViewString).Commands.goBack,
88-
undefined,
89-
);
90-
}
91-
92-
reload = () => {
93-
UIManager.dispatchViewManagerCommand(
94-
this.getWebViewHandle(),
95-
UIManager.getViewManagerConfig(this.RCTWebViewString).Commands.reload,
96-
undefined,
97-
);
98-
}
99-
100-
injectJavaScript = (data: string) => {
101-
UIManager.dispatchViewManagerCommand(
102-
this.getWebViewHandle(),
103-
UIManager.getViewManagerConfig(this.RCTWebViewString).Commands.injectJavaScript,
104-
[data],
105-
);
106-
}
107-
108-
postMessage = (data: string) => {
109-
const message = this.getInjectableJSMessage(data);
110-
UIManager.dispatchViewManagerCommand(
111-
this.getWebViewHandle(),
112-
UIManager.getViewManagerConfig(this.RCTWebViewString).Commands.injectJavaScript,
113-
[message],
114-
);
115-
};
116-
117-
getInjectableJSMessage = (message: string ) => {
118-
return `(function() {window.dispatchEvent(new MessageEvent('message', {data: ${JSON.stringify(
119-
message
120-
)}}));})();`;
121-
}
122-
123-
124-
125-
/**
126-
* We return an event with a bunch of fields including:
127-
* url, title, loading, canGoBack, canGoForward
128-
*/
129-
updateNavigationState = (event: WebViewNavigationEvent) => {
130-
if (this.props.onNavigationStateChange) {
131-
this.props.onNavigationStateChange(event.nativeEvent);
132-
}
133-
}
134-
135-
getWebViewHandle = () => {
136-
// eslint-disable-next-line react/no-string-refs
137-
return findNodeHandle(this.webViewRef.current);
138-
}
139-
140-
onLoadingStart = (event: WebViewNavigationEvent) => {
141-
const { onLoadStart } = this.props;
142-
if(onLoadStart) {
143-
onLoadStart(event);
144-
}
145-
this.updateNavigationState(event);
146-
}
147-
148-
onLoadingProgress = (event: WebViewProgressEvent) => {
149-
const { onLoadProgress } = this.props;
150-
if (onLoadProgress) {
151-
onLoadProgress(event);
152-
}
153-
};
154-
155-
onLoadingError = (event: WebViewErrorEvent) => {
156-
event.persist(); // persist this event because we need to store it
157-
const { onError, onLoadEnd } = this.props;
158-
if (onError) {
159-
onError(event);
160-
} else {
161-
console.warn('Encountered an error loading page', event.nativeEvent);
162-
}
163-
164-
if (onLoadEnd) {
165-
onLoadEnd(event);
67+
const RCTWebViewString = (RnwVersionSupportsWebView2 && useWebView2) ? 'RCTWebView2' : 'RCTWebView';
68+
69+
const onShouldStartLoadWithRequestCallback = useCallback((shouldStart: boolean, url: string, lockIdentifier?: number) => {
70+
if (lockIdentifier) {
71+
if (RCTWebViewString === 'RCTWebView'){
72+
NativeModules.RCTWebView.onShouldStartLoadWithRequestCallback(shouldStart, lockIdentifier);
73+
}else{
74+
NativeModules.RCTWebView2.onShouldStartLoadWithRequestCallback(shouldStart, lockIdentifier);
75+
}
76+
} else if (shouldStart) {
77+
Commands.loadUrl(webViewRef, url);
16678
}
167-
if (event.isDefaultPrevented()) return;
168-
169-
this.setState({
170-
lastErrorEvent: event.nativeEvent,
171-
viewState: 'ERROR',
172-
});
173-
};
174-
175-
onLoadingFinish =(event: WebViewNavigationEvent) => {
176-
const {onLoad, onLoadEnd} = this.props;
177-
if(onLoad) {
178-
onLoad(event);
179-
}
180-
if(onLoadEnd) {
181-
onLoadEnd(event);
182-
}
183-
this.setState({
184-
viewState: 'IDLE',
185-
});
186-
this.updateNavigationState(event);
187-
}
188-
189-
onMessage = (event: WebViewMessageEvent) => {
190-
const { onMessage } = this.props;
191-
if (onMessage) {
192-
onMessage(event);
193-
}
194-
}
195-
196-
onHttpError = (event: WebViewHttpErrorEvent) => {
197-
const { onHttpError } = this.props;
198-
if (onHttpError) {
199-
onHttpError(event);
200-
}
201-
}
202-
203-
render () {
204-
const {
205-
nativeConfig = {},
206-
onMessage,
207-
onShouldStartLoadWithRequest: onShouldStartLoadWithRequestProp,
208-
originWhitelist,
209-
renderError,
210-
renderLoading,
211-
style,
212-
containerStyle,
213-
useWebView2,
214-
...otherProps
215-
} = this.props;
216-
217-
let otherView = null;
218-
219-
if (this.state.viewState === 'LOADING') {
220-
otherView = this.props.renderLoading && this.props.renderLoading();
221-
} else if (this.state.viewState === 'ERROR') {
222-
const errorEvent = this.state.lastErrorEvent;
223-
otherView = this.props.renderError
224-
&& this.props.renderError(
225-
errorEvent.domain,
226-
errorEvent.code,
227-
errorEvent.description,
228-
);
229-
} else if (this.state.viewState !== 'IDLE') {
230-
console.error('RCTWebView invalid state encountered: ', this.state.viewState);
231-
}
232-
233-
const webViewStyles = [styles.container, this.props.style];
234-
if (
235-
this.state.viewState === 'LOADING'
236-
|| this.state.viewState === 'ERROR'
237-
) {
238-
// if we're in either LOADING or ERROR states, don't show the webView
239-
webViewStyles.push(styles.hidden);
240-
}
241-
242-
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
243-
()=>{},
244-
// casting cause it's in the default props
245-
originWhitelist as readonly string[],
246-
onShouldStartLoadWithRequestProp,
79+
}, []);
80+
81+
const { onLoadingStart, onShouldStartLoadWithRequest, onMessage, viewState, setViewState, lastErrorEvent, onHttpError, onLoadingError, onLoadingFinish, onLoadingProgress } = useWebWiewLogic({
82+
onNavigationStateChange,
83+
onLoad,
84+
onError,
85+
onHttpErrorProp,
86+
onLoadEnd,
87+
onLoadProgress,
88+
onLoadStart,
89+
onMessageProp,
90+
startInLoadingState,
91+
originWhitelist,
92+
onShouldStartLoadWithRequestProp,
93+
onShouldStartLoadWithRequestCallback,
94+
})
95+
96+
useImperativeHandle(ref, () => ({
97+
goForward: () => Commands.goForward(webViewRef.current),
98+
goBack: () => Commands.goBack(webViewRef.current),
99+
reload: () => {
100+
setViewState(
101+
'LOADING',
102+
); Commands.reload(webViewRef.current)
103+
},
104+
stopLoading: () => Commands.stopLoading(webViewRef.current),
105+
postMessage: (data: string) => Commands.postMessage(webViewRef.current, data),
106+
injectJavaScript: (data: string) => Commands.injectJavaScript(webViewRef.current, data),
107+
requestFocus: () => Commands.requestFocus(webViewRef.current),
108+
}), [setViewState, webViewRef]);
109+
110+
let otherView = null;
111+
if (viewState === 'LOADING') {
112+
otherView = (renderLoading || defaultRenderLoading)();
113+
} else if (viewState === 'ERROR') {
114+
invariant(lastErrorEvent != null, 'lastErrorEvent expected to be non-null');
115+
otherView = (renderError || defaultRenderError)(
116+
lastErrorEvent.domain,
117+
lastErrorEvent.code,
118+
lastErrorEvent.description,
247119
);
120+
} else if (viewState !== 'IDLE') {
121+
console.error(`RNCWebView invalid state encountered: ${viewState}`);
122+
}
248123

249-
const NativeWebView
250-
= (this.RnwVersionSupportsWebView2 && this.props.useWebView2)? RCTWebView2 : RCTWebView;
124+
const webViewStyles = [styles.container, styles.webView, style];
125+
const webViewContainerStyle = [styles.container, containerStyle];
126+
127+
const NativeWebView
128+
= (RnwVersionSupportsWebView2 && useWebView2)? RCTWebView2 : RCTWebView;
129+
130+
const webView = <NativeWebView
131+
key="webViewKey"
132+
{...otherProps}
133+
messagingEnabled={typeof onMessage === 'function'}
134+
onLoadingError={onLoadingError}
135+
onLoadingFinish={onLoadingFinish}
136+
onLoadingProgress={onLoadingProgress}
137+
onLoadingStart={onLoadingStart}
138+
onHttpError={onHttpError}
139+
onMessage={onMessage}
140+
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
141+
ref={webViewRef}
142+
// TODO: find a better way to type this.
143+
source={resolveAssetSource(source as ImageSourcePropType)}
144+
style={webViewStyles}
145+
cacheEnabled={cacheEnabled}
146+
{...nativeConfig?.props}
147+
/>
148+
149+
return (
150+
<View style={webViewContainerStyle}>
151+
{webView}
152+
{otherView}
153+
</View>
154+
);
155+
});
251156

252-
const webView = (
253-
<NativeWebView
254-
ref={this.webViewRef}
255-
key="webViewKey"
256-
{...otherProps}
257-
messagingEnabled={typeof onMessage === 'function'}
258-
onLoadingError={this.onLoadingError}
259-
onLoadingFinish={this.onLoadingFinish}
260-
onLoadingProgress={this.onLoadingProgress}
261-
onLoadingStart={this.onLoadingStart}
262-
onHttpError={this.onHttpError}
263-
onMessage={this.onMessage}
264-
onScroll={this.props.onScroll}
265-
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
266-
source={resolveAssetSource(this.props.source as ImageSourcePropType)}
267-
style={webViewStyles}
268-
{...nativeConfig.props}
269-
/>
270-
);
157+
// native implementation should return "true" only for Android 5+
158+
const isFileUploadSupported: () => Promise<boolean>
159+
= async () => false;
271160

272-
return (
273-
<View style={styles.container}>
274-
{webView}
275-
{otherView}
276-
</View>
277-
);
278-
}
161+
const WebView = Object.assign(WebViewComponent, {isFileUploadSupported});
279162

280-
}
163+
export default WebView;

0 commit comments

Comments
 (0)