Initial
|
@ -0,0 +1,49 @@
|
||||||
|
# OSX
|
||||||
|
#
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Xcode
|
||||||
|
#
|
||||||
|
build/
|
||||||
|
*.pbxuser
|
||||||
|
!default.pbxuser
|
||||||
|
*.mode1v3
|
||||||
|
!default.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
!default.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
!default.perspectivev3
|
||||||
|
xcuserdata
|
||||||
|
*.xccheckout
|
||||||
|
*.moved-aside
|
||||||
|
DerivedData
|
||||||
|
*.hmap
|
||||||
|
*.ipa
|
||||||
|
*.xcuserstate
|
||||||
|
project.xcworkspace
|
||||||
|
Pods/
|
||||||
|
|
||||||
|
# Android/IJ
|
||||||
|
#
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
local.properties
|
||||||
|
lib/android/src/main/gen
|
||||||
|
|
||||||
|
# node.js
|
||||||
|
#
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Rubygem bundles
|
||||||
|
#
|
||||||
|
bundles/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
|
@ -0,0 +1,492 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var ReactNative = require('react-native');
|
||||||
|
var {
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
WebView,
|
||||||
|
} = ReactNative;
|
||||||
|
|
||||||
|
var HEADER = '#3b5998';
|
||||||
|
var BGWASH = 'rgba(255,255,255,0.8)';
|
||||||
|
var DISABLED_WASH = 'rgba(255,255,255,0.25)';
|
||||||
|
|
||||||
|
var TEXT_INPUT_REF = 'urlInput';
|
||||||
|
var WEBVIEW_REF = 'webview';
|
||||||
|
var DEFAULT_URL = 'https://m.facebook.com';
|
||||||
|
const FILE_SYSTEM_ORIGIN_WHITE_LIST = ['file://*', 'http://*', 'https://*'];
|
||||||
|
|
||||||
|
class WebViewExample extends React.Component<{}, $FlowFixMeState> {
|
||||||
|
state = {
|
||||||
|
url: DEFAULT_URL,
|
||||||
|
status: 'No Page Loaded',
|
||||||
|
backButtonEnabled: false,
|
||||||
|
forwardButtonEnabled: false,
|
||||||
|
loading: true,
|
||||||
|
scalesPageToFit: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
inputText = '';
|
||||||
|
|
||||||
|
handleTextInputChange = event => {
|
||||||
|
var url = event.nativeEvent.text;
|
||||||
|
if (!/^[a-zA-Z-_]+:/.test(url)) {
|
||||||
|
url = 'http://' + url;
|
||||||
|
}
|
||||||
|
this.inputText = url;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.inputText = this.state.url;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container]}>
|
||||||
|
<View style={[styles.addressBarRow]}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={this.goBack}
|
||||||
|
style={
|
||||||
|
this.state.backButtonEnabled
|
||||||
|
? styles.navButton
|
||||||
|
: styles.disabledButton
|
||||||
|
}>
|
||||||
|
<Text>{'<'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={this.goForward}
|
||||||
|
style={
|
||||||
|
this.state.forwardButtonEnabled
|
||||||
|
? styles.navButton
|
||||||
|
: styles.disabledButton
|
||||||
|
}>
|
||||||
|
<Text>{'>'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TextInput
|
||||||
|
ref={TEXT_INPUT_REF}
|
||||||
|
autoCapitalize="none"
|
||||||
|
defaultValue={this.state.url}
|
||||||
|
onSubmitEditing={this.onSubmitEditing}
|
||||||
|
onChange={this.handleTextInputChange}
|
||||||
|
clearButtonMode="while-editing"
|
||||||
|
style={styles.addressBarTextInput}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity onPress={this.pressGoButton}>
|
||||||
|
<View style={styles.goButton}>
|
||||||
|
<Text>Go!</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<WebView
|
||||||
|
ref={WEBVIEW_REF}
|
||||||
|
automaticallyAdjustContentInsets={false}
|
||||||
|
style={styles.webView}
|
||||||
|
source={{ uri: this.state.url }}
|
||||||
|
javaScriptEnabled={true}
|
||||||
|
domStorageEnabled={true}
|
||||||
|
decelerationRate="normal"
|
||||||
|
onNavigationStateChange={this.onNavigationStateChange}
|
||||||
|
onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
|
||||||
|
startInLoadingState={true}
|
||||||
|
scalesPageToFit={this.state.scalesPageToFit}
|
||||||
|
/>
|
||||||
|
<View style={styles.statusBar}>
|
||||||
|
<Text style={styles.statusBarText}>{this.state.status}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack = () => {
|
||||||
|
this.refs[WEBVIEW_REF].goBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
goForward = () => {
|
||||||
|
this.refs[WEBVIEW_REF].goForward();
|
||||||
|
};
|
||||||
|
|
||||||
|
reload = () => {
|
||||||
|
this.refs[WEBVIEW_REF].reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest = event => {
|
||||||
|
// Implement any custom loading logic here, don't forget to return!
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
onNavigationStateChange = navState => {
|
||||||
|
this.setState({
|
||||||
|
backButtonEnabled: navState.canGoBack,
|
||||||
|
forwardButtonEnabled: navState.canGoForward,
|
||||||
|
url: navState.url,
|
||||||
|
status: navState.title,
|
||||||
|
loading: navState.loading,
|
||||||
|
scalesPageToFit: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmitEditing = event => {
|
||||||
|
this.pressGoButton();
|
||||||
|
};
|
||||||
|
|
||||||
|
pressGoButton = () => {
|
||||||
|
var url = this.inputText.toLowerCase();
|
||||||
|
if (url === this.state.url) {
|
||||||
|
this.reload();
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
url: url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// dismiss keyboard
|
||||||
|
this.refs[TEXT_INPUT_REF].blur();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Button extends React.Component<$FlowFixMeProps> {
|
||||||
|
_handlePress = () => {
|
||||||
|
if (this.props.enabled !== false && this.props.onPress) {
|
||||||
|
this.props.onPress();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<TouchableWithoutFeedback onPress={this._handlePress}>
|
||||||
|
<View style={styles.button}>
|
||||||
|
<Text>{this.props.text}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScaledWebView extends React.Component<{}, $FlowFixMeState> {
|
||||||
|
state = {
|
||||||
|
scalingEnabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<WebView
|
||||||
|
style={{
|
||||||
|
backgroundColor: BGWASH,
|
||||||
|
height: 200,
|
||||||
|
}}
|
||||||
|
source={{ uri: 'https://facebook.github.io/react/' }}
|
||||||
|
scalesPageToFit={this.state.scalingEnabled}
|
||||||
|
/>
|
||||||
|
<View style={styles.buttons}>
|
||||||
|
{this.state.scalingEnabled ? (
|
||||||
|
<Button
|
||||||
|
text="Scaling:ON"
|
||||||
|
enabled={true}
|
||||||
|
onPress={() => this.setState({ scalingEnabled: false })}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
text="Scaling:OFF"
|
||||||
|
enabled={true}
|
||||||
|
onPress={() => this.setState({ scalingEnabled: true })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessagingTest extends React.Component<{}, $FlowFixMeState> {
|
||||||
|
webview = null;
|
||||||
|
|
||||||
|
state = {
|
||||||
|
messagesReceivedFromWebView: 0,
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
onMessage = e =>
|
||||||
|
this.setState({
|
||||||
|
messagesReceivedFromWebView: this.state.messagesReceivedFromWebView + 1,
|
||||||
|
message: e.nativeEvent.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
postMessage = () => {
|
||||||
|
if (this.webview) {
|
||||||
|
this.webview.postMessage('"Hello" from React Native!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): React.Node {
|
||||||
|
const { messagesReceivedFromWebView, message } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, { height: 200 }]}>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text>
|
||||||
|
Messages received from web view: {messagesReceivedFromWebView}
|
||||||
|
</Text>
|
||||||
|
<Text>{message || '(No message)'}</Text>
|
||||||
|
<View style={styles.buttons}>
|
||||||
|
<Button
|
||||||
|
text="Send Message to Web View"
|
||||||
|
enabled
|
||||||
|
onPress={this.postMessage}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<WebView
|
||||||
|
ref={webview => {
|
||||||
|
this.webview = webview;
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
backgroundColor: BGWASH,
|
||||||
|
height: 100,
|
||||||
|
}}
|
||||||
|
originWhitelist={FILE_SYSTEM_ORIGIN_WHITE_LIST}
|
||||||
|
source={require('./messagingtest.html')}
|
||||||
|
onMessage={this.onMessage}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InjectJS extends React.Component<{}> {
|
||||||
|
webview = null;
|
||||||
|
injectJS = () => {
|
||||||
|
const script = 'document.write("Injected JS ")';
|
||||||
|
if (this.webview) {
|
||||||
|
this.webview.injectJavaScript(script);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<WebView
|
||||||
|
ref={webview => {
|
||||||
|
this.webview = webview;
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
backgroundColor: BGWASH,
|
||||||
|
height: 300,
|
||||||
|
}}
|
||||||
|
source={{ uri: 'https://www.facebook.com' }}
|
||||||
|
scalesPageToFit={true}
|
||||||
|
/>
|
||||||
|
<View style={styles.buttons}>
|
||||||
|
<Button text="Inject JS" enabled onPress={this.injectJS} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: HEADER,
|
||||||
|
},
|
||||||
|
addressBarRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
webView: {
|
||||||
|
backgroundColor: BGWASH,
|
||||||
|
height: 350,
|
||||||
|
},
|
||||||
|
addressBarTextInput: {
|
||||||
|
backgroundColor: BGWASH,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
borderRadius: 3,
|
||||||
|
borderWidth: 1,
|
||||||
|
height: 24,
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingTop: 3,
|
||||||
|
paddingBottom: 3,
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
navButton: {
|
||||||
|
width: 20,
|
||||||
|
padding: 3,
|
||||||
|
marginRight: 3,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: BGWASH,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
borderRadius: 3,
|
||||||
|
},
|
||||||
|
disabledButton: {
|
||||||
|
width: 20,
|
||||||
|
padding: 3,
|
||||||
|
marginRight: 3,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: DISABLED_WASH,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
borderRadius: 3,
|
||||||
|
},
|
||||||
|
goButton: {
|
||||||
|
height: 24,
|
||||||
|
padding: 3,
|
||||||
|
marginLeft: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: BGWASH,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
borderRadius: 3,
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
},
|
||||||
|
statusBar: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingLeft: 5,
|
||||||
|
height: 22,
|
||||||
|
},
|
||||||
|
statusBarText: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 13,
|
||||||
|
},
|
||||||
|
spinner: {
|
||||||
|
width: 20,
|
||||||
|
marginRight: 6,
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
height: 30,
|
||||||
|
backgroundColor: 'black',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
flex: 0.5,
|
||||||
|
width: 0,
|
||||||
|
margin: 5,
|
||||||
|
borderColor: 'gray',
|
||||||
|
borderWidth: 1,
|
||||||
|
backgroundColor: 'gray',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const HTML = `
|
||||||
|
<!DOCTYPE html>\n
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Hello Static World</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;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
padding: 45px;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #33f;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello Static World</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports.displayName = (undefined: ?string);
|
||||||
|
exports.title = '<WebView>';
|
||||||
|
exports.description = 'Base component to display web content';
|
||||||
|
exports.examples = [
|
||||||
|
{
|
||||||
|
title: 'Simple Browser',
|
||||||
|
render(): React.Element<any> {
|
||||||
|
return <WebViewExample />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Scale Page to Fit',
|
||||||
|
render(): React.Element<any> {
|
||||||
|
return <ScaledWebView />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Bundled HTML',
|
||||||
|
render(): React.Element<any> {
|
||||||
|
return (
|
||||||
|
<WebView
|
||||||
|
style={{
|
||||||
|
backgroundColor: BGWASH,
|
||||||
|
height: 100,
|
||||||
|
}}
|
||||||
|
originWhitelist={FILE_SYSTEM_ORIGIN_WHITE_LIST}
|
||||||
|
source={require('./helloworld.html')}
|
||||||
|
scalesPageToFit={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Static HTML',
|
||||||
|
render(): React.Element<any> {
|
||||||
|
return (
|
||||||
|
<WebView
|
||||||
|
style={{
|
||||||
|
backgroundColor: BGWASH,
|
||||||
|
height: 100,
|
||||||
|
}}
|
||||||
|
source={{ html: HTML }}
|
||||||
|
scalesPageToFit={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'POST Test',
|
||||||
|
render(): React.Element<any> {
|
||||||
|
return (
|
||||||
|
<WebView
|
||||||
|
style={{
|
||||||
|
backgroundColor: BGWASH,
|
||||||
|
height: 100,
|
||||||
|
}}
|
||||||
|
source={{
|
||||||
|
uri: 'http://www.posttestserver.com/post.php',
|
||||||
|
method: 'POST',
|
||||||
|
body: 'foo=bar&bar=foo',
|
||||||
|
}}
|
||||||
|
scalesPageToFit={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Messaging Test',
|
||||||
|
render(): React.Element<any> {
|
||||||
|
return <MessagingTest />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Inject JavaScript',
|
||||||
|
render(): React.Element<any> {
|
||||||
|
return <InjectJS />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["react-native"]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
[android]
|
||||||
|
target = Google Inc.:Google APIs:23
|
||||||
|
|
||||||
|
[maven_repositories]
|
||||||
|
central = https://repo1.maven.org/maven2
|
|
@ -0,0 +1,67 @@
|
||||||
|
[ignore]
|
||||||
|
; We fork some components by platform
|
||||||
|
.*/*[.]android.js
|
||||||
|
|
||||||
|
; Ignore "BUCK" generated dirs
|
||||||
|
<PROJECT_ROOT>/\.buckd/
|
||||||
|
|
||||||
|
; Ignore unexpected extra "@providesModule"
|
||||||
|
.*/node_modules/.*/node_modules/fbjs/.*
|
||||||
|
|
||||||
|
; Ignore duplicate module providers
|
||||||
|
; For RN Apps installed via npm, "Libraries" folder is inside
|
||||||
|
; "node_modules/react-native" but in the source repo it is in the root
|
||||||
|
.*/Libraries/react-native/React.js
|
||||||
|
|
||||||
|
; Ignore polyfills
|
||||||
|
.*/Libraries/polyfills/.*
|
||||||
|
|
||||||
|
; Ignore metro
|
||||||
|
.*/node_modules/metro/.*
|
||||||
|
|
||||||
|
[include]
|
||||||
|
|
||||||
|
[libs]
|
||||||
|
node_modules/react-native/Libraries/react-native/react-native-interface.js
|
||||||
|
node_modules/react-native/flow/
|
||||||
|
node_modules/react-native/flow-github/
|
||||||
|
|
||||||
|
[options]
|
||||||
|
emoji=true
|
||||||
|
|
||||||
|
module.system=haste
|
||||||
|
module.system.haste.use_name_reducers=true
|
||||||
|
# get basename
|
||||||
|
module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1'
|
||||||
|
# strip .js or .js.flow suffix
|
||||||
|
module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1'
|
||||||
|
# strip .ios suffix
|
||||||
|
module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1'
|
||||||
|
module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1'
|
||||||
|
module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1'
|
||||||
|
module.system.haste.paths.blacklist=.*/__tests__/.*
|
||||||
|
module.system.haste.paths.blacklist=.*/__mocks__/.*
|
||||||
|
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/Animated/src/polyfills/.*
|
||||||
|
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/Libraries/.*
|
||||||
|
|
||||||
|
munge_underscores=true
|
||||||
|
|
||||||
|
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
|
||||||
|
|
||||||
|
module.file_ext=.js
|
||||||
|
module.file_ext=.jsx
|
||||||
|
module.file_ext=.json
|
||||||
|
module.file_ext=.native.js
|
||||||
|
|
||||||
|
suppress_type=$FlowIssue
|
||||||
|
suppress_type=$FlowFixMe
|
||||||
|
suppress_type=$FlowFixMeProps
|
||||||
|
suppress_type=$FlowFixMeState
|
||||||
|
|
||||||
|
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||||
|
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||||
|
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||||
|
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
||||||
|
|
||||||
|
[version]
|
||||||
|
^0.75.0
|
|
@ -0,0 +1 @@
|
||||||
|
*.pbxproj -text
|
|
@ -0,0 +1,56 @@
|
||||||
|
# OSX
|
||||||
|
#
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Xcode
|
||||||
|
#
|
||||||
|
build/
|
||||||
|
*.pbxuser
|
||||||
|
!default.pbxuser
|
||||||
|
*.mode1v3
|
||||||
|
!default.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
!default.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
!default.perspectivev3
|
||||||
|
xcuserdata
|
||||||
|
*.xccheckout
|
||||||
|
*.moved-aside
|
||||||
|
DerivedData
|
||||||
|
*.hmap
|
||||||
|
*.ipa
|
||||||
|
*.xcuserstate
|
||||||
|
project.xcworkspace
|
||||||
|
|
||||||
|
# Android/IntelliJ
|
||||||
|
#
|
||||||
|
build/
|
||||||
|
.idea
|
||||||
|
.gradle
|
||||||
|
local.properties
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# node.js
|
||||||
|
#
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# BUCK
|
||||||
|
buck-out/
|
||||||
|
\.buckd/
|
||||||
|
*.keystore
|
||||||
|
|
||||||
|
# fastlane
|
||||||
|
#
|
||||||
|
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||||
|
# screenshots whenever they are needed.
|
||||||
|
# For more information about the recommended setup visit:
|
||||||
|
# https://docs.fastlane.tools/best-practices/source-control/
|
||||||
|
|
||||||
|
*/fastlane/report.xml
|
||||||
|
*/fastlane/Preview.html
|
||||||
|
*/fastlane/screenshots
|
||||||
|
|
||||||
|
# Bundle artifact
|
||||||
|
*.jsbundle
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* Sample React Native App
|
||||||
|
* https://github.com/facebook/react-native
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Platform, StyleSheet } from 'react-native';
|
||||||
|
import { WebView } from '../'
|
||||||
|
|
||||||
|
const instructions = Platform.select({
|
||||||
|
ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
|
||||||
|
android:
|
||||||
|
'Double tap R on your keyboard to reload,\n' +
|
||||||
|
'Shake or press menu button for dev menu',
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props = {};
|
||||||
|
export default class App extends Component<Props> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<WebView style={styles.container} source={{ url: "https://infinite.red/ignite" }} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#F5FCFF',
|
||||||
|
},
|
||||||
|
welcome: {
|
||||||
|
fontSize: 20,
|
||||||
|
textAlign: 'center',
|
||||||
|
margin: 10,
|
||||||
|
},
|
||||||
|
instructions: {
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#333333',
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,65 @@
|
||||||
|
# To learn about Buck see [Docs](https://buckbuild.com/).
|
||||||
|
# To run your application with Buck:
|
||||||
|
# - install Buck
|
||||||
|
# - `npm start` - to start the packager
|
||||||
|
# - `cd android`
|
||||||
|
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
|
||||||
|
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
|
||||||
|
# - `buck install -r android/app` - compile, install and run application
|
||||||
|
#
|
||||||
|
|
||||||
|
lib_deps = []
|
||||||
|
|
||||||
|
for jarfile in glob(['libs/*.jar']):
|
||||||
|
name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')]
|
||||||
|
lib_deps.append(':' + name)
|
||||||
|
prebuilt_jar(
|
||||||
|
name = name,
|
||||||
|
binary_jar = jarfile,
|
||||||
|
)
|
||||||
|
|
||||||
|
for aarfile in glob(['libs/*.aar']):
|
||||||
|
name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')]
|
||||||
|
lib_deps.append(':' + name)
|
||||||
|
android_prebuilt_aar(
|
||||||
|
name = name,
|
||||||
|
aar = aarfile,
|
||||||
|
)
|
||||||
|
|
||||||
|
android_library(
|
||||||
|
name = "all-libs",
|
||||||
|
exported_deps = lib_deps,
|
||||||
|
)
|
||||||
|
|
||||||
|
android_library(
|
||||||
|
name = "app-code",
|
||||||
|
srcs = glob([
|
||||||
|
"src/main/java/**/*.java",
|
||||||
|
]),
|
||||||
|
deps = [
|
||||||
|
":all-libs",
|
||||||
|
":build_config",
|
||||||
|
":res",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
android_build_config(
|
||||||
|
name = "build_config",
|
||||||
|
package = "com.webbiesample",
|
||||||
|
)
|
||||||
|
|
||||||
|
android_resource(
|
||||||
|
name = "res",
|
||||||
|
package = "com.webbiesample",
|
||||||
|
res = "src/main/res",
|
||||||
|
)
|
||||||
|
|
||||||
|
android_binary(
|
||||||
|
name = "app",
|
||||||
|
keystore = "//android/keystores:debug",
|
||||||
|
manifest = "src/main/AndroidManifest.xml",
|
||||||
|
package_type = "debug",
|
||||||
|
deps = [
|
||||||
|
":app-code",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,155 @@
|
||||||
|
apply plugin: "com.android.application"
|
||||||
|
|
||||||
|
import com.android.build.OutputFile
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
|
||||||
|
* and bundleReleaseJsAndAssets).
|
||||||
|
* These basically call `react-native bundle` with the correct arguments during the Android build
|
||||||
|
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
|
||||||
|
* bundle directly from the development server. Below you can see all the possible configurations
|
||||||
|
* and their defaults. If you decide to add a configuration block, make sure to add it before the
|
||||||
|
* `apply from: "../../node_modules/react-native/react.gradle"` line.
|
||||||
|
*
|
||||||
|
* project.ext.react = [
|
||||||
|
* // the name of the generated asset file containing your JS bundle
|
||||||
|
* bundleAssetName: "index.android.bundle",
|
||||||
|
*
|
||||||
|
* // the entry file for bundle generation
|
||||||
|
* entryFile: "index.android.js",
|
||||||
|
*
|
||||||
|
* // whether to bundle JS and assets in debug mode
|
||||||
|
* bundleInDebug: false,
|
||||||
|
*
|
||||||
|
* // whether to bundle JS and assets in release mode
|
||||||
|
* bundleInRelease: true,
|
||||||
|
*
|
||||||
|
* // whether to bundle JS and assets in another build variant (if configured).
|
||||||
|
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
|
||||||
|
* // The configuration property can be in the following formats
|
||||||
|
* // 'bundleIn${productFlavor}${buildType}'
|
||||||
|
* // 'bundleIn${buildType}'
|
||||||
|
* // bundleInFreeDebug: true,
|
||||||
|
* // bundleInPaidRelease: true,
|
||||||
|
* // bundleInBeta: true,
|
||||||
|
*
|
||||||
|
* // whether to disable dev mode in custom build variants (by default only disabled in release)
|
||||||
|
* // for example: to disable dev mode in the staging build type (if configured)
|
||||||
|
* devDisabledInStaging: true,
|
||||||
|
* // The configuration property can be in the following formats
|
||||||
|
* // 'devDisabledIn${productFlavor}${buildType}'
|
||||||
|
* // 'devDisabledIn${buildType}'
|
||||||
|
*
|
||||||
|
* // the root of your project, i.e. where "package.json" lives
|
||||||
|
* root: "../../",
|
||||||
|
*
|
||||||
|
* // where to put the JS bundle asset in debug mode
|
||||||
|
* jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
|
||||||
|
*
|
||||||
|
* // where to put the JS bundle asset in release mode
|
||||||
|
* jsBundleDirRelease: "$buildDir/intermediates/assets/release",
|
||||||
|
*
|
||||||
|
* // where to put drawable resources / React Native assets, e.g. the ones you use via
|
||||||
|
* // require('./image.png')), in debug mode
|
||||||
|
* resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
|
||||||
|
*
|
||||||
|
* // where to put drawable resources / React Native assets, e.g. the ones you use via
|
||||||
|
* // require('./image.png')), in release mode
|
||||||
|
* resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
|
||||||
|
*
|
||||||
|
* // by default the gradle tasks are skipped if none of the JS files or assets change; this means
|
||||||
|
* // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
|
||||||
|
* // date; if you have any other folders that you want to ignore for performance reasons (gradle
|
||||||
|
* // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
|
||||||
|
* // for example, you might want to remove it from here.
|
||||||
|
* inputExcludes: ["android/**", "ios/**"],
|
||||||
|
*
|
||||||
|
* // override which node gets called and with what additional arguments
|
||||||
|
* nodeExecutableAndArgs: ["node"],
|
||||||
|
*
|
||||||
|
* // supply additional arguments to the packager
|
||||||
|
* extraPackagerArgs: []
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
|
||||||
|
project.ext.react = [
|
||||||
|
entryFile: "index.js"
|
||||||
|
]
|
||||||
|
|
||||||
|
apply from: "../../node_modules/react-native/react.gradle"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set this to true to create two separate APKs instead of one:
|
||||||
|
* - An APK that only works on ARM devices
|
||||||
|
* - An APK that only works on x86 devices
|
||||||
|
* The advantage is the size of the APK is reduced by about 4MB.
|
||||||
|
* Upload all the APKs to the Play Store and people will download
|
||||||
|
* the correct one based on the CPU architecture of their device.
|
||||||
|
*/
|
||||||
|
def enableSeparateBuildPerCPUArchitecture = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run Proguard to shrink the Java bytecode in release builds.
|
||||||
|
*/
|
||||||
|
def enableProguardInReleaseBuilds = false
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||||
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.webbiesample"
|
||||||
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
ndk {
|
||||||
|
abiFilters "armeabi-v7a", "x86"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
reset()
|
||||||
|
enable enableSeparateBuildPerCPUArchitecture
|
||||||
|
universalApk false // If true, also generate a universal APK
|
||||||
|
include "armeabi-v7a", "x86"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled enableProguardInReleaseBuilds
|
||||||
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// applicationVariants are e.g. debug, release
|
||||||
|
applicationVariants.all { variant ->
|
||||||
|
variant.outputs.each { output ->
|
||||||
|
// For each separate APK per architecture, set a unique version code as described here:
|
||||||
|
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
|
||||||
|
def versionCodes = ["armeabi-v7a":1, "x86":2]
|
||||||
|
def abi = output.getFilter(OutputFile.ABI)
|
||||||
|
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||||
|
output.versionCodeOverride =
|
||||||
|
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile fileTree(dir: "libs", include: ["*.jar"])
|
||||||
|
compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
|
||||||
|
|
||||||
|
compile project(':ReactAndroid')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run this once to be able to run the application with BUCK
|
||||||
|
// puts all compile dependencies into folder libs for BUCK to use
|
||||||
|
task copyDownloadableDepsToLibs(type: Copy) {
|
||||||
|
from configurations.compile
|
||||||
|
into 'libs'
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations.all {
|
||||||
|
exclude group: 'com.facebook.react', module: 'react-native'
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# By default, the flags in this file are appended to flags specified
|
||||||
|
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||||
|
# You can edit the include path and order by changing the proguardFiles
|
||||||
|
# directive in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
|
@ -0,0 +1,26 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.webbiesample">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".MainApplication"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.webbiesample;
|
||||||
|
|
||||||
|
import com.facebook.react.ReactActivity;
|
||||||
|
|
||||||
|
public class MainActivity extends ReactActivity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the main component registered from JavaScript.
|
||||||
|
* This is used to schedule rendering of the component.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected String getMainComponentName() {
|
||||||
|
return "WebbieSample";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.webbiesample;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
|
||||||
|
import com.facebook.react.ReactApplication;
|
||||||
|
import com.facebook.react.ReactNativeHost;
|
||||||
|
import com.facebook.react.ReactPackage;
|
||||||
|
import com.facebook.react.shell.MainReactPackage;
|
||||||
|
import com.facebook.soloader.SoLoader;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MainApplication extends Application implements ReactApplication {
|
||||||
|
|
||||||
|
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
|
||||||
|
@Override
|
||||||
|
public boolean getUseDeveloperSupport() {
|
||||||
|
return BuildConfig.DEBUG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<ReactPackage> getPackages() {
|
||||||
|
return Arrays.<ReactPackage>asList(
|
||||||
|
new MainReactPackage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getJSMainModuleName() {
|
||||||
|
return "index";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReactNativeHost getReactNativeHost() {
|
||||||
|
return mReactNativeHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
SoLoader.init(this, /* native exopackage */ false);
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">WebbieSample</string>
|
||||||
|
</resources>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:1.3.1'
|
||||||
|
classpath 'de.undercouch:gradle-download-task:3.1.2'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
maven {
|
||||||
|
url 'https://maven.google.com/'
|
||||||
|
name 'Google'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:2.3.3'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
jcenter()
|
||||||
|
maven {
|
||||||
|
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||||
|
url "$rootDir/../node_modules/react-native/android"
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
url 'https://maven.google.com/'
|
||||||
|
name 'Google'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
buildToolsVersion = "26.0.3"
|
||||||
|
minSdkVersion = 16
|
||||||
|
compileSdkVersion = 26
|
||||||
|
targetSdkVersion = 26
|
||||||
|
supportLibVersion = "26.1.0"
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||||
|
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
|
||||||
|
android.useDeprecatedNdk=true
|
|
@ -0,0 +1,5 @@
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-all.zip
|
|
@ -0,0 +1,164 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn ( ) {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die ( ) {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# For Cygwin, ensure paths are in UNIX format before anything is touched.
|
||||||
|
if $cygwin ; then
|
||||||
|
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >&-
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >&-
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||||
|
function splitJvmOpts() {
|
||||||
|
JVM_OPTS=("$@")
|
||||||
|
}
|
||||||
|
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||||
|
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
|
@ -0,0 +1,90 @@
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windowz variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
goto execute
|
||||||
|
|
||||||
|
:4NT_args
|
||||||
|
@rem Get arguments from the 4NT Shell from JP Software
|
||||||
|
set CMD_LINE_ARGS=%$
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
|
@ -0,0 +1,8 @@
|
||||||
|
keystore(
|
||||||
|
name = "debug",
|
||||||
|
properties = "debug.keystore.properties",
|
||||||
|
store = "debug.keystore",
|
||||||
|
visibility = [
|
||||||
|
"PUBLIC",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,4 @@
|
||||||
|
key.store=debug.keystore
|
||||||
|
key.alias=androiddebugkey
|
||||||
|
key.store.password=android
|
||||||
|
key.alias.password=android
|
|
@ -0,0 +1,7 @@
|
||||||
|
rootProject.name = 'WebbieSample'
|
||||||
|
|
||||||
|
include ':app'
|
||||||
|
include ':ReactAndroid'
|
||||||
|
|
||||||
|
project(':ReactAndroid').projectDir = new File(
|
||||||
|
rootProject.projectDir, '../node_modules/react-native/ReactAndroid')
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"name": "WebbieSample",
|
||||||
|
"displayName": "WebbieSample"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
/** @format */
|
||||||
|
|
||||||
|
import {AppRegistry} from 'react-native';
|
||||||
|
import App from './App';
|
||||||
|
import {name as appName} from './app.json';
|
||||||
|
|
||||||
|
AppRegistry.registerComponent(appName, () => App);
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>armv7</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string></string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<!--See http://ste.vn/2015/06/10/configuring-app-transport-security-ios-9-osx-10-11/ -->
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionDomains</key>
|
||||||
|
<dict>
|
||||||
|
<key>localhost</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>BNDL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,129 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "0820"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "NO"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "2D2A28121D9B038B00D4039D"
|
||||||
|
BuildableName = "libReact.a"
|
||||||
|
BlueprintName = "React-tvOS"
|
||||||
|
ReferencedContainer = "container:../node_modules/react-native/React/React.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
|
||||||
|
BuildableName = "WebbieSample-tvOS.app"
|
||||||
|
BlueprintName = "WebbieSample-tvOS"
|
||||||
|
ReferencedContainer = "container:WebbieSample.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "NO"
|
||||||
|
buildForArchiving = "NO"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "2D02E48F1E0B4A5D006451C7"
|
||||||
|
BuildableName = "WebbieSample-tvOSTests.xctest"
|
||||||
|
BlueprintName = "WebbieSample-tvOSTests"
|
||||||
|
ReferencedContainer = "container:WebbieSample.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "2D02E48F1E0B4A5D006451C7"
|
||||||
|
BuildableName = "WebbieSample-tvOSTests.xctest"
|
||||||
|
BlueprintName = "WebbieSample-tvOSTests"
|
||||||
|
ReferencedContainer = "container:WebbieSample.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
|
||||||
|
BuildableName = "WebbieSample-tvOS.app"
|
||||||
|
BlueprintName = "WebbieSample-tvOS"
|
||||||
|
ReferencedContainer = "container:WebbieSample.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
|
||||||
|
BuildableName = "WebbieSample-tvOS.app"
|
||||||
|
BlueprintName = "WebbieSample-tvOS"
|
||||||
|
ReferencedContainer = "container:WebbieSample.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
|
||||||
|
BuildableName = "WebbieSample-tvOS.app"
|
||||||
|
BlueprintName = "WebbieSample-tvOS"
|
||||||
|
ReferencedContainer = "container:WebbieSample.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
|
@ -0,0 +1,129 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "0620"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "NO"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "83CBBA2D1A601D0E00E9B192"
|
||||||
|
BuildableName = "libReact.a"
|
||||||
|
BlueprintName = "React"
|
||||||
|
ReferencedContainer = "container:../node_modules/react-native/React/React.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
|
BuildableName = "WebbieSample.app"
|
||||||
|
BlueprintName = "WebbieSample"
|
||||||
|
ReferencedContainer = "container:WebbieSample.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "NO"
|
||||||
|
buildForArchiving = "NO"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
||||||
|
BuildableName = "WebbieSampleTests.xctest"
|
||||||
|
BlueprintName = "WebbieSampleTests"
|
||||||
|
ReferencedContainer = "container:WebbieSample.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
||||||
|
BuildableName = "WebbieSampleTests.xctest"
|
||||||
|
BlueprintName = "WebbieSampleTests"
|
||||||
|
ReferencedContainer = "container:WebbieSample.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
|
BuildableName = "WebbieSample.app"
|
||||||
|
BlueprintName = "WebbieSample"
|
||||||
|
ReferencedContainer = "container:WebbieSample.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
|
BuildableName = "WebbieSample.app"
|
||||||
|
BlueprintName = "WebbieSample"
|
||||||
|
ReferencedContainer = "container:WebbieSample.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
|
BuildableName = "WebbieSample.app"
|
||||||
|
BlueprintName = "WebbieSample"
|
||||||
|
ReferencedContainer = "container:WebbieSample.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||||
|
|
||||||
|
@property (nonatomic, strong) UIWindow *window;
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import "AppDelegate.h"
|
||||||
|
|
||||||
|
#import <React/RCTBundleURLProvider.h>
|
||||||
|
#import <React/RCTRootView.h>
|
||||||
|
|
||||||
|
@implementation AppDelegate
|
||||||
|
|
||||||
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||||
|
{
|
||||||
|
NSURL *jsCodeLocation;
|
||||||
|
|
||||||
|
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
|
||||||
|
|
||||||
|
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
|
||||||
|
moduleName:@"WebbieSample"
|
||||||
|
initialProperties:nil
|
||||||
|
launchOptions:launchOptions];
|
||||||
|
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
|
||||||
|
|
||||||
|
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||||
|
UIViewController *rootViewController = [UIViewController new];
|
||||||
|
rootViewController.view = rootView;
|
||||||
|
self.window.rootViewController = rootViewController;
|
||||||
|
[self.window makeKeyAndVisible];
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="7702" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7701"/>
|
||||||
|
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Powered by React Native" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
|
||||||
|
<rect key="frame" x="20" y="439" width="441" height="21"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="WebbieSample" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
|
||||||
|
<rect key="frame" x="20" y="140" width="441" height="43"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
|
||||||
|
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC"/>
|
||||||
|
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l"/>
|
||||||
|
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0"/>
|
||||||
|
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9"/>
|
||||||
|
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g"/>
|
||||||
|
</constraints>
|
||||||
|
<nil key="simulatedStatusBarMetrics"/>
|
||||||
|
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||||
|
<point key="canvasLocation" x="548" y="455"/>
|
||||||
|
</view>
|
||||||
|
</objects>
|
||||||
|
</document>
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "29x29",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "29x29",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "40x40",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "40x40",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "60x60",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "60x60",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>WebbieSample</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>armv7</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string></string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<!--See http://ste.vn/2015/06/10/configuring-app-transport-security-ios-9-osx-10-11/ -->
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionDomains</key>
|
||||||
|
<dict>
|
||||||
|
<key>localhost</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
#import "AppDelegate.h"
|
||||||
|
|
||||||
|
int main(int argc, char * argv[]) {
|
||||||
|
@autoreleasepool {
|
||||||
|
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>BNDL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#import <XCTest/XCTest.h>
|
||||||
|
|
||||||
|
#import <React/RCTLog.h>
|
||||||
|
#import <React/RCTRootView.h>
|
||||||
|
|
||||||
|
#define TIMEOUT_SECONDS 600
|
||||||
|
#define TEXT_TO_LOOK_FOR @"Welcome to React Native!"
|
||||||
|
|
||||||
|
@interface WebbieSampleTests : XCTestCase
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation WebbieSampleTests
|
||||||
|
|
||||||
|
- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test
|
||||||
|
{
|
||||||
|
if (test(view)) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
for (UIView *subview in [view subviews]) {
|
||||||
|
if ([self findSubviewInView:subview matching:test]) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testRendersWelcomeScreen
|
||||||
|
{
|
||||||
|
UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];
|
||||||
|
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
|
||||||
|
BOOL foundElement = NO;
|
||||||
|
|
||||||
|
__block NSString *redboxError = nil;
|
||||||
|
RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
|
||||||
|
if (level >= RCTLogLevelError) {
|
||||||
|
redboxError = message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
|
||||||
|
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
||||||
|
[[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
||||||
|
|
||||||
|
foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) {
|
||||||
|
if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
RCTSetLogFunction(RCTDefaultLogFunction);
|
||||||
|
|
||||||
|
XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
|
||||||
|
XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "WebbieSample",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "node node_modules/react-native/local-cli/cli.js start",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "16.4.1",
|
||||||
|
"react-native": "../../../react-native",
|
||||||
|
"react-native-webview": "../../"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-jest": "23.4.2",
|
||||||
|
"babel-preset-react-native": "^5",
|
||||||
|
"jest": "23.4.2",
|
||||||
|
"react-test-renderer": "16.4.1"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "react-native"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var ReactNative = require('react-native');
|
||||||
|
var {StyleSheet, Text, TouchableHighlight, View, WebView} = ReactNative;
|
||||||
|
|
||||||
|
var RCTNetworking = require('RCTNetworking');
|
||||||
|
|
||||||
|
class XHRExampleCookies extends React.Component<any, any> {
|
||||||
|
cancelled: boolean;
|
||||||
|
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
this.cancelled = false;
|
||||||
|
this.state = {
|
||||||
|
status: '',
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setCookie(domain: string) {
|
||||||
|
var {a, b} = this.state;
|
||||||
|
var url = `https://${domain}/cookies/set?a=${a}&b=${b}`;
|
||||||
|
fetch(url).then(response => {
|
||||||
|
this.setStatus(`Cookies a=${a}, b=${b} set`);
|
||||||
|
this.refreshWebview();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
status: 'Setting cookies...',
|
||||||
|
a: a + 1,
|
||||||
|
b: b + 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getCookies(domain: string) {
|
||||||
|
fetch(`https://${domain}/cookies`)
|
||||||
|
.then(response => {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
this.setStatus(
|
||||||
|
`Got cookies ${JSON.stringify(data.cookies)} from server`,
|
||||||
|
);
|
||||||
|
this.refreshWebview();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setStatus('Getting cookies...');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCookies() {
|
||||||
|
RCTNetworking.clearCookies(cleared => {
|
||||||
|
this.setStatus('Cookies cleared, had cookies=' + cleared.toString());
|
||||||
|
this.refreshWebview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshWebview() {
|
||||||
|
this.refs.webview.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(status: string) {
|
||||||
|
this.setState({status});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<TouchableHighlight
|
||||||
|
style={styles.wrapper}
|
||||||
|
onPress={this.setCookie.bind(this, 'httpbin.org')}>
|
||||||
|
<View style={styles.button}>
|
||||||
|
<Text>Set cookie</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableHighlight>
|
||||||
|
<TouchableHighlight
|
||||||
|
style={styles.wrapper}
|
||||||
|
onPress={this.setCookie.bind(this, 'eu.httpbin.org')}>
|
||||||
|
<View style={styles.button}>
|
||||||
|
<Text>Set cookie (EU)</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableHighlight>
|
||||||
|
<TouchableHighlight
|
||||||
|
style={styles.wrapper}
|
||||||
|
onPress={this.getCookies.bind(this, 'httpbin.org')}>
|
||||||
|
<View style={styles.button}>
|
||||||
|
<Text>Get cookies</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableHighlight>
|
||||||
|
<TouchableHighlight
|
||||||
|
style={styles.wrapper}
|
||||||
|
onPress={this.getCookies.bind(this, 'eu.httpbin.org')}>
|
||||||
|
<View style={styles.button}>
|
||||||
|
<Text>Get cookies (EU)</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableHighlight>
|
||||||
|
<TouchableHighlight
|
||||||
|
style={styles.wrapper}
|
||||||
|
onPress={this.clearCookies.bind(this)}>
|
||||||
|
<View style={styles.button}>
|
||||||
|
<Text>Clear cookies</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableHighlight>
|
||||||
|
<Text>{this.state.status}</Text>
|
||||||
|
<TouchableHighlight
|
||||||
|
style={styles.wrapper}
|
||||||
|
onPress={this.refreshWebview.bind(this)}>
|
||||||
|
<View style={styles.button}>
|
||||||
|
<Text>Refresh Webview</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableHighlight>
|
||||||
|
<WebView
|
||||||
|
ref="webview"
|
||||||
|
source={{uri: 'http://httpbin.org/cookies'}}
|
||||||
|
style={{height: 100}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var styles = StyleSheet.create({
|
||||||
|
wrapper: {
|
||||||
|
borderRadius: 5,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
backgroundColor: '#eeeeee',
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = XHRExampleCookies;
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "react-native-webview",
|
||||||
|
"description": "React Native WebView component for iOS + Android",
|
||||||
|
"main": "src/index",
|
||||||
|
"authors": [
|
||||||
|
"Jamon Holmgren <jamonholmgren@gmail.com>"
|
||||||
|
],
|
||||||
|
"version": "0.0.1",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.0",
|
||||||
|
"react-native": "^0.55"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"escape-string-regexp": "^1.0.5"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 23
|
||||||
|
buildToolsVersion "25.0.0"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 16
|
||||||
|
targetSdkVersion 22
|
||||||
|
versionCode 19
|
||||||
|
versionName "1.1.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile 'com.facebook.react:react-native:+'
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.infinitered.irwebview">
|
||||||
|
</manifest>
|
|
@ -0,0 +1,693 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.infinitered.webview;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import com.facebook.react.uimanager.UIManagerModule;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Picture;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.ViewGroup.LayoutParams;
|
||||||
|
import android.webkit.ConsoleMessage;
|
||||||
|
import android.webkit.CookieManager;
|
||||||
|
import android.webkit.GeolocationPermissions;
|
||||||
|
import android.webkit.JavascriptInterface;
|
||||||
|
import android.webkit.ValueCallback;
|
||||||
|
import android.webkit.WebChromeClient;
|
||||||
|
import android.webkit.WebSettings;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
import android.webkit.WebViewClient;
|
||||||
|
import com.facebook.common.logging.FLog;
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.bridge.LifecycleEventListener;
|
||||||
|
import com.facebook.react.bridge.ReactContext;
|
||||||
|
import com.facebook.react.bridge.ReadableArray;
|
||||||
|
import com.facebook.react.bridge.ReadableMap;
|
||||||
|
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
import com.facebook.react.common.MapBuilder;
|
||||||
|
import com.facebook.react.common.ReactConstants;
|
||||||
|
import com.facebook.react.common.build.ReactBuildConfig;
|
||||||
|
import com.facebook.react.module.annotations.ReactModule;
|
||||||
|
import com.facebook.react.uimanager.SimpleViewManager;
|
||||||
|
import com.facebook.react.uimanager.ThemedReactContext;
|
||||||
|
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||||
|
import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
|
||||||
|
import com.facebook.react.uimanager.events.Event;
|
||||||
|
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||||
|
import com.infinitered.webview.events.TopLoadingErrorEvent;
|
||||||
|
import com.infinitered.webview.events.TopLoadingFinishEvent;
|
||||||
|
import com.infinitered.webview.events.TopLoadingStartEvent;
|
||||||
|
import com.infinitered.webview.events.TopMessageEvent;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages instances of {@link WebView}
|
||||||
|
*
|
||||||
|
* Can accept following commands:
|
||||||
|
* - GO_BACK
|
||||||
|
* - GO_FORWARD
|
||||||
|
* - RELOAD
|
||||||
|
*
|
||||||
|
* {@link WebView} instances could emit following direct events:
|
||||||
|
* - topLoadingFinish
|
||||||
|
* - topLoadingStart
|
||||||
|
* - topLoadingError
|
||||||
|
*
|
||||||
|
* Each event will carry the following properties:
|
||||||
|
* - target - view's react tag
|
||||||
|
* - url - url set for the webview
|
||||||
|
* - loading - whether webview is in a loading state
|
||||||
|
* - title - title of the current page
|
||||||
|
* - canGoBack - boolean, whether there is anything on a history stack to go back
|
||||||
|
* - canGoForward - boolean, whether it is possible to request GO_FORWARD command
|
||||||
|
*/
|
||||||
|
@ReactModule(name = ReactWebViewManager.REACT_CLASS)
|
||||||
|
public class ReactWebViewManager extends SimpleViewManager<WebView> {
|
||||||
|
|
||||||
|
protected static final String REACT_CLASS = "RCTWebView";
|
||||||
|
|
||||||
|
protected static final String HTML_ENCODING = "UTF-8";
|
||||||
|
protected static final String HTML_MIME_TYPE = "text/html";
|
||||||
|
protected static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE";
|
||||||
|
|
||||||
|
protected static final String HTTP_METHOD_POST = "POST";
|
||||||
|
|
||||||
|
public static final int COMMAND_GO_BACK = 1;
|
||||||
|
public static final int COMMAND_GO_FORWARD = 2;
|
||||||
|
public static final int COMMAND_RELOAD = 3;
|
||||||
|
public static final int COMMAND_STOP_LOADING = 4;
|
||||||
|
public static final int COMMAND_POST_MESSAGE = 5;
|
||||||
|
public static final int COMMAND_INJECT_JAVASCRIPT = 6;
|
||||||
|
|
||||||
|
// Use `webView.loadUrl("about:blank")` to reliably reset the view
|
||||||
|
// state and release page resources (including any running JavaScript).
|
||||||
|
protected static final String BLANK_URL = "about:blank";
|
||||||
|
|
||||||
|
protected WebViewConfig mWebViewConfig;
|
||||||
|
protected @Nullable WebView.PictureListener mPictureListener;
|
||||||
|
|
||||||
|
protected static class ReactWebViewClient extends WebViewClient {
|
||||||
|
|
||||||
|
protected boolean mLastLoadFailed = false;
|
||||||
|
protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
|
||||||
|
protected @Nullable List<Pattern> mOriginWhitelist;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageFinished(WebView webView, String url) {
|
||||||
|
super.onPageFinished(webView, url);
|
||||||
|
|
||||||
|
if (!mLastLoadFailed) {
|
||||||
|
ReactWebView reactWebView = (ReactWebView) webView;
|
||||||
|
reactWebView.callInjectedJavaScript();
|
||||||
|
reactWebView.linkBridge();
|
||||||
|
emitFinishEvent(webView, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageStarted(WebView webView, String url, Bitmap favicon) {
|
||||||
|
super.onPageStarted(webView, url, favicon);
|
||||||
|
mLastLoadFailed = false;
|
||||||
|
|
||||||
|
dispatchEvent(
|
||||||
|
webView,
|
||||||
|
new TopLoadingStartEvent(
|
||||||
|
webView.getId(),
|
||||||
|
createWebViewEvent(webView, url)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||||
|
if (url.equals(BLANK_URL)) return false;
|
||||||
|
|
||||||
|
// url blacklisting
|
||||||
|
if (mUrlPrefixesForDefaultIntent != null && mUrlPrefixesForDefaultIntent.size() > 0) {
|
||||||
|
ArrayList<Object> urlPrefixesForDefaultIntent =
|
||||||
|
mUrlPrefixesForDefaultIntent.toArrayList();
|
||||||
|
for (Object urlPrefix : urlPrefixesForDefaultIntent) {
|
||||||
|
if (url.startsWith((String) urlPrefix)) {
|
||||||
|
launchIntent(view.getContext(), url);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mOriginWhitelist != null && shouldHandleURL(mOriginWhitelist, url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
launchIntent(view.getContext(), url);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void launchIntent(Context context, String url) {
|
||||||
|
try {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||||
|
context.startActivity(intent);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldHandleURL(List<Pattern> originWhitelist, String url) {
|
||||||
|
Uri uri = Uri.parse(url);
|
||||||
|
String scheme = uri.getScheme() != null ? uri.getScheme() : "";
|
||||||
|
String authority = uri.getAuthority() != null ? uri.getAuthority() : "";
|
||||||
|
String urlToCheck = scheme + "://" + authority;
|
||||||
|
for (Pattern pattern : originWhitelist) {
|
||||||
|
if (pattern.matcher(urlToCheck).matches()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceivedError(
|
||||||
|
WebView webView,
|
||||||
|
int errorCode,
|
||||||
|
String description,
|
||||||
|
String failingUrl) {
|
||||||
|
super.onReceivedError(webView, errorCode, description, failingUrl);
|
||||||
|
mLastLoadFailed = true;
|
||||||
|
|
||||||
|
// In case of an error JS side expect to get a finish event first, and then get an error event
|
||||||
|
// Android WebView does it in the opposite way, so we need to simulate that behavior
|
||||||
|
emitFinishEvent(webView, failingUrl);
|
||||||
|
|
||||||
|
WritableMap eventData = createWebViewEvent(webView, failingUrl);
|
||||||
|
eventData.putDouble("code", errorCode);
|
||||||
|
eventData.putString("description", description);
|
||||||
|
|
||||||
|
dispatchEvent(
|
||||||
|
webView,
|
||||||
|
new TopLoadingErrorEvent(webView.getId(), eventData));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void emitFinishEvent(WebView webView, String url) {
|
||||||
|
dispatchEvent(
|
||||||
|
webView,
|
||||||
|
new TopLoadingFinishEvent(
|
||||||
|
webView.getId(),
|
||||||
|
createWebViewEvent(webView, url)));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected WritableMap createWebViewEvent(WebView webView, String url) {
|
||||||
|
WritableMap event = Arguments.createMap();
|
||||||
|
event.putDouble("target", webView.getId());
|
||||||
|
// Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks
|
||||||
|
// like onPageFinished
|
||||||
|
event.putString("url", url);
|
||||||
|
event.putBoolean("loading", !mLastLoadFailed && webView.getProgress() != 100);
|
||||||
|
event.putString("title", webView.getTitle());
|
||||||
|
event.putBoolean("canGoBack", webView.canGoBack());
|
||||||
|
event.putBoolean("canGoForward", webView.canGoForward());
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
|
||||||
|
mUrlPrefixesForDefaultIntent = specialUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOriginWhitelist(List<Pattern> originWhitelist) {
|
||||||
|
mOriginWhitelist = originWhitelist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclass of {@link WebView} that implements {@link LifecycleEventListener} interface in order
|
||||||
|
* to call {@link WebView#destroy} on activity destroy event and also to clear the client
|
||||||
|
*/
|
||||||
|
protected static class ReactWebView extends WebView implements LifecycleEventListener {
|
||||||
|
protected @Nullable String injectedJS;
|
||||||
|
protected boolean messagingEnabled = false;
|
||||||
|
protected @Nullable ReactWebViewClient mReactWebViewClient;
|
||||||
|
|
||||||
|
protected class ReactWebViewBridge {
|
||||||
|
ReactWebView mContext;
|
||||||
|
|
||||||
|
ReactWebViewBridge(ReactWebView c) {
|
||||||
|
mContext = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
public void postMessage(String message) {
|
||||||
|
mContext.onMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebView must be created with an context of the current activity
|
||||||
|
*
|
||||||
|
* Activity Context is required for creation of dialogs internally by WebView
|
||||||
|
* Reactive Native needed for access to ReactNative internal system functionality
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public ReactWebView(ThemedReactContext reactContext) {
|
||||||
|
super(reactContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHostResume() {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHostPause() {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHostDestroy() {
|
||||||
|
cleanupCallbacksAndDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setWebViewClient(WebViewClient client) {
|
||||||
|
super.setWebViewClient(client);
|
||||||
|
mReactWebViewClient = (ReactWebViewClient)client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable ReactWebViewClient getReactWebViewClient() {
|
||||||
|
return mReactWebViewClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInjectedJavaScript(@Nullable String js) {
|
||||||
|
injectedJS = js;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ReactWebViewBridge createReactWebViewBridge(ReactWebView webView) {
|
||||||
|
return new ReactWebViewBridge(webView);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessagingEnabled(boolean enabled) {
|
||||||
|
if (messagingEnabled == enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messagingEnabled = enabled;
|
||||||
|
if (enabled) {
|
||||||
|
addJavascriptInterface(createReactWebViewBridge(this), BRIDGE_NAME);
|
||||||
|
linkBridge();
|
||||||
|
} else {
|
||||||
|
removeJavascriptInterface(BRIDGE_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void callInjectedJavaScript() {
|
||||||
|
if (getSettings().getJavaScriptEnabled() &&
|
||||||
|
injectedJS != null &&
|
||||||
|
!TextUtils.isEmpty(injectedJS)) {
|
||||||
|
loadUrl("javascript:(function() {\n" + injectedJS + ";\n})();");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void linkBridge() {
|
||||||
|
if (messagingEnabled) {
|
||||||
|
if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
// See isNative in lodash
|
||||||
|
String testPostMessageNative = "String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
|
||||||
|
evaluateJavascript(testPostMessageNative, new ValueCallback<String>() {
|
||||||
|
@Override
|
||||||
|
public void onReceiveValue(String value) {
|
||||||
|
if (value.equals("true")) {
|
||||||
|
FLog.w(ReactConstants.TAG, "Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUrl("javascript:(" +
|
||||||
|
"window.originalPostMessage = window.postMessage," +
|
||||||
|
"window.postMessage = function(data) {" +
|
||||||
|
BRIDGE_NAME + ".postMessage(String(data));" +
|
||||||
|
"}" +
|
||||||
|
")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onMessage(String message) {
|
||||||
|
dispatchEvent(this, new TopMessageEvent(this.getId(), message));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void cleanupCallbacksAndDestroy() {
|
||||||
|
setWebViewClient(null);
|
||||||
|
destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReactWebViewManager() {
|
||||||
|
mWebViewConfig = new WebViewConfig() {
|
||||||
|
public void configWebView(WebView webView) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReactWebViewManager(WebViewConfig webViewConfig) {
|
||||||
|
mWebViewConfig = webViewConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return REACT_CLASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ReactWebView createReactWebViewInstance(ThemedReactContext reactContext) {
|
||||||
|
return new ReactWebView(reactContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
protected WebView createViewInstance(ThemedReactContext reactContext) {
|
||||||
|
ReactWebView webView = createReactWebViewInstance(reactContext);
|
||||||
|
webView.setWebChromeClient(new WebChromeClient() {
|
||||||
|
@Override
|
||||||
|
public boolean onConsoleMessage(ConsoleMessage message) {
|
||||||
|
if (ReactBuildConfig.DEBUG) {
|
||||||
|
return super.onConsoleMessage(message);
|
||||||
|
}
|
||||||
|
// Ignore console logs in non debug builds.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
|
||||||
|
callback.invoke(origin, true, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reactContext.addLifecycleEventListener(webView);
|
||||||
|
mWebViewConfig.configWebView(webView);
|
||||||
|
WebSettings settings = webView.getSettings();
|
||||||
|
settings.setBuiltInZoomControls(true);
|
||||||
|
settings.setDisplayZoomControls(false);
|
||||||
|
settings.setDomStorageEnabled(true);
|
||||||
|
|
||||||
|
settings.setAllowFileAccess(false);
|
||||||
|
settings.setAllowContentAccess(false);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
settings.setAllowFileAccessFromFileURLs(false);
|
||||||
|
setAllowUniversalAccessFromFileURLs(webView, false);
|
||||||
|
}
|
||||||
|
setMixedContentMode(webView, "never");
|
||||||
|
|
||||||
|
// Fixes broken full-screen modals/galleries due to body height being 0.
|
||||||
|
webView.setLayoutParams(
|
||||||
|
new LayoutParams(LayoutParams.MATCH_PARENT,
|
||||||
|
LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
setGeolocationEnabled(webView, false);
|
||||||
|
if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
WebView.setWebContentsDebuggingEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return webView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "javaScriptEnabled")
|
||||||
|
public void setJavaScriptEnabled(WebView view, boolean enabled) {
|
||||||
|
view.getSettings().setJavaScriptEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "thirdPartyCookiesEnabled")
|
||||||
|
public void setThirdPartyCookiesEnabled(WebView view, boolean enabled) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
CookieManager.getInstance().setAcceptThirdPartyCookies(view, enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "scalesPageToFit")
|
||||||
|
public void setScalesPageToFit(WebView view, boolean enabled) {
|
||||||
|
view.getSettings().setUseWideViewPort(!enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "domStorageEnabled")
|
||||||
|
public void setDomStorageEnabled(WebView view, boolean enabled) {
|
||||||
|
view.getSettings().setDomStorageEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "userAgent")
|
||||||
|
public void setUserAgent(WebView view, @Nullable String userAgent) {
|
||||||
|
if (userAgent != null) {
|
||||||
|
// TODO(8496850): Fix incorrect behavior when property is unset (uA == null)
|
||||||
|
view.getSettings().setUserAgentString(userAgent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "mediaPlaybackRequiresUserAction")
|
||||||
|
public void setMediaPlaybackRequiresUserAction(WebView view, boolean requires) {
|
||||||
|
view.getSettings().setMediaPlaybackRequiresUserGesture(requires);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "allowUniversalAccessFromFileURLs")
|
||||||
|
public void setAllowUniversalAccessFromFileURLs(WebView view, boolean allow) {
|
||||||
|
view.getSettings().setAllowUniversalAccessFromFileURLs(allow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "saveFormDataDisabled")
|
||||||
|
public void setSaveFormDataDisabled(WebView view, boolean disable) {
|
||||||
|
view.getSettings().setSaveFormData(!disable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "injectedJavaScript")
|
||||||
|
public void setInjectedJavaScript(WebView view, @Nullable String injectedJavaScript) {
|
||||||
|
((ReactWebView) view).setInjectedJavaScript(injectedJavaScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "messagingEnabled")
|
||||||
|
public void setMessagingEnabled(WebView view, boolean enabled) {
|
||||||
|
((ReactWebView) view).setMessagingEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "source")
|
||||||
|
public void setSource(WebView view, @Nullable ReadableMap source) {
|
||||||
|
if (source != null) {
|
||||||
|
if (source.hasKey("html")) {
|
||||||
|
String html = source.getString("html");
|
||||||
|
if (source.hasKey("baseUrl")) {
|
||||||
|
view.loadDataWithBaseURL(
|
||||||
|
source.getString("baseUrl"), html, HTML_MIME_TYPE, HTML_ENCODING, null);
|
||||||
|
} else {
|
||||||
|
view.loadData(html, HTML_MIME_TYPE, HTML_ENCODING);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (source.hasKey("uri")) {
|
||||||
|
String url = source.getString("uri");
|
||||||
|
String previousUrl = view.getUrl();
|
||||||
|
if (previousUrl != null && previousUrl.equals(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (source.hasKey("method")) {
|
||||||
|
String method = source.getString("method");
|
||||||
|
if (method.equals(HTTP_METHOD_POST)) {
|
||||||
|
byte[] postData = null;
|
||||||
|
if (source.hasKey("body")) {
|
||||||
|
String body = source.getString("body");
|
||||||
|
try {
|
||||||
|
postData = body.getBytes("UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
postData = body.getBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (postData == null) {
|
||||||
|
postData = new byte[0];
|
||||||
|
}
|
||||||
|
view.postUrl(url, postData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HashMap<String, String> headerMap = new HashMap<>();
|
||||||
|
if (source.hasKey("headers")) {
|
||||||
|
ReadableMap headers = source.getMap("headers");
|
||||||
|
ReadableMapKeySetIterator iter = headers.keySetIterator();
|
||||||
|
while (iter.hasNextKey()) {
|
||||||
|
String key = iter.nextKey();
|
||||||
|
if ("user-agent".equals(key.toLowerCase(Locale.ENGLISH))) {
|
||||||
|
if (view.getSettings() != null) {
|
||||||
|
view.getSettings().setUserAgentString(headers.getString(key));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headerMap.put(key, headers.getString(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.loadUrl(url, headerMap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.loadUrl(BLANK_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "onContentSizeChange")
|
||||||
|
public void setOnContentSizeChange(WebView view, boolean sendContentSizeChangeEvents) {
|
||||||
|
if (sendContentSizeChangeEvents) {
|
||||||
|
view.setPictureListener(getPictureListener());
|
||||||
|
} else {
|
||||||
|
view.setPictureListener(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "mixedContentMode")
|
||||||
|
public void setMixedContentMode(WebView view, @Nullable String mixedContentMode) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
if (mixedContentMode == null || "never".equals(mixedContentMode)) {
|
||||||
|
view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
|
||||||
|
} else if ("always".equals(mixedContentMode)) {
|
||||||
|
view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
||||||
|
} else if ("compatibility".equals(mixedContentMode)) {
|
||||||
|
view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "urlPrefixesForDefaultIntent")
|
||||||
|
public void setUrlPrefixesForDefaultIntent(
|
||||||
|
WebView view,
|
||||||
|
@Nullable ReadableArray urlPrefixesForDefaultIntent) {
|
||||||
|
ReactWebViewClient client = ((ReactWebView) view).getReactWebViewClient();
|
||||||
|
if (client != null && urlPrefixesForDefaultIntent != null) {
|
||||||
|
client.setUrlPrefixesForDefaultIntent(urlPrefixesForDefaultIntent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "geolocationEnabled")
|
||||||
|
public void setGeolocationEnabled(
|
||||||
|
WebView view,
|
||||||
|
@Nullable Boolean isGeolocationEnabled) {
|
||||||
|
view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "originWhitelist")
|
||||||
|
public void setOriginWhitelist(
|
||||||
|
WebView view,
|
||||||
|
@Nullable ReadableArray originWhitelist) {
|
||||||
|
ReactWebViewClient client = ((ReactWebView) view).getReactWebViewClient();
|
||||||
|
if (client != null && originWhitelist != null) {
|
||||||
|
List<Pattern> whiteList = new LinkedList<>();
|
||||||
|
for (int i = 0 ; i < originWhitelist.size() ; i++) {
|
||||||
|
whiteList.add(Pattern.compile(originWhitelist.getString(i)));
|
||||||
|
}
|
||||||
|
client.setOriginWhitelist(whiteList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
|
||||||
|
// Do not register default touch emitter and let WebView implementation handle touches
|
||||||
|
view.setWebViewClient(new ReactWebViewClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Map<String, Integer> getCommandsMap() {
|
||||||
|
return MapBuilder.of(
|
||||||
|
"goBack", COMMAND_GO_BACK,
|
||||||
|
"goForward", COMMAND_GO_FORWARD,
|
||||||
|
"reload", COMMAND_RELOAD,
|
||||||
|
"stopLoading", COMMAND_STOP_LOADING,
|
||||||
|
"postMessage", COMMAND_POST_MESSAGE,
|
||||||
|
"injectJavaScript", COMMAND_INJECT_JAVASCRIPT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray args) {
|
||||||
|
switch (commandId) {
|
||||||
|
case COMMAND_GO_BACK:
|
||||||
|
root.goBack();
|
||||||
|
break;
|
||||||
|
case COMMAND_GO_FORWARD:
|
||||||
|
root.goForward();
|
||||||
|
break;
|
||||||
|
case COMMAND_RELOAD:
|
||||||
|
root.reload();
|
||||||
|
break;
|
||||||
|
case COMMAND_STOP_LOADING:
|
||||||
|
root.stopLoading();
|
||||||
|
break;
|
||||||
|
case COMMAND_POST_MESSAGE:
|
||||||
|
try {
|
||||||
|
JSONObject eventInitDict = new JSONObject();
|
||||||
|
eventInitDict.put("data", args.getString(0));
|
||||||
|
root.loadUrl("javascript:(function () {" +
|
||||||
|
"var event;" +
|
||||||
|
"var data = " + eventInitDict.toString() + ";" +
|
||||||
|
"try {" +
|
||||||
|
"event = new MessageEvent('message', data);" +
|
||||||
|
"} catch (e) {" +
|
||||||
|
"event = document.createEvent('MessageEvent');" +
|
||||||
|
"event.initMessageEvent('message', true, true, data.data, data.origin, data.lastEventId, data.source);" +
|
||||||
|
"}" +
|
||||||
|
"document.dispatchEvent(event);" +
|
||||||
|
"})();");
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case COMMAND_INJECT_JAVASCRIPT:
|
||||||
|
root.loadUrl("javascript:" + args.getString(0));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDropViewInstance(WebView webView) {
|
||||||
|
super.onDropViewInstance(webView);
|
||||||
|
((ThemedReactContext) webView.getContext()).removeLifecycleEventListener((ReactWebView) webView);
|
||||||
|
((ReactWebView) webView).cleanupCallbacksAndDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected WebView.PictureListener getPictureListener() {
|
||||||
|
if (mPictureListener == null) {
|
||||||
|
mPictureListener = new WebView.PictureListener() {
|
||||||
|
@Override
|
||||||
|
public void onNewPicture(WebView webView, Picture picture) {
|
||||||
|
dispatchEvent(
|
||||||
|
webView,
|
||||||
|
new ContentSizeChangeEvent(
|
||||||
|
webView.getId(),
|
||||||
|
webView.getWidth(),
|
||||||
|
webView.getContentHeight()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return mPictureListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void dispatchEvent(WebView webView, Event event) {
|
||||||
|
ReactContext reactContext = (ReactContext) webView.getContext();
|
||||||
|
EventDispatcher eventDispatcher =
|
||||||
|
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
|
||||||
|
eventDispatcher.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.infinitered.webview;
|
||||||
|
|
||||||
|
import android.webkit.WebView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement this interface in order to config your {@link WebView}. An instance of that
|
||||||
|
* implementation will have to be given as a constructor argument to {@link ReactWebViewManager}.
|
||||||
|
*/
|
||||||
|
public interface WebViewConfig {
|
||||||
|
|
||||||
|
void configWebView(WebView webView);
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.react.views.webview.events;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
import com.facebook.react.uimanager.events.Event;
|
||||||
|
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when there is an error in loading.
|
||||||
|
*/
|
||||||
|
public class TopLoadingErrorEvent extends Event<TopLoadingErrorEvent> {
|
||||||
|
|
||||||
|
public static final String EVENT_NAME = "topLoadingError";
|
||||||
|
private WritableMap mEventData;
|
||||||
|
|
||||||
|
public TopLoadingErrorEvent(int viewId, WritableMap eventData) {
|
||||||
|
super(viewId);
|
||||||
|
mEventData = eventData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEventName() {
|
||||||
|
return EVENT_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canCoalesce() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public short getCoalescingKey() {
|
||||||
|
// All events for a given view can be coalesced.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispatch(RCTEventEmitter rctEventEmitter) {
|
||||||
|
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), mEventData);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.react.views.webview.events;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
import com.facebook.react.uimanager.events.Event;
|
||||||
|
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when loading is completed.
|
||||||
|
*/
|
||||||
|
public class TopLoadingFinishEvent extends Event<TopLoadingFinishEvent> {
|
||||||
|
|
||||||
|
public static final String EVENT_NAME = "topLoadingFinish";
|
||||||
|
private WritableMap mEventData;
|
||||||
|
|
||||||
|
public TopLoadingFinishEvent(int viewId, WritableMap eventData) {
|
||||||
|
super(viewId);
|
||||||
|
mEventData = eventData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEventName() {
|
||||||
|
return EVENT_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canCoalesce() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public short getCoalescingKey() {
|
||||||
|
// All events for a given view can be coalesced.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispatch(RCTEventEmitter rctEventEmitter) {
|
||||||
|
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), mEventData);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.react.views.webview.events;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
import com.facebook.react.uimanager.events.Event;
|
||||||
|
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when loading has started
|
||||||
|
*/
|
||||||
|
public class TopLoadingStartEvent extends Event<TopLoadingStartEvent> {
|
||||||
|
|
||||||
|
public static final String EVENT_NAME = "topLoadingStart";
|
||||||
|
private WritableMap mEventData;
|
||||||
|
|
||||||
|
public TopLoadingStartEvent(int viewId, WritableMap eventData) {
|
||||||
|
super(viewId);
|
||||||
|
mEventData = eventData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEventName() {
|
||||||
|
return EVENT_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canCoalesce() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public short getCoalescingKey() {
|
||||||
|
// All events for a given view can be coalesced.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispatch(RCTEventEmitter rctEventEmitter) {
|
||||||
|
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), mEventData);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.react.views.webview.events;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.uimanager.events.Event;
|
||||||
|
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when there is an error in loading.
|
||||||
|
*/
|
||||||
|
public class TopMessageEvent extends Event<TopMessageEvent> {
|
||||||
|
|
||||||
|
public static final String EVENT_NAME = "topMessage";
|
||||||
|
private final String mData;
|
||||||
|
|
||||||
|
public TopMessageEvent(int viewId, String data) {
|
||||||
|
super(viewId);
|
||||||
|
mData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEventName() {
|
||||||
|
return EVENT_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canCoalesce() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public short getCoalescingKey() {
|
||||||
|
// All events for a given view can be coalesced.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispatch(RCTEventEmitter rctEventEmitter) {
|
||||||
|
WritableMap data = Arguments.createMap();
|
||||||
|
data.putString("data", mData);
|
||||||
|
rctEventEmitter.receiveEvent(getViewTag(), EVENT_NAME, data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <React/RCTView.h>
|
||||||
|
|
||||||
|
@class RCTWebView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special scheme used to pass messages to the injectedJavaScript
|
||||||
|
* code without triggering a page load. Usage:
|
||||||
|
*
|
||||||
|
* window.location.href = RCTJSNavigationScheme + '://hello'
|
||||||
|
*/
|
||||||
|
extern NSString *const RCTJSNavigationScheme;
|
||||||
|
|
||||||
|
@protocol RCTWebViewDelegate <NSObject>
|
||||||
|
|
||||||
|
- (BOOL)webView:(RCTWebView *)webView
|
||||||
|
shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
|
||||||
|
withCallback:(RCTDirectEventBlock)callback;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface RCTWebView : RCTView
|
||||||
|
|
||||||
|
@property (nonatomic, weak) id<RCTWebViewDelegate> delegate;
|
||||||
|
|
||||||
|
@property (nonatomic, copy) NSDictionary *source;
|
||||||
|
@property (nonatomic, assign) UIEdgeInsets contentInset;
|
||||||
|
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
|
||||||
|
@property (nonatomic, assign) BOOL messagingEnabled;
|
||||||
|
@property (nonatomic, copy) NSString *injectedJavaScript;
|
||||||
|
@property (nonatomic, assign) BOOL scalesPageToFit;
|
||||||
|
|
||||||
|
- (void)goForward;
|
||||||
|
- (void)goBack;
|
||||||
|
- (void)reload;
|
||||||
|
- (void)stopLoading;
|
||||||
|
- (void)postMessage:(NSString *)message;
|
||||||
|
- (void)injectJavaScript:(NSString *)script;
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,351 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import "RCTWebView.h"
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
#import "RCTAutoInsetsProtocol.h"
|
||||||
|
#import "RCTConvert.h"
|
||||||
|
#import "RCTEventDispatcher.h"
|
||||||
|
#import "RCTLog.h"
|
||||||
|
#import "RCTUtils.h"
|
||||||
|
#import "RCTView.h"
|
||||||
|
#import "UIView+React.h"
|
||||||
|
|
||||||
|
NSString *const RCTJSNavigationScheme = @"react-js-navigation";
|
||||||
|
|
||||||
|
static NSString *const kPostMessageHost = @"postMessage";
|
||||||
|
|
||||||
|
@interface RCTWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>
|
||||||
|
|
||||||
|
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
|
||||||
|
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
|
||||||
|
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
|
||||||
|
@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
|
||||||
|
@property (nonatomic, copy) RCTDirectEventBlock onMessage;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation RCTWebView
|
||||||
|
{
|
||||||
|
UIWebView *_webView;
|
||||||
|
NSString *_injectedJavaScript;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc
|
||||||
|
{
|
||||||
|
_webView.delegate = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithFrame:(CGRect)frame
|
||||||
|
{
|
||||||
|
if ((self = [super initWithFrame:frame])) {
|
||||||
|
super.backgroundColor = [UIColor clearColor];
|
||||||
|
_automaticallyAdjustContentInsets = YES;
|
||||||
|
_contentInset = UIEdgeInsetsZero;
|
||||||
|
_webView = [[UIWebView alloc] initWithFrame:self.bounds];
|
||||||
|
_webView.delegate = self;
|
||||||
|
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
|
||||||
|
if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) {
|
||||||
|
_webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
[self addSubview:_webView];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
||||||
|
|
||||||
|
- (void)goForward
|
||||||
|
{
|
||||||
|
[_webView goForward];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)goBack
|
||||||
|
{
|
||||||
|
[_webView goBack];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)reload
|
||||||
|
{
|
||||||
|
NSURLRequest *request = [RCTConvert NSURLRequest:self.source];
|
||||||
|
if (request.URL && !_webView.request.URL.absoluteString.length) {
|
||||||
|
[_webView loadRequest:request];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[_webView reload];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)stopLoading
|
||||||
|
{
|
||||||
|
[_webView stopLoading];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)postMessage:(NSString *)message
|
||||||
|
{
|
||||||
|
NSDictionary *eventInitDict = @{
|
||||||
|
@"data": message,
|
||||||
|
};
|
||||||
|
NSString *source = [NSString
|
||||||
|
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
|
||||||
|
RCTJSONStringify(eventInitDict, NULL)
|
||||||
|
];
|
||||||
|
[_webView stringByEvaluatingJavaScriptFromString:source];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)injectJavaScript:(NSString *)script
|
||||||
|
{
|
||||||
|
[_webView stringByEvaluatingJavaScriptFromString:script];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setSource:(NSDictionary *)source
|
||||||
|
{
|
||||||
|
if (![_source isEqualToDictionary:source]) {
|
||||||
|
_source = [source copy];
|
||||||
|
|
||||||
|
// Check for a static html source first
|
||||||
|
NSString *html = [RCTConvert NSString:source[@"html"]];
|
||||||
|
if (html) {
|
||||||
|
NSURL *baseURL = [RCTConvert NSURL:source[@"baseUrl"]];
|
||||||
|
if (!baseURL) {
|
||||||
|
baseURL = [NSURL URLWithString:@"about:blank"];
|
||||||
|
}
|
||||||
|
[_webView loadHTMLString:html baseURL:baseURL];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSURLRequest *request = [RCTConvert NSURLRequest:source];
|
||||||
|
// Because of the way React works, as pages redirect, we actually end up
|
||||||
|
// passing the redirect urls back here, so we ignore them if trying to load
|
||||||
|
// the same url. We'll expose a call to 'reload' to allow a user to load
|
||||||
|
// the existing page.
|
||||||
|
if ([request.URL isEqual:_webView.request.URL]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!request.URL) {
|
||||||
|
// Clear the webview
|
||||||
|
[_webView loadHTMLString:@"" baseURL:nil];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[_webView loadRequest:request];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)layoutSubviews
|
||||||
|
{
|
||||||
|
[super layoutSubviews];
|
||||||
|
_webView.frame = self.bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setContentInset:(UIEdgeInsets)contentInset
|
||||||
|
{
|
||||||
|
_contentInset = contentInset;
|
||||||
|
[RCTView autoAdjustInsetsForView:self
|
||||||
|
withScrollView:_webView.scrollView
|
||||||
|
updateOffset:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setScalesPageToFit:(BOOL)scalesPageToFit
|
||||||
|
{
|
||||||
|
if (_webView.scalesPageToFit != scalesPageToFit) {
|
||||||
|
_webView.scalesPageToFit = scalesPageToFit;
|
||||||
|
[_webView reload];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)scalesPageToFit
|
||||||
|
{
|
||||||
|
return _webView.scalesPageToFit;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||||
|
{
|
||||||
|
CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor);
|
||||||
|
self.opaque = _webView.opaque = (alpha == 1.0);
|
||||||
|
_webView.backgroundColor = backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIColor *)backgroundColor
|
||||||
|
{
|
||||||
|
return _webView.backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSMutableDictionary<NSString *, id> *)baseEvent
|
||||||
|
{
|
||||||
|
NSMutableDictionary<NSString *, id> *event = [[NSMutableDictionary alloc] initWithDictionary:@{
|
||||||
|
@"url": _webView.request.URL.absoluteString ?: @"",
|
||||||
|
@"loading" : @(_webView.loading),
|
||||||
|
@"title": [_webView stringByEvaluatingJavaScriptFromString:@"document.title"],
|
||||||
|
@"canGoBack": @(_webView.canGoBack),
|
||||||
|
@"canGoForward" : @(_webView.canGoForward),
|
||||||
|
}];
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)refreshContentInset
|
||||||
|
{
|
||||||
|
[RCTView autoAdjustInsetsForView:self
|
||||||
|
withScrollView:_webView.scrollView
|
||||||
|
updateOffset:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - UIWebViewDelegate methods
|
||||||
|
|
||||||
|
- (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
|
||||||
|
navigationType:(UIWebViewNavigationType)navigationType
|
||||||
|
{
|
||||||
|
BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme];
|
||||||
|
|
||||||
|
static NSDictionary<NSNumber *, NSString *> *navigationTypes;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
navigationTypes = @{
|
||||||
|
@(UIWebViewNavigationTypeLinkClicked): @"click",
|
||||||
|
@(UIWebViewNavigationTypeFormSubmitted): @"formsubmit",
|
||||||
|
@(UIWebViewNavigationTypeBackForward): @"backforward",
|
||||||
|
@(UIWebViewNavigationTypeReload): @"reload",
|
||||||
|
@(UIWebViewNavigationTypeFormResubmitted): @"formresubmit",
|
||||||
|
@(UIWebViewNavigationTypeOther): @"other",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// skip this for the JS Navigation handler
|
||||||
|
if (!isJSNavigation && _onShouldStartLoadWithRequest) {
|
||||||
|
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||||
|
[event addEntriesFromDictionary: @{
|
||||||
|
@"url": (request.URL).absoluteString,
|
||||||
|
@"navigationType": navigationTypes[@(navigationType)]
|
||||||
|
}];
|
||||||
|
if (![self.delegate webView:self
|
||||||
|
shouldStartLoadForRequest:event
|
||||||
|
withCallback:_onShouldStartLoadWithRequest]) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_onLoadingStart) {
|
||||||
|
// We have this check to filter out iframe requests and whatnot
|
||||||
|
BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];
|
||||||
|
if (isTopFrame) {
|
||||||
|
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||||
|
[event addEntriesFromDictionary: @{
|
||||||
|
@"url": (request.URL).absoluteString,
|
||||||
|
@"navigationType": navigationTypes[@(navigationType)]
|
||||||
|
}];
|
||||||
|
_onLoadingStart(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJSNavigation && [request.URL.host isEqualToString:kPostMessageHost]) {
|
||||||
|
NSString *data = request.URL.query;
|
||||||
|
data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
||||||
|
data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
||||||
|
|
||||||
|
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||||
|
[event addEntriesFromDictionary: @{
|
||||||
|
@"data": data,
|
||||||
|
}];
|
||||||
|
|
||||||
|
NSString *source = @"document.dispatchEvent(new MessageEvent('message:received'));";
|
||||||
|
|
||||||
|
[_webView stringByEvaluatingJavaScriptFromString:source];
|
||||||
|
|
||||||
|
_onMessage(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// JS Navigation handler
|
||||||
|
return !isJSNavigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error
|
||||||
|
{
|
||||||
|
if (_onLoadingError) {
|
||||||
|
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
|
||||||
|
// NSURLErrorCancelled is reported when a page has a redirect OR if you load
|
||||||
|
// a new URL in the WebView before the previous one came back. We can just
|
||||||
|
// ignore these since they aren't real errors.
|
||||||
|
// http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102) {
|
||||||
|
// Error code 102 "Frame load interrupted" is raised by the UIWebView if
|
||||||
|
// its delegate returns FALSE from webView:shouldStartLoadWithRequest:navigationType
|
||||||
|
// when the URL is from an http redirect. This is a common pattern when
|
||||||
|
// implementing OAuth with a WebView.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||||
|
[event addEntriesFromDictionary:@{
|
||||||
|
@"domain": error.domain,
|
||||||
|
@"code": @(error.code),
|
||||||
|
@"description": error.localizedDescription,
|
||||||
|
}];
|
||||||
|
_onLoadingError(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)webViewDidFinishLoad:(UIWebView *)webView
|
||||||
|
{
|
||||||
|
if (_messagingEnabled) {
|
||||||
|
#if RCT_DEV
|
||||||
|
// See isNative in lodash
|
||||||
|
NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
|
||||||
|
BOOL postMessageIsNative = [
|
||||||
|
[webView stringByEvaluatingJavaScriptFromString:testPostMessageNative]
|
||||||
|
isEqualToString:@"true"
|
||||||
|
];
|
||||||
|
if (!postMessageIsNative) {
|
||||||
|
RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
NSString *source = [NSString stringWithFormat:
|
||||||
|
@"(function() {"
|
||||||
|
"window.originalPostMessage = window.postMessage;"
|
||||||
|
|
||||||
|
"var messageQueue = [];"
|
||||||
|
"var messagePending = false;"
|
||||||
|
|
||||||
|
"function processQueue() {"
|
||||||
|
"if (!messageQueue.length || messagePending) return;"
|
||||||
|
"messagePending = true;"
|
||||||
|
"window.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
|
||||||
|
"}"
|
||||||
|
|
||||||
|
"window.postMessage = function(data) {"
|
||||||
|
"messageQueue.push(String(data));"
|
||||||
|
"processQueue();"
|
||||||
|
"};"
|
||||||
|
|
||||||
|
"document.addEventListener('message:received', function(e) {"
|
||||||
|
"messagePending = false;"
|
||||||
|
"processQueue();"
|
||||||
|
"});"
|
||||||
|
"})();", RCTJSNavigationScheme, kPostMessageHost
|
||||||
|
];
|
||||||
|
[webView stringByEvaluatingJavaScriptFromString:source];
|
||||||
|
}
|
||||||
|
if (_injectedJavaScript != nil) {
|
||||||
|
NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript];
|
||||||
|
|
||||||
|
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||||
|
event[@"jsEvaluationValue"] = jsEvaluationValue;
|
||||||
|
|
||||||
|
_onLoadingFinish(event);
|
||||||
|
}
|
||||||
|
// we only need the final 'finishLoad' call so only fire the event when we're actually done loading.
|
||||||
|
else if (_onLoadingFinish && !webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) {
|
||||||
|
_onLoadingFinish([self baseEvent]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <React/RCTViewManager.h>
|
||||||
|
|
||||||
|
@interface RCTWebViewManager : RCTViewManager
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,158 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import "RCTWebViewManager.h"
|
||||||
|
|
||||||
|
#import "RCTBridge.h"
|
||||||
|
#import "RCTUIManager.h"
|
||||||
|
#import "RCTWebView.h"
|
||||||
|
#import "UIView+React.h"
|
||||||
|
|
||||||
|
@interface RCTWebViewManager () <RCTWebViewDelegate>
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation RCTWebViewManager
|
||||||
|
{
|
||||||
|
NSConditionLock *_shouldStartLoadLock;
|
||||||
|
BOOL _shouldStartLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_MODULE()
|
||||||
|
|
||||||
|
- (UIView *)view
|
||||||
|
{
|
||||||
|
RCTWebView *webView = [RCTWebView new];
|
||||||
|
webView.delegate = self;
|
||||||
|
return webView;
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary)
|
||||||
|
RCT_REMAP_VIEW_PROPERTY(bounces, _webView.scrollView.bounces, BOOL)
|
||||||
|
RCT_REMAP_VIEW_PROPERTY(scrollEnabled, _webView.scrollView.scrollEnabled, BOOL)
|
||||||
|
RCT_REMAP_VIEW_PROPERTY(decelerationRate, _webView.scrollView.decelerationRate, CGFloat)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(scalesPageToFit, BOOL)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock)
|
||||||
|
RCT_REMAP_VIEW_PROPERTY(allowsInlineMediaPlayback, _webView.allowsInlineMediaPlayback, BOOL)
|
||||||
|
RCT_REMAP_VIEW_PROPERTY(mediaPlaybackRequiresUserAction, _webView.mediaPlaybackRequiresUserAction, BOOL)
|
||||||
|
RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, _webView.dataDetectorTypes, UIDataDetectorTypes)
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(goBack:(nonnull NSNumber *)reactTag)
|
||||||
|
{
|
||||||
|
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
|
||||||
|
RCTWebView *view = viewRegistry[reactTag];
|
||||||
|
if (![view isKindOfClass:[RCTWebView class]]) {
|
||||||
|
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
|
||||||
|
} else {
|
||||||
|
[view goBack];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(goForward:(nonnull NSNumber *)reactTag)
|
||||||
|
{
|
||||||
|
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||||
|
id view = viewRegistry[reactTag];
|
||||||
|
if (![view isKindOfClass:[RCTWebView class]]) {
|
||||||
|
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
|
||||||
|
} else {
|
||||||
|
[view goForward];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag)
|
||||||
|
{
|
||||||
|
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
|
||||||
|
RCTWebView *view = viewRegistry[reactTag];
|
||||||
|
if (![view isKindOfClass:[RCTWebView class]]) {
|
||||||
|
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
|
||||||
|
} else {
|
||||||
|
[view reload];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(stopLoading:(nonnull NSNumber *)reactTag)
|
||||||
|
{
|
||||||
|
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
|
||||||
|
RCTWebView *view = viewRegistry[reactTag];
|
||||||
|
if (![view isKindOfClass:[RCTWebView class]]) {
|
||||||
|
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
|
||||||
|
} else {
|
||||||
|
[view stopLoading];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message)
|
||||||
|
{
|
||||||
|
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
|
||||||
|
RCTWebView *view = viewRegistry[reactTag];
|
||||||
|
if (![view isKindOfClass:[RCTWebView class]]) {
|
||||||
|
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
|
||||||
|
} else {
|
||||||
|
[view postMessage:message];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(injectJavaScript:(nonnull NSNumber *)reactTag script:(NSString *)script)
|
||||||
|
{
|
||||||
|
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
|
||||||
|
RCTWebView *view = viewRegistry[reactTag];
|
||||||
|
if (![view isKindOfClass:[RCTWebView class]]) {
|
||||||
|
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
|
||||||
|
} else {
|
||||||
|
[view injectJavaScript:script];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Exported synchronous methods
|
||||||
|
|
||||||
|
- (BOOL)webView:(__unused RCTWebView *)webView
|
||||||
|
shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
|
||||||
|
withCallback:(RCTDirectEventBlock)callback
|
||||||
|
{
|
||||||
|
_shouldStartLoadLock = [[NSConditionLock alloc] initWithCondition:arc4random()];
|
||||||
|
_shouldStartLoad = YES;
|
||||||
|
request[@"lockIdentifier"] = @(_shouldStartLoadLock.condition);
|
||||||
|
callback(request);
|
||||||
|
|
||||||
|
// Block the main thread for a maximum of 250ms until the JS thread returns
|
||||||
|
if ([_shouldStartLoadLock lockWhenCondition:0 beforeDate:[NSDate dateWithTimeIntervalSinceNow:.25]]) {
|
||||||
|
BOOL returnValue = _shouldStartLoad;
|
||||||
|
[_shouldStartLoadLock unlock];
|
||||||
|
_shouldStartLoadLock = nil;
|
||||||
|
return returnValue;
|
||||||
|
} else {
|
||||||
|
RCTLogWarn(@"Did not receive response to shouldStartLoad in time, defaulting to YES");
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(startLoadWithResult:(BOOL)result lockIdentifier:(NSInteger)lockIdentifier)
|
||||||
|
{
|
||||||
|
if ([_shouldStartLoadLock tryLockWhenCondition:lockIdentifier]) {
|
||||||
|
_shouldStartLoad = result;
|
||||||
|
[_shouldStartLoadLock unlockWithCondition:0];
|
||||||
|
} else {
|
||||||
|
RCTLogWarn(@"startLoadWithResult invoked with invalid lockIdentifier: "
|
||||||
|
"got %lld, expected %lld", (long long)lockIdentifier, (long long)_shouldStartLoadLock.condition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,479 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2018-present, Infinite Red, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const EdgeInsetsPropType = require('EdgeInsetsPropType');
|
||||||
|
const ActivityIndicator = require('ActivityIndicator');
|
||||||
|
const React = require('React');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const ReactNative = require('ReactNative');
|
||||||
|
const StyleSheet = require('StyleSheet');
|
||||||
|
const UIManager = require('UIManager');
|
||||||
|
const View = require('View');
|
||||||
|
const ViewPropTypes = require('ViewPropTypes');
|
||||||
|
const WebViewShared = require('WebViewShared');
|
||||||
|
|
||||||
|
const deprecatedPropType = require('deprecatedPropType');
|
||||||
|
const keyMirror = require('fbjs/lib/keyMirror');
|
||||||
|
const requireNativeComponent = require('requireNativeComponent');
|
||||||
|
const resolveAssetSource = require('resolveAssetSource');
|
||||||
|
|
||||||
|
const RCT_WEBVIEW_REF = 'webview';
|
||||||
|
|
||||||
|
const WebViewState = keyMirror({
|
||||||
|
IDLE: null,
|
||||||
|
LOADING: null,
|
||||||
|
ERROR: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultRenderLoading = () => (
|
||||||
|
<View style={styles.loadingView}>
|
||||||
|
<ActivityIndicator style={styles.loadingProgressBar} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a native WebView.
|
||||||
|
*/
|
||||||
|
class WebView extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...ViewPropTypes,
|
||||||
|
renderError: PropTypes.func,
|
||||||
|
renderLoading: PropTypes.func,
|
||||||
|
onLoad: PropTypes.func,
|
||||||
|
onLoadEnd: PropTypes.func,
|
||||||
|
onLoadStart: PropTypes.func,
|
||||||
|
onError: PropTypes.func,
|
||||||
|
automaticallyAdjustContentInsets: PropTypes.bool,
|
||||||
|
contentInset: EdgeInsetsPropType,
|
||||||
|
onNavigationStateChange: PropTypes.func,
|
||||||
|
onMessage: PropTypes.func,
|
||||||
|
onContentSizeChange: PropTypes.func,
|
||||||
|
startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load
|
||||||
|
style: ViewPropTypes.style,
|
||||||
|
|
||||||
|
html: deprecatedPropType(
|
||||||
|
PropTypes.string,
|
||||||
|
'Use the `source` prop instead.',
|
||||||
|
),
|
||||||
|
|
||||||
|
url: deprecatedPropType(PropTypes.string, 'Use the `source` prop instead.'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads static html or a uri (with optional headers) in the WebView.
|
||||||
|
*/
|
||||||
|
source: PropTypes.oneOfType([
|
||||||
|
PropTypes.shape({
|
||||||
|
/*
|
||||||
|
* The URI to load in the WebView. Can be a local or remote file.
|
||||||
|
*/
|
||||||
|
uri: PropTypes.string,
|
||||||
|
/*
|
||||||
|
* The HTTP Method to use. Defaults to GET if not specified.
|
||||||
|
* NOTE: On Android, only GET and POST are supported.
|
||||||
|
*/
|
||||||
|
method: PropTypes.oneOf(['GET', 'POST']),
|
||||||
|
/*
|
||||||
|
* Additional HTTP headers to send with the request.
|
||||||
|
* NOTE: On Android, this can only be used with GET requests.
|
||||||
|
*/
|
||||||
|
headers: PropTypes.object,
|
||||||
|
/*
|
||||||
|
* The HTTP body to send with the request. This must be a valid
|
||||||
|
* UTF-8 string, and will be sent exactly as specified, with no
|
||||||
|
* additional encoding (e.g. URL-escaping or base64) applied.
|
||||||
|
* NOTE: On Android, this can only be used with POST requests.
|
||||||
|
*/
|
||||||
|
body: PropTypes.string,
|
||||||
|
}),
|
||||||
|
PropTypes.shape({
|
||||||
|
/*
|
||||||
|
* A static HTML page to display in the WebView.
|
||||||
|
*/
|
||||||
|
html: PropTypes.string,
|
||||||
|
/*
|
||||||
|
* The base URL to be used for any relative links in the HTML.
|
||||||
|
*/
|
||||||
|
baseUrl: PropTypes.string,
|
||||||
|
}),
|
||||||
|
/*
|
||||||
|
* Used internally by packager.
|
||||||
|
*/
|
||||||
|
PropTypes.number,
|
||||||
|
]),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used on Android only, JS is enabled by default for WebView on iOS
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
javaScriptEnabled: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used on Android Lollipop and above only, third party cookies are enabled
|
||||||
|
* by default for WebView on Android Kitkat and below and on iOS
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
thirdPartyCookiesEnabled: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used on Android only, controls whether DOM Storage is enabled or not
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
domStorageEnabled: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether Geolocation is enabled. The default is false.
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
geolocationEnabled: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the JS to be injected when the webpage loads.
|
||||||
|
*/
|
||||||
|
injectedJavaScript: PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the webpage scales to fit the view and the user can change the scale.
|
||||||
|
*/
|
||||||
|
scalesPageToFit: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the user-agent for this WebView. The user-agent can also be set in native using
|
||||||
|
* WebViewConfig. This prop will overwrite that config.
|
||||||
|
*/
|
||||||
|
userAgent: PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to locate this view in end-to-end tests.
|
||||||
|
*/
|
||||||
|
testID: PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether HTML5 audio & videos require the user to tap before they can
|
||||||
|
* start playing. The default value is `false`.
|
||||||
|
*/
|
||||||
|
mediaPlaybackRequiresUserAction: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean that sets whether JavaScript running in the context of a file
|
||||||
|
* scheme URL should be allowed to access content from any origin.
|
||||||
|
* Including accessing content from other file scheme URLs
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
allowUniversalAccessFromFileURLs: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of origin strings to allow being navigated to. The strings allow
|
||||||
|
* wildcards and get matched against *just* the origin (not the full URL).
|
||||||
|
* If the user taps to navigate to a new page but the new page is not in
|
||||||
|
* this whitelist, the URL will be opened by the Android OS.
|
||||||
|
* The default whitelisted origins are "http://*" and "https://*".
|
||||||
|
*/
|
||||||
|
originWhitelist: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that accepts a string that will be passed to the WebView and
|
||||||
|
* executed immediately as JavaScript.
|
||||||
|
*/
|
||||||
|
injectJavaScript: PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the mixed content mode. i.e WebView will allow a secure origin to load content from any other origin.
|
||||||
|
*
|
||||||
|
* Possible values for `mixedContentMode` are:
|
||||||
|
*
|
||||||
|
* - `'never'` (default) - WebView will not allow a secure origin to load content from an insecure origin.
|
||||||
|
* - `'always'` - WebView will allow a secure origin to load content from any other origin, even if that origin is insecure.
|
||||||
|
* - `'compatibility'` - WebView will attempt to be compatible with the approach of a modern web browser with regard to mixed content.
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
mixedContentMode: PropTypes.oneOf(['never', 'always', 'compatibility']),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used on Android only, controls whether form autocomplete data should be saved
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
saveFormDataDisabled: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the native component used to render the WebView. Enables a custom native
|
||||||
|
* WebView which uses the same JavaScript as the original WebView.
|
||||||
|
*/
|
||||||
|
nativeConfig: PropTypes.shape({
|
||||||
|
/*
|
||||||
|
* The native component used to render the WebView.
|
||||||
|
*/
|
||||||
|
component: PropTypes.any,
|
||||||
|
/*
|
||||||
|
* Set props directly on the native component WebView. Enables custom props which the
|
||||||
|
* original WebView doesn't pass through.
|
||||||
|
*/
|
||||||
|
props: PropTypes.object,
|
||||||
|
/*
|
||||||
|
* Set the ViewManager to use for communication with the native side.
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
viewManager: PropTypes.object,
|
||||||
|
}),
|
||||||
|
/*
|
||||||
|
* Used on Android only, controls whether the given list of URL prefixes should
|
||||||
|
* make {@link com.facebook.react.views.webview.ReactWebViewClient} to launch a
|
||||||
|
* default activity intent for those URL instead of loading it within the webview.
|
||||||
|
* Use this to list URLs that WebView cannot handle, e.g. a PDF url.
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
urlPrefixesForDefaultIntent: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
javaScriptEnabled: true,
|
||||||
|
thirdPartyCookiesEnabled: true,
|
||||||
|
scalesPageToFit: true,
|
||||||
|
saveFormDataDisabled: false,
|
||||||
|
originWhitelist: WebViewShared.defaultOriginWhitelist,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
viewState: WebViewState.IDLE,
|
||||||
|
lastErrorEvent: null,
|
||||||
|
startInLoadingState: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
UNSAFE_componentWillMount() {
|
||||||
|
if (this.props.startInLoadingState) {
|
||||||
|
this.setState({ viewState: WebViewState.LOADING });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let otherView = null;
|
||||||
|
|
||||||
|
if (this.state.viewState === WebViewState.LOADING) {
|
||||||
|
otherView = (this.props.renderLoading || defaultRenderLoading)();
|
||||||
|
} else if (this.state.viewState === WebViewState.ERROR) {
|
||||||
|
const errorEvent = this.state.lastErrorEvent;
|
||||||
|
otherView =
|
||||||
|
this.props.renderError &&
|
||||||
|
this.props.renderError(
|
||||||
|
errorEvent.domain,
|
||||||
|
errorEvent.code,
|
||||||
|
errorEvent.description,
|
||||||
|
);
|
||||||
|
} else if (this.state.viewState !== WebViewState.IDLE) {
|
||||||
|
console.error(
|
||||||
|
'RCTWebView invalid state encountered: ' + this.state.loading,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const webViewStyles = [styles.container, this.props.style];
|
||||||
|
if (
|
||||||
|
this.state.viewState === WebViewState.LOADING ||
|
||||||
|
this.state.viewState === WebViewState.ERROR
|
||||||
|
) {
|
||||||
|
// if we're in either LOADING or ERROR states, don't show the webView
|
||||||
|
webViewStyles.push(styles.hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = this.props.source || {};
|
||||||
|
if (this.props.html) {
|
||||||
|
source.html = this.props.html;
|
||||||
|
} else if (this.props.url) {
|
||||||
|
source.uri = this.props.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.method === 'POST' && source.headers) {
|
||||||
|
console.warn(
|
||||||
|
'WebView: `source.headers` is not supported when using POST.',
|
||||||
|
);
|
||||||
|
} else if (source.method === 'GET' && source.body) {
|
||||||
|
console.warn('WebView: `source.body` is not supported when using GET.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nativeConfig = this.props.nativeConfig || {};
|
||||||
|
|
||||||
|
const originWhitelist = (this.props.originWhitelist || []).map(
|
||||||
|
WebViewShared.originWhitelistToRegex,
|
||||||
|
);
|
||||||
|
|
||||||
|
let NativeWebView = nativeConfig.component || RCTWebView;
|
||||||
|
|
||||||
|
const webView = (
|
||||||
|
<NativeWebView
|
||||||
|
ref={RCT_WEBVIEW_REF}
|
||||||
|
key="webViewKey"
|
||||||
|
style={webViewStyles}
|
||||||
|
source={resolveAssetSource(source)}
|
||||||
|
scalesPageToFit={this.props.scalesPageToFit}
|
||||||
|
injectedJavaScript={this.props.injectedJavaScript}
|
||||||
|
userAgent={this.props.userAgent}
|
||||||
|
javaScriptEnabled={this.props.javaScriptEnabled}
|
||||||
|
thirdPartyCookiesEnabled={this.props.thirdPartyCookiesEnabled}
|
||||||
|
domStorageEnabled={this.props.domStorageEnabled}
|
||||||
|
messagingEnabled={typeof this.props.onMessage === 'function'}
|
||||||
|
onMessage={this.onMessage}
|
||||||
|
contentInset={this.props.contentInset}
|
||||||
|
automaticallyAdjustContentInsets={
|
||||||
|
this.props.automaticallyAdjustContentInsets
|
||||||
|
}
|
||||||
|
onContentSizeChange={this.props.onContentSizeChange}
|
||||||
|
onLoadingStart={this.onLoadingStart}
|
||||||
|
onLoadingFinish={this.onLoadingFinish}
|
||||||
|
onLoadingError={this.onLoadingError}
|
||||||
|
testID={this.props.testID}
|
||||||
|
geolocationEnabled={this.props.geolocationEnabled}
|
||||||
|
mediaPlaybackRequiresUserAction={
|
||||||
|
this.props.mediaPlaybackRequiresUserAction
|
||||||
|
}
|
||||||
|
allowUniversalAccessFromFileURLs={
|
||||||
|
this.props.allowUniversalAccessFromFileURLs
|
||||||
|
}
|
||||||
|
originWhitelist={originWhitelist}
|
||||||
|
mixedContentMode={this.props.mixedContentMode}
|
||||||
|
saveFormDataDisabled={this.props.saveFormDataDisabled}
|
||||||
|
urlPrefixesForDefaultIntent={this.props.urlPrefixesForDefaultIntent}
|
||||||
|
{...nativeConfig.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{webView}
|
||||||
|
{otherView}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
goForward = () => {
|
||||||
|
UIManager.dispatchViewManagerCommand(
|
||||||
|
this.getWebViewHandle(),
|
||||||
|
UIManager.RCTWebView.Commands.goForward,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
goBack = () => {
|
||||||
|
UIManager.dispatchViewManagerCommand(
|
||||||
|
this.getWebViewHandle(),
|
||||||
|
UIManager.RCTWebView.Commands.goBack,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
reload = () => {
|
||||||
|
this.setState({
|
||||||
|
viewState: WebViewState.LOADING,
|
||||||
|
});
|
||||||
|
UIManager.dispatchViewManagerCommand(
|
||||||
|
this.getWebViewHandle(),
|
||||||
|
UIManager.RCTWebView.Commands.reload,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
stopLoading = () => {
|
||||||
|
UIManager.dispatchViewManagerCommand(
|
||||||
|
this.getWebViewHandle(),
|
||||||
|
UIManager.RCTWebView.Commands.stopLoading,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
postMessage = data => {
|
||||||
|
UIManager.dispatchViewManagerCommand(
|
||||||
|
this.getWebViewHandle(),
|
||||||
|
UIManager.RCTWebView.Commands.postMessage,
|
||||||
|
[String(data)],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects a javascript string into the referenced WebView. Deliberately does not
|
||||||
|
* return a response because using eval() to return a response breaks this method
|
||||||
|
* on pages with a Content Security Policy that disallows eval(). If you need that
|
||||||
|
* functionality, look into postMessage/onMessage.
|
||||||
|
*/
|
||||||
|
injectJavaScript = data => {
|
||||||
|
UIManager.dispatchViewManagerCommand(
|
||||||
|
this.getWebViewHandle(),
|
||||||
|
UIManager.RCTWebView.Commands.injectJavaScript,
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We return an event with a bunch of fields including:
|
||||||
|
* url, title, loading, canGoBack, canGoForward
|
||||||
|
*/
|
||||||
|
updateNavigationState = event => {
|
||||||
|
if (this.props.onNavigationStateChange) {
|
||||||
|
this.props.onNavigationStateChange(event.nativeEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getWebViewHandle = () => {
|
||||||
|
return ReactNative.findNodeHandle(this.refs[RCT_WEBVIEW_REF]);
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoadingStart = event => {
|
||||||
|
const onLoadStart = this.props.onLoadStart;
|
||||||
|
onLoadStart && onLoadStart(event);
|
||||||
|
this.updateNavigationState(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoadingError = event => {
|
||||||
|
event.persist(); // persist this event because we need to store it
|
||||||
|
const { onError, onLoadEnd } = this.props;
|
||||||
|
onError && onError(event);
|
||||||
|
onLoadEnd && onLoadEnd(event);
|
||||||
|
console.warn('Encountered an error loading page', event.nativeEvent);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
lastErrorEvent: event.nativeEvent,
|
||||||
|
viewState: WebViewState.ERROR,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoadingFinish = event => {
|
||||||
|
const { onLoad, onLoadEnd } = this.props;
|
||||||
|
onLoad && onLoad(event);
|
||||||
|
onLoadEnd && onLoadEnd(event);
|
||||||
|
this.setState({
|
||||||
|
viewState: WebViewState.IDLE,
|
||||||
|
});
|
||||||
|
this.updateNavigationState(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMessage = (event) => {
|
||||||
|
const { onMessage } = this.props;
|
||||||
|
onMessage && onMessage(event);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const RCTWebView = requireNativeComponent('RCTWebView');
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
hidden: {
|
||||||
|
height: 0,
|
||||||
|
flex: 0, // disable 'flex:1' when hiding a View
|
||||||
|
},
|
||||||
|
loadingView: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
loadingProgressBar: {
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = WebView;
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
const ReactNative = require('react-native');
|
||||||
|
const { WebView } = ReactNative;
|
||||||
|
|
||||||
|
const { TestModule } = ReactNative.NativeModules;
|
||||||
|
|
||||||
|
class WebViewTest extends React.Component {
|
||||||
|
render() {
|
||||||
|
let firstMessageReceived = false;
|
||||||
|
let secondMessageReceived = false;
|
||||||
|
function processMessage(e) {
|
||||||
|
const message = e.nativeEvent.data;
|
||||||
|
if (message === 'First') {
|
||||||
|
firstMessageReceived = true;
|
||||||
|
}
|
||||||
|
if (message === 'Second') {
|
||||||
|
secondMessageReceived = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// got both messages
|
||||||
|
if (firstMessageReceived && secondMessageReceived) {
|
||||||
|
TestModule.markTestPassed(true);
|
||||||
|
}
|
||||||
|
// wait for next message
|
||||||
|
else if (firstMessageReceived && !secondMessageReceived) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// first message got lost
|
||||||
|
else if (!firstMessageReceived && secondMessageReceived) {
|
||||||
|
throw new Error('First message got lost');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const html =
|
||||||
|
'Hello world' +
|
||||||
|
'<script>' +
|
||||||
|
"window.setTimeout(function(){window.postMessage('First'); window.postMessage('Second')}, 0)" +
|
||||||
|
'</script>';
|
||||||
|
|
||||||
|
// fail if messages didn't get through;
|
||||||
|
window.setTimeout(function () {
|
||||||
|
throw new Error(
|
||||||
|
firstMessageReceived
|
||||||
|
? 'Both messages got lost'
|
||||||
|
: 'Second message got lost',
|
||||||
|
);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
const source = {
|
||||||
|
html: html,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WebView
|
||||||
|
source={source}
|
||||||
|
onMessage={processMessage}
|
||||||
|
originWhitelist={['about:blank']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WebViewTest.displayName = 'WebViewTest';
|
||||||
|
|
||||||
|
module.exports = WebViewTest;
|
|
@ -0,0 +1,683 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2018-present, Infinite Red, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
* @noflow
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const ActivityIndicator = require('ActivityIndicator');
|
||||||
|
const EdgeInsetsPropType = require('EdgeInsetsPropType');
|
||||||
|
const Linking = require('Linking');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const React = require('React');
|
||||||
|
const ReactNative = require('ReactNative');
|
||||||
|
const ScrollView = require('ScrollView');
|
||||||
|
const StyleSheet = require('StyleSheet');
|
||||||
|
const Text = require('Text');
|
||||||
|
const UIManager = require('UIManager');
|
||||||
|
const View = require('View');
|
||||||
|
const ViewPropTypes = require('ViewPropTypes');
|
||||||
|
const WebViewShared = require('WebViewShared');
|
||||||
|
|
||||||
|
const deprecatedPropType = require('deprecatedPropType');
|
||||||
|
const invariant = require('fbjs/lib/invariant');
|
||||||
|
const keyMirror = require('fbjs/lib/keyMirror');
|
||||||
|
const processDecelerationRate = require('processDecelerationRate');
|
||||||
|
const requireNativeComponent = require('requireNativeComponent');
|
||||||
|
const resolveAssetSource = require('resolveAssetSource');
|
||||||
|
|
||||||
|
const RCTWebViewManager = require('NativeModules').WebViewManager;
|
||||||
|
|
||||||
|
const BGWASH = 'rgba(255,255,255,0.8)';
|
||||||
|
const RCT_WEBVIEW_REF = 'webview';
|
||||||
|
|
||||||
|
const WebViewState = keyMirror({
|
||||||
|
IDLE: null,
|
||||||
|
LOADING: null,
|
||||||
|
ERROR: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const NavigationType = keyMirror({
|
||||||
|
click: true,
|
||||||
|
formsubmit: true,
|
||||||
|
backforward: true,
|
||||||
|
reload: true,
|
||||||
|
formresubmit: true,
|
||||||
|
other: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const JSNavigationScheme = 'react-js-navigation';
|
||||||
|
|
||||||
|
// type ErrorEvent = {
|
||||||
|
// domain: any,
|
||||||
|
// code: any,
|
||||||
|
// description: any,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// type Event = Object;
|
||||||
|
|
||||||
|
const DataDetectorTypes = [
|
||||||
|
'phoneNumber',
|
||||||
|
'link',
|
||||||
|
'address',
|
||||||
|
'calendarEvent',
|
||||||
|
'none',
|
||||||
|
'all',
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultRenderLoading = () => (
|
||||||
|
<View style={styles.loadingView}>
|
||||||
|
<ActivityIndicator />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
const defaultRenderError = (errorDomain, errorCode, errorDesc) => (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<Text style={styles.errorTextTitle}>Error loading page</Text>
|
||||||
|
<Text style={styles.errorText}>{'Domain: ' + errorDomain}</Text>
|
||||||
|
<Text style={styles.errorText}>{'Error Code: ' + errorCode}</Text>
|
||||||
|
<Text style={styles.errorText}>{'Description: ' + errorDesc}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `WebView` renders web content in a native view.
|
||||||
|
*
|
||||||
|
*```
|
||||||
|
* import React, { Component } from 'react';
|
||||||
|
* import { WebView } from 'react-native';
|
||||||
|
*
|
||||||
|
* class MyWeb extends Component {
|
||||||
|
* render() {
|
||||||
|
* return (
|
||||||
|
* <WebView
|
||||||
|
* source={{uri: 'https://github.com/facebook/react-native'}}
|
||||||
|
* style={{marginTop: 20}}
|
||||||
|
* />
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*```
|
||||||
|
*
|
||||||
|
* You can use this component to navigate back and forth in the web view's
|
||||||
|
* history and configure various properties for the web content.
|
||||||
|
*/
|
||||||
|
class WebView extends React.Component {
|
||||||
|
static JSNavigationScheme = JSNavigationScheme;
|
||||||
|
static NavigationType = NavigationType;
|
||||||
|
static propTypes = {
|
||||||
|
...ViewPropTypes,
|
||||||
|
|
||||||
|
html: deprecatedPropType(
|
||||||
|
PropTypes.string,
|
||||||
|
'Use the `source` prop instead.',
|
||||||
|
),
|
||||||
|
|
||||||
|
url: deprecatedPropType(PropTypes.string, 'Use the `source` prop instead.'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads static html or a uri (with optional headers) in the WebView.
|
||||||
|
*/
|
||||||
|
source: PropTypes.oneOfType([
|
||||||
|
PropTypes.shape({
|
||||||
|
/*
|
||||||
|
* The URI to load in the `WebView`. Can be a local or remote file.
|
||||||
|
*/
|
||||||
|
uri: PropTypes.string,
|
||||||
|
/*
|
||||||
|
* The HTTP Method to use. Defaults to GET if not specified.
|
||||||
|
* NOTE: On Android, only GET and POST are supported.
|
||||||
|
*/
|
||||||
|
method: PropTypes.string,
|
||||||
|
/*
|
||||||
|
* Additional HTTP headers to send with the request.
|
||||||
|
* NOTE: On Android, this can only be used with GET requests.
|
||||||
|
*/
|
||||||
|
headers: PropTypes.object,
|
||||||
|
/*
|
||||||
|
* The HTTP body to send with the request. This must be a valid
|
||||||
|
* UTF-8 string, and will be sent exactly as specified, with no
|
||||||
|
* additional encoding (e.g. URL-escaping or base64) applied.
|
||||||
|
* NOTE: On Android, this can only be used with POST requests.
|
||||||
|
*/
|
||||||
|
body: PropTypes.string,
|
||||||
|
}),
|
||||||
|
PropTypes.shape({
|
||||||
|
/*
|
||||||
|
* A static HTML page to display in the WebView.
|
||||||
|
*/
|
||||||
|
html: PropTypes.string,
|
||||||
|
/*
|
||||||
|
* The base URL to be used for any relative links in the HTML.
|
||||||
|
*/
|
||||||
|
baseUrl: PropTypes.string,
|
||||||
|
}),
|
||||||
|
/*
|
||||||
|
* Used internally by packager.
|
||||||
|
*/
|
||||||
|
PropTypes.number,
|
||||||
|
]),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that returns a view to show if there's an error.
|
||||||
|
*/
|
||||||
|
renderError: PropTypes.func, // view to show if there's an error
|
||||||
|
/**
|
||||||
|
* Function that returns a loading indicator.
|
||||||
|
*/
|
||||||
|
renderLoading: PropTypes.func,
|
||||||
|
/**
|
||||||
|
* Function that is invoked when the `WebView` has finished loading.
|
||||||
|
*/
|
||||||
|
onLoad: PropTypes.func,
|
||||||
|
/**
|
||||||
|
* Function that is invoked when the `WebView` load succeeds or fails.
|
||||||
|
*/
|
||||||
|
onLoadEnd: PropTypes.func,
|
||||||
|
/**
|
||||||
|
* Function that is invoked when the `WebView` starts loading.
|
||||||
|
*/
|
||||||
|
onLoadStart: PropTypes.func,
|
||||||
|
/**
|
||||||
|
* Function that is invoked when the `WebView` load fails.
|
||||||
|
*/
|
||||||
|
onError: PropTypes.func,
|
||||||
|
/**
|
||||||
|
* Boolean value that determines whether the web view bounces
|
||||||
|
* when it reaches the edge of the content. The default value is `true`.
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
bounces: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* A floating-point number that determines how quickly the scroll view
|
||||||
|
* decelerates after the user lifts their finger. You may also use the
|
||||||
|
* string shortcuts `"normal"` and `"fast"` which match the underlying iOS
|
||||||
|
* settings for `UIScrollViewDecelerationRateNormal` and
|
||||||
|
* `UIScrollViewDecelerationRateFast` respectively:
|
||||||
|
*
|
||||||
|
* - normal: 0.998
|
||||||
|
* - fast: 0.99 (the default for iOS web view)
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
decelerationRate: PropTypes.oneOfType([
|
||||||
|
PropTypes.oneOf(['fast', 'normal']),
|
||||||
|
PropTypes.number,
|
||||||
|
]),
|
||||||
|
/**
|
||||||
|
* Boolean value that determines whether scrolling is enabled in the
|
||||||
|
* `WebView`. The default value is `true`.
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
scrollEnabled: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Controls whether to adjust the content inset for web views that are
|
||||||
|
* placed behind a navigation bar, tab bar, or toolbar. The default value
|
||||||
|
* is `true`.
|
||||||
|
*/
|
||||||
|
automaticallyAdjustContentInsets: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* The amount by which the web view content is inset from the edges of
|
||||||
|
* the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
contentInset: EdgeInsetsPropType,
|
||||||
|
/**
|
||||||
|
* Function that is invoked when the `WebView` loading starts or ends.
|
||||||
|
*/
|
||||||
|
onNavigationStateChange: PropTypes.func,
|
||||||
|
/**
|
||||||
|
* A function that is invoked when the webview calls `window.postMessage`.
|
||||||
|
* Setting this property will inject a `postMessage` global into your
|
||||||
|
* webview, but will still call pre-existing values of `postMessage`.
|
||||||
|
*
|
||||||
|
* `window.postMessage` accepts one argument, `data`, which will be
|
||||||
|
* available on the event object, `event.nativeEvent.data`. `data`
|
||||||
|
* must be a string.
|
||||||
|
*/
|
||||||
|
onMessage: PropTypes.func,
|
||||||
|
/**
|
||||||
|
* Boolean value that forces the `WebView` to show the loading view
|
||||||
|
* on the first load.
|
||||||
|
*/
|
||||||
|
startInLoadingState: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* The style to apply to the `WebView`.
|
||||||
|
*/
|
||||||
|
style: ViewPropTypes.style,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the types of data converted to clickable URLs in the web view's content.
|
||||||
|
* By default only phone numbers are detected.
|
||||||
|
*
|
||||||
|
* You can provide one type or an array of many types.
|
||||||
|
*
|
||||||
|
* Possible values for `dataDetectorTypes` are:
|
||||||
|
*
|
||||||
|
* - `'phoneNumber'`
|
||||||
|
* - `'link'`
|
||||||
|
* - `'address'`
|
||||||
|
* - `'calendarEvent'`
|
||||||
|
* - `'none'`
|
||||||
|
* - `'all'`
|
||||||
|
*
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
dataDetectorTypes: PropTypes.oneOfType([
|
||||||
|
PropTypes.oneOf(DataDetectorTypes),
|
||||||
|
PropTypes.arrayOf(PropTypes.oneOf(DataDetectorTypes)),
|
||||||
|
]),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean value to enable JavaScript in the `WebView`. Used on Android only
|
||||||
|
* as JavaScript is enabled by default on iOS. The default value is `true`.
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
javaScriptEnabled: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean value to enable third party cookies in the `WebView`. Used on
|
||||||
|
* Android Lollipop and above only as third party cookies are enabled by
|
||||||
|
* default on Android Kitkat and below and on iOS. The default value is `true`.
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
thirdPartyCookiesEnabled: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean value to control whether DOM Storage is enabled. Used only in
|
||||||
|
* Android.
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
domStorageEnabled: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set this to provide JavaScript that will be injected into the web page
|
||||||
|
* when the view loads.
|
||||||
|
*/
|
||||||
|
injectedJavaScript: PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the user-agent for the `WebView`.
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
userAgent: PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean that controls whether the web content is scaled to fit
|
||||||
|
* the view and enables the user to change the scale. The default value
|
||||||
|
* is `true`.
|
||||||
|
*/
|
||||||
|
scalesPageToFit: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that allows custom handling of any web view requests. Return
|
||||||
|
* `true` from the function to continue loading the request and `false`
|
||||||
|
* to stop loading.
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
onShouldStartLoadWithRequest: PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean that determines whether HTML5 videos play inline or use the
|
||||||
|
* native full-screen controller. The default value is `false`.
|
||||||
|
*
|
||||||
|
* **NOTE** : In order for video to play inline, not only does this
|
||||||
|
* property need to be set to `true`, but the video element in the HTML
|
||||||
|
* document must also include the `webkit-playsinline` attribute.
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
allowsInlineMediaPlayback: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean that determines whether HTML5 audio and video requires the user
|
||||||
|
* to tap them before they start playing. The default value is `true`.
|
||||||
|
*/
|
||||||
|
mediaPlaybackRequiresUserAction: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of origin strings to allow being navigated to. The strings allow
|
||||||
|
* wildcards and get matched against *just* the origin (not the full URL).
|
||||||
|
* If the user taps to navigate to a new page but the new page is not in
|
||||||
|
* this whitelist, we will open the URL in Safari.
|
||||||
|
* The default whitelisted origins are "http://*" and "https://*".
|
||||||
|
*/
|
||||||
|
originWhitelist: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that accepts a string that will be passed to the WebView and
|
||||||
|
* executed immediately as JavaScript.
|
||||||
|
*/
|
||||||
|
injectJavaScript: PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the mixed content mode. i.e WebView will allow a secure origin to load content from any other origin.
|
||||||
|
*
|
||||||
|
* Possible values for `mixedContentMode` are:
|
||||||
|
*
|
||||||
|
* - `'never'` (default) - WebView will not allow a secure origin to load content from an insecure origin.
|
||||||
|
* - `'always'` - WebView will allow a secure origin to load content from any other origin, even if that origin is insecure.
|
||||||
|
* - `'compatibility'` - WebView will attempt to be compatible with the approach of a modern web browser with regard to mixed content.
|
||||||
|
* @platform android
|
||||||
|
*/
|
||||||
|
mixedContentMode: PropTypes.oneOf(['never', 'always', 'compatibility']),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the native component used to render the WebView. Enables a custom native
|
||||||
|
* WebView which uses the same JavaScript as the original WebView.
|
||||||
|
*/
|
||||||
|
nativeConfig: PropTypes.shape({
|
||||||
|
/*
|
||||||
|
* The native component used to render the WebView.
|
||||||
|
*/
|
||||||
|
component: PropTypes.any,
|
||||||
|
/*
|
||||||
|
* Set props directly on the native component WebView. Enables custom props which the
|
||||||
|
* original WebView doesn't pass through.
|
||||||
|
*/
|
||||||
|
props: PropTypes.object,
|
||||||
|
/*
|
||||||
|
* Set the ViewManager to use for communication with the native side.
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
viewManager: PropTypes.object,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
originWhitelist: WebViewShared.defaultOriginWhitelist,
|
||||||
|
scalesPageToFit: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
viewState: WebViewState.IDLE,
|
||||||
|
lastErrorEvent: null,
|
||||||
|
startInLoadingState: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
UNSAFE_componentWillMount() {
|
||||||
|
if (this.props.startInLoadingState) {
|
||||||
|
this.setState({ viewState: WebViewState.LOADING });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let otherView = null;
|
||||||
|
|
||||||
|
if (this.state.viewState === WebViewState.LOADING) {
|
||||||
|
otherView = (this.props.renderLoading || defaultRenderLoading)();
|
||||||
|
} else if (this.state.viewState === WebViewState.ERROR) {
|
||||||
|
const errorEvent = this.state.lastErrorEvent;
|
||||||
|
invariant(errorEvent != null, 'lastErrorEvent expected to be non-null');
|
||||||
|
otherView = (this.props.renderError || defaultRenderError)(
|
||||||
|
errorEvent.domain,
|
||||||
|
errorEvent.code,
|
||||||
|
errorEvent.description,
|
||||||
|
);
|
||||||
|
} else if (this.state.viewState !== WebViewState.IDLE) {
|
||||||
|
console.error(
|
||||||
|
'RCTWebView invalid state encountered: ' + this.state.loading,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const webViewStyles = [styles.container, styles.webView, this.props.style];
|
||||||
|
if (
|
||||||
|
this.state.viewState === WebViewState.LOADING ||
|
||||||
|
this.state.viewState === WebViewState.ERROR
|
||||||
|
) {
|
||||||
|
// if we're in either LOADING or ERROR states, don't show the webView
|
||||||
|
webViewStyles.push(styles.hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nativeConfig = this.props.nativeConfig || {};
|
||||||
|
|
||||||
|
const viewManager = nativeConfig.viewManager || RCTWebViewManager;
|
||||||
|
|
||||||
|
const compiledWhitelist = (this.props.originWhitelist || []).map(
|
||||||
|
WebViewShared.originWhitelistToRegex,
|
||||||
|
);
|
||||||
|
const onShouldStartLoadWithRequest = (event) => {
|
||||||
|
let shouldStart = true;
|
||||||
|
const { url } = event.nativeEvent;
|
||||||
|
const origin = WebViewShared.extractOrigin(url);
|
||||||
|
const passesWhitelist = compiledWhitelist.some(x =>
|
||||||
|
new RegExp(x).test(origin),
|
||||||
|
);
|
||||||
|
shouldStart = shouldStart && passesWhitelist;
|
||||||
|
if (!passesWhitelist) {
|
||||||
|
Linking.openURL(url);
|
||||||
|
}
|
||||||
|
if (this.props.onShouldStartLoadWithRequest) {
|
||||||
|
shouldStart =
|
||||||
|
shouldStart &&
|
||||||
|
this.props.onShouldStartLoadWithRequest(event.nativeEvent);
|
||||||
|
}
|
||||||
|
viewManager.startLoadWithResult(
|
||||||
|
!!shouldStart,
|
||||||
|
event.nativeEvent.lockIdentifier,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const decelerationRate = processDecelerationRate(
|
||||||
|
this.props.decelerationRate,
|
||||||
|
);
|
||||||
|
|
||||||
|
const source = this.props.source || {};
|
||||||
|
if (this.props.html) {
|
||||||
|
source.html = this.props.html;
|
||||||
|
} else if (this.props.url) {
|
||||||
|
source.uri = this.props.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagingEnabled = typeof this.props.onMessage === 'function';
|
||||||
|
|
||||||
|
const NativeWebView = nativeConfig.component || RCTWebView;
|
||||||
|
|
||||||
|
const webView = (
|
||||||
|
<NativeWebView
|
||||||
|
ref={RCT_WEBVIEW_REF}
|
||||||
|
key="webViewKey"
|
||||||
|
style={webViewStyles}
|
||||||
|
source={resolveAssetSource(source)}
|
||||||
|
injectedJavaScript={this.props.injectedJavaScript}
|
||||||
|
bounces={this.props.bounces}
|
||||||
|
scrollEnabled={this.props.scrollEnabled}
|
||||||
|
decelerationRate={decelerationRate}
|
||||||
|
contentInset={this.props.contentInset}
|
||||||
|
automaticallyAdjustContentInsets={
|
||||||
|
this.props.automaticallyAdjustContentInsets
|
||||||
|
}
|
||||||
|
onLoadingStart={this._onLoadingStart}
|
||||||
|
onLoadingFinish={this._onLoadingFinish}
|
||||||
|
onLoadingError={this._onLoadingError}
|
||||||
|
messagingEnabled={messagingEnabled}
|
||||||
|
onMessage={this._onMessage}
|
||||||
|
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
|
||||||
|
scalesPageToFit={this.props.scalesPageToFit}
|
||||||
|
allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}
|
||||||
|
mediaPlaybackRequiresUserAction={
|
||||||
|
this.props.mediaPlaybackRequiresUserAction
|
||||||
|
}
|
||||||
|
dataDetectorTypes={this.props.dataDetectorTypes}
|
||||||
|
{...nativeConfig.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{webView}
|
||||||
|
{otherView}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go forward one page in the web view's history.
|
||||||
|
*/
|
||||||
|
goForward = () => {
|
||||||
|
UIManager.dispatchViewManagerCommand(
|
||||||
|
this.getWebViewHandle(),
|
||||||
|
UIManager.RCTWebView.Commands.goForward,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go back one page in the web view's history.
|
||||||
|
*/
|
||||||
|
goBack = () => {
|
||||||
|
UIManager.dispatchViewManagerCommand(
|
||||||
|
this.getWebViewHandle(),
|
||||||
|
UIManager.RCTWebView.Commands.goBack,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the current page.
|
||||||
|
*/
|
||||||
|
reload = () => {
|
||||||
|
this.setState({ viewState: WebViewState.LOADING });
|
||||||
|
UIManager.dispatchViewManagerCommand(
|
||||||
|
this.getWebViewHandle(),
|
||||||
|
UIManager.RCTWebView.Commands.reload,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop loading the current page.
|
||||||
|
*/
|
||||||
|
stopLoading = () => {
|
||||||
|
UIManager.dispatchViewManagerCommand(
|
||||||
|
this.getWebViewHandle(),
|
||||||
|
UIManager.RCTWebView.Commands.stopLoading,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts a message to the web view, which will emit a `message` event.
|
||||||
|
* Accepts one argument, `data`, which must be a string.
|
||||||
|
*
|
||||||
|
* In your webview, you'll need to something like the following.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* document.addEventListener('message', e => { document.title = e.data; });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
postMessage = data => {
|
||||||
|
UIManager.dispatchViewManagerCommand(
|
||||||
|
this.getWebViewHandle(),
|
||||||
|
UIManager.RCTWebView.Commands.postMessage,
|
||||||
|
[String(data)],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects a javascript string into the referenced WebView. Deliberately does not
|
||||||
|
* return a response because using eval() to return a response breaks this method
|
||||||
|
* on pages with a Content Security Policy that disallows eval(). If you need that
|
||||||
|
* functionality, look into postMessage/onMessage.
|
||||||
|
*/
|
||||||
|
injectJavaScript = data => {
|
||||||
|
UIManager.dispatchViewManagerCommand(
|
||||||
|
this.getWebViewHandle(),
|
||||||
|
UIManager.RCTWebView.Commands.injectJavaScript,
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We return an event with a bunch of fields including:
|
||||||
|
* url, title, loading, canGoBack, canGoForward
|
||||||
|
*/
|
||||||
|
_updateNavigationState = (event) => {
|
||||||
|
if (this.props.onNavigationStateChange) {
|
||||||
|
this.props.onNavigationStateChange(event.nativeEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the native `WebView` node.
|
||||||
|
*/
|
||||||
|
getWebViewHandle = () => {
|
||||||
|
return ReactNative.findNodeHandle(this.refs[RCT_WEBVIEW_REF]);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onLoadingStart = (event) => {
|
||||||
|
const onLoadStart = this.props.onLoadStart;
|
||||||
|
onLoadStart && onLoadStart(event);
|
||||||
|
this._updateNavigationState(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onLoadingError = (event) => {
|
||||||
|
event.persist(); // persist this event because we need to store it
|
||||||
|
const { onError, onLoadEnd } = this.props;
|
||||||
|
onError && onError(event);
|
||||||
|
onLoadEnd && onLoadEnd(event);
|
||||||
|
console.warn('Encountered an error loading page', event.nativeEvent);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
lastErrorEvent: event.nativeEvent,
|
||||||
|
viewState: WebViewState.ERROR,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
_onLoadingFinish = (event) => {
|
||||||
|
const { onLoad, onLoadEnd } = this.props;
|
||||||
|
onLoad && onLoad(event);
|
||||||
|
onLoadEnd && onLoadEnd(event);
|
||||||
|
this.setState({
|
||||||
|
viewState: WebViewState.IDLE,
|
||||||
|
});
|
||||||
|
this._updateNavigationState(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onMessage = (event) => {
|
||||||
|
const { onMessage } = this.props;
|
||||||
|
onMessage && onMessage(event);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const RCTWebView = requireNativeComponent('RCTWebView');
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
errorContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: BGWASH,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
errorTextTitle: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '500',
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
hidden: {
|
||||||
|
height: 0,
|
||||||
|
flex: 0, // disable 'flex:1' when hiding a View
|
||||||
|
},
|
||||||
|
loadingView: {
|
||||||
|
backgroundColor: BGWASH,
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: 100,
|
||||||
|
},
|
||||||
|
webView: {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = WebView;
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2018-present, Infinite Red, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const escapeStringRegexp = require('escape-string-regexp');
|
||||||
|
|
||||||
|
const WebViewShared = {
|
||||||
|
defaultOriginWhitelist: ['http://*', 'https://*'],
|
||||||
|
extractOrigin: (url) => {
|
||||||
|
const result = /^[A-Za-z0-9]+:(\/\/)?[^/]*/.exec(url);
|
||||||
|
return result === null ? null : result[0];
|
||||||
|
},
|
||||||
|
originWhitelistToRegex: (originWhitelist) => {
|
||||||
|
return escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = WebViewShared;
|
|
@ -0,0 +1,60 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2018-present, Infinite Red, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const WebViewShared = require('WebViewShared');
|
||||||
|
describe('WebViewShared', () => {
|
||||||
|
it('extracts the origin correctly', () => {
|
||||||
|
expect(WebViewShared.extractOrigin('http://facebook.com')).toBe(
|
||||||
|
'http://facebook.com',
|
||||||
|
);
|
||||||
|
expect(WebViewShared.extractOrigin('https://facebook.com')).toBe(
|
||||||
|
'https://facebook.com',
|
||||||
|
);
|
||||||
|
expect(WebViewShared.extractOrigin('http://facebook.com:8081')).toBe(
|
||||||
|
'http://facebook.com:8081',
|
||||||
|
);
|
||||||
|
expect(WebViewShared.extractOrigin('ftp://facebook.com')).toBe(
|
||||||
|
'ftp://facebook.com',
|
||||||
|
);
|
||||||
|
expect(WebViewShared.extractOrigin('myweirdscheme://')).toBe(
|
||||||
|
'myweirdscheme://',
|
||||||
|
);
|
||||||
|
expect(WebViewShared.extractOrigin('http://facebook.com/')).toBe(
|
||||||
|
'http://facebook.com',
|
||||||
|
);
|
||||||
|
expect(WebViewShared.extractOrigin('http://facebook.com/longerurl')).toBe(
|
||||||
|
'http://facebook.com',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
WebViewShared.extractOrigin('http://facebook.com/http://facebook.com'),
|
||||||
|
).toBe('http://facebook.com');
|
||||||
|
expect(
|
||||||
|
WebViewShared.extractOrigin('http://facebook.com//http://facebook.com'),
|
||||||
|
).toBe('http://facebook.com');
|
||||||
|
expect(
|
||||||
|
WebViewShared.extractOrigin('http://facebook.com//http://facebook.com//'),
|
||||||
|
).toBe('http://facebook.com');
|
||||||
|
expect(WebViewShared.extractOrigin('about:blank')).toBe('about:blank');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects bad urls', () => {
|
||||||
|
expect(WebViewShared.extractOrigin('a/b')).toBeNull();
|
||||||
|
expect(WebViewShared.extractOrigin('a//b')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a whitelist regex correctly', () => {
|
||||||
|
expect(WebViewShared.originWhitelistToRegex('http://*')).toBe('http://.*');
|
||||||
|
expect(WebViewShared.originWhitelistToRegex('*')).toBe('.*');
|
||||||
|
expect(WebViewShared.originWhitelistToRegex('*//test')).toBe('.*//test');
|
||||||
|
expect(WebViewShared.originWhitelistToRegex('*/*')).toBe('.*/.*');
|
||||||
|
expect(WebViewShared.originWhitelistToRegex('*.com')).toBe('.*\\.com');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,7 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
escape-string-regexp@^1.0.5:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|