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"
|