├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── Web3Webview.android.js ├── Web3Webview.ios.js ├── android ├── .project ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── web3webview │ │ ├── InputStreamWithInjectedJS.java │ │ ├── ProgressEvent.java │ │ ├── Web3WebviewFileProvider.java │ │ ├── Web3WebviewManager.java │ │ ├── Web3WebviewModule.java │ │ └── Web3WebviewPackage.java │ └── res │ └── xml │ └── file_provider_paths.xml ├── index.js ├── ios ├── RNWeb3Webview.h ├── RNWeb3Webview.m ├── RNWeb3Webview.podspec ├── RNWeb3Webview.xcodeproj │ └── project.pbxproj ├── RNWeb3Webview.xcworkspace │ └── contents.xcworkspacedata ├── RNWeb3WebviewManager.h ├── RNWeb3WebviewManager.m ├── WKProcessPool+SharedProcessPool.h ├── WKProcessPool+SharedProcessPool.m ├── WeakScriptMessageDelegate.h └── WeakScriptMessageDelegate.m └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # OSX 3 | # 4 | .DS_Store 5 | 6 | # node.js 7 | # 8 | node_modules/ 9 | npm-debug.log 10 | yarn-error.log 11 | 12 | 13 | # Xcode 14 | # 15 | build/ 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata 25 | *.xccheckout 26 | *.moved-aside 27 | DerivedData 28 | *.hmap 29 | *.ipa 30 | *.xcuserstate 31 | project.xcworkspace 32 | 33 | 34 | # Android/IntelliJ 35 | # 36 | build/ 37 | .idea 38 | .gradle 39 | local.properties 40 | *.iml 41 | 42 | # BUCK 43 | buck-out/ 44 | \.buckd/ 45 | *.keystore 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bruno Barbieri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # react-native-web3-webview 3 | 4 | [![npm](https://img.shields.io/npm/v/react-native-web3-webview.svg)](https://npmjs.com/package/react-native-web3-webview) [![npm](https://img.shields.io/npm/dm/react-native-web3-webview.svg)](https://npmjs.com/package/react-native-web3-webview) 5 | 6 | 7 | A cross platform react native webview with some improvements that allow a javascript injection, which is specifically used by web3 mobile dapp browsers. 8 | 9 | The iOS version is based on [react-native-wkwebview-reborn](https://github.com/CRAlpha/react-native-wkwebview) and the Android version is based on [react-native-webview-bridge](https://github.com/alinz/react-native-webview-bridge) and [react-native-webview](https://github.com/react-native-community/react-native-webview/) 10 | 11 | ## Getting started 12 | 13 | `$ npm install react-native-web3-webview --save` 14 | 15 | ### Mostly automatic installation 16 | 17 | `$ react-native link react-native-web3-webview` 18 | 19 | ### Manual installation 20 | 21 | 22 | #### iOS 23 | 24 | 1. In XCode, in the project navigator, right click `Libraries` ➜ `Add Files to [your project's name]` 25 | 2. Go to `node_modules` ➜ `react-native-web3-webview` and add `RNWeb3Webview.xcodeproj` 26 | 3. In XCode, in the project navigator, select your project. Add `libRNWeb3Webview.a` to your project's `Build Phases` ➜ `Link Binary With Libraries` 27 | 4. Run your project (`Cmd+R`)< 28 | 29 | #### Android 30 | 31 | 1. Open up `android/app/src/main/java/[...]/MainActivity.java` 32 | - Add `import com.reactlibrary.RNWeb3WebviewPackage;` to the imports at the top of the file 33 | - Add `new RNWeb3WebviewPackage()` to the list returned by the `getPackages()` method 34 | 2. Append the following lines to `android/settings.gradle`: 35 | ``` 36 | include ':react-native-web3-webview' 37 | project(':react-native-web3-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-web3-webview/android') 38 | ``` 39 | 3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: 40 | ``` 41 | compile project(':react-native-web3-webview') 42 | ``` 43 | 44 | 45 | ## Usage 46 | ```javascript 47 | import Web3Webview from 'react-native-web3-webview'; 48 | 49 | ... 50 | 51 | render(){ 52 | return ( 53 | 62 | ); 63 | } 64 | 65 | 66 | ``` 67 | 68 | -------------------------------------------------------------------------------- /Web3Webview.android.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from "react"; 4 | import PropTypes from "prop-types"; 5 | import keyMirror from "fbjs/lib/keyMirror"; 6 | import resolveAssetSource from "react-native/Libraries/Image/resolveAssetSource"; 7 | import WebViewShared from "react-native/Libraries/Components/WebView/WebViewShared"; 8 | 9 | import { 10 | requireNativeComponent, 11 | ViewPropTypes, 12 | EdgeInsetsPropType, 13 | ActivityIndicator, 14 | StyleSheet, 15 | UIManager, 16 | View, 17 | findNodeHandle, 18 | NativeModules 19 | } from "react-native"; 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | flex: 1, 24 | backgroundColor: "white" // eslint-disable-line 25 | }, 26 | hidden: { 27 | height: 0, 28 | flex: 0 // disable 'flex:1' when hiding a View 29 | }, 30 | loadingView: { 31 | flex: 1, 32 | justifyContent: "center", 33 | alignItems: "center" 34 | }, 35 | loadingProgressBar: { 36 | height: 20 37 | } 38 | }); 39 | 40 | const WebViewState = keyMirror({ 41 | IDLE: null, 42 | LOADING: null, 43 | ERROR: null 44 | }); 45 | 46 | const defaultRenderLoading = show => 47 | show 48 | ? (( 49 | 50 | 51 | 52 | ): null) 53 | : null; 54 | 55 | /** 56 | * Renders a native WebView. 57 | */ 58 | const Web3Webview = requireNativeComponent("Web3Webview"); 59 | 60 | export default class WebView extends React.Component { 61 | static propTypes = { 62 | ...ViewPropTypes, 63 | renderError: PropTypes.func, 64 | renderLoading: PropTypes.func, 65 | onLoad: PropTypes.func, 66 | onLoadEnd: PropTypes.func, 67 | onLoadStart: PropTypes.func, 68 | onError: PropTypes.func, 69 | onProgress: PropTypes.func, 70 | automaticallyAdjustContentInsets: PropTypes.bool, 71 | contentInset: EdgeInsetsPropType, 72 | onNavigationStateChange: PropTypes.func, 73 | onMessage: PropTypes.func, 74 | onContentSizeChange: PropTypes.func, 75 | onScroll: PropTypes.func, 76 | startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load 77 | style: ViewPropTypes.style, 78 | 79 | /** 80 | * Loads static html or a uri (with optional headers) in the WebView. 81 | */ 82 | source: PropTypes.oneOfType([ 83 | PropTypes.shape({ 84 | /* 85 | * The URI to load in the WebView. Can be a local or remote file. 86 | */ 87 | uri: PropTypes.string, 88 | /* 89 | * The HTTP Method to use. Defaults to GET if not specified. 90 | * NOTE: On Android, only GET and POST are supported. 91 | */ 92 | method: PropTypes.oneOf(["GET", "POST"]), 93 | /* 94 | * Additional HTTP headers to send with the request. 95 | * NOTE: On Android, this can only be used with GET requests. 96 | */ 97 | headers: PropTypes.object, 98 | /* 99 | * The HTTP body to send with the request. This must be a valid 100 | * UTF-8 string, and will be sent exactly as specified, with no 101 | * additional encoding (e.g. URL-escaping or base64) applied. 102 | * NOTE: On Android, this can only be used with POST requests. 103 | */ 104 | body: PropTypes.string 105 | }), 106 | PropTypes.shape({ 107 | /* 108 | * A static HTML page to display in the WebView. 109 | */ 110 | html: PropTypes.string, 111 | /* 112 | * The base URL to be used for any relative links in the HTML. 113 | */ 114 | baseUrl: PropTypes.string 115 | }), 116 | /* 117 | * Used internally by packager. 118 | */ 119 | PropTypes.number 120 | ]), 121 | 122 | /** 123 | * Used on Android only, JS is enabled by default for WebView on iOS 124 | * @platform android 125 | */ 126 | javaScriptEnabled: PropTypes.bool, 127 | 128 | /** 129 | * Used on Android Lollipop and above only, third party cookies are enabled 130 | * by default for WebView on Android Kitkat and below and on iOS 131 | * @platform android 132 | */ 133 | thirdPartyCookiesEnabled: PropTypes.bool, 134 | 135 | /** 136 | * Used on Android only, controls whether DOM Storage is enabled or not 137 | * @platform android 138 | */ 139 | domStorageEnabled: PropTypes.bool, 140 | 141 | /** 142 | * Sets whether Geolocation is enabled. The default is false. 143 | * @platform android 144 | */ 145 | geolocationEnabled: PropTypes.bool, 146 | 147 | /** 148 | * Sets the JS to be injected when the webpage loads. 149 | */ 150 | injectedJavaScript: PropTypes.string, 151 | 152 | /** 153 | * Sets the JS to be injected when the webpage starts loading. 154 | */ 155 | injectedOnStartLoadingJavaScript: PropTypes.string, 156 | /** 157 | * Sets whether the webpage scales to fit the view and the user can change the scale. 158 | */ 159 | scalesPageToFit: PropTypes.bool, 160 | 161 | /** 162 | * Sets the user-agent for this WebView. The user-agent can also be set in native using 163 | * WebViewConfig. This prop will overwrite that config. 164 | */ 165 | userAgent: PropTypes.string, 166 | 167 | /** 168 | * Used to locate this view in end-to-end tests. 169 | */ 170 | testID: PropTypes.string, 171 | 172 | /** 173 | * Determines whether HTML5 audio & videos require the user to tap before they can 174 | * start playing. The default value is `false`. 175 | */ 176 | mediaPlaybackRequiresUserAction: PropTypes.bool, 177 | 178 | /** 179 | * Boolean that sets whether JavaScript running in the context of a file 180 | * scheme URL should be allowed to access content from any origin. 181 | * Including accessing content from other file scheme URLs 182 | * @platform android 183 | */ 184 | allowUniversalAccessFromFileURLs: PropTypes.bool, 185 | 186 | /** 187 | * List of origin strings to allow being navigated to. The strings allow 188 | * wildcards and get matched against *just* the origin (not the full URL). 189 | * If the user taps to navigate to a new page but the new page is not in 190 | * this whitelist, the URL will be opened by the Android OS. 191 | * The default whitelisted origins are "http://*" and "https://*". 192 | */ 193 | originWhitelist: PropTypes.arrayOf(PropTypes.string), 194 | 195 | /** 196 | * Function that accepts a string that will be passed to the WebView and 197 | * executed immediately as JavaScript. 198 | */ 199 | injectJavaScript: PropTypes.func, 200 | 201 | /** 202 | * Specifies the mixed content mode. i.e WebView will allow a secure origin to load content from any other origin. 203 | * 204 | * Possible values for `mixedContentMode` are: 205 | * 206 | * - `'never'` (default) - WebView will not allow a secure origin to load content from an insecure origin. 207 | * - `'always'` - WebView will allow a secure origin to load content from any other origin, even if that origin is insecure. 208 | * - `'compatibility'` - WebView will attempt to be compatible with the approach of a modern web browser with regard to mixed content. 209 | * @platform android 210 | */ 211 | mixedContentMode: PropTypes.oneOf(["never", "always", "compatibility"]), 212 | 213 | /** 214 | * Used on Android only, controls whether form autocomplete data should be saved 215 | * @platform android 216 | */ 217 | saveFormDataDisabled: PropTypes.bool, 218 | 219 | /** 220 | * Override the native component used to render the WebView. Enables a custom native 221 | * WebView which uses the same JavaScript as the original WebView. 222 | */ 223 | nativeConfig: PropTypes.shape({ 224 | /* 225 | * The native component used to render the WebView. 226 | */ 227 | component: PropTypes.any, 228 | /* 229 | * Set props directly on the native component WebView. Enables custom props which the 230 | * original WebView doesn't pass through. 231 | */ 232 | props: PropTypes.object, 233 | /* 234 | * Set the ViewManager to use for communication with the native side. 235 | * @platform ios 236 | */ 237 | viewManager: PropTypes.object 238 | }), 239 | /* 240 | * Used on Android only, controls whether the given list of URL prefixes should 241 | * make {@link com.facebook.react.views.webview.ReactWebViewClient} to launch a 242 | * default activity intent for those URL instead of loading it within the webview. 243 | * Use this to list URLs that WebView cannot handle, e.g. a PDF url. 244 | * @platform android 245 | */ 246 | urlPrefixesForDefaultIntent: PropTypes.arrayOf(PropTypes.string), 247 | /** 248 | * Show the default loader view 249 | */ 250 | showDefaultLoader: PropTypes.bool 251 | }; 252 | 253 | static defaultProps = { 254 | javaScriptEnabled: true, 255 | messagingEnabled: true, 256 | thirdPartyCookiesEnabled: true, 257 | scalesPageToFit: true, 258 | saveFormDataDisabled: false, 259 | originWhitelist: WebViewShared.defaultOriginWhitelist, 260 | showDefaultLoader: false, 261 | openNewWindowInWebView: true 262 | }; 263 | 264 | static isFileUploadSupported = async () => { 265 | // native implementation should return "true" only for Android 5+ 266 | return NativeModules.Web3Webview.isFileUploadSupported(); 267 | } 268 | 269 | state = { 270 | viewState: WebViewState.IDLE, 271 | lastErrorEvent: null, 272 | startInLoadingState: true 273 | }; 274 | 275 | UNSAFE_componentWillMount() { 276 | if (this.props.startInLoadingState) { 277 | this.setState({ viewState: WebViewState.LOADING }); 278 | } 279 | } 280 | 281 | setWebviewRef = ref => { 282 | this.webview = ref; 283 | }; 284 | 285 | render() { 286 | let otherView = null; 287 | 288 | if (this.state.viewState === WebViewState.LOADING) { 289 | otherView = this.props.renderLoading 290 | ? this.props.renderLoading() 291 | : defaultRenderLoading(this.props.showDefaultLoader); 292 | } else if (this.state.viewState === WebViewState.ERROR) { 293 | const errorEvent = this.state.lastErrorEvent; 294 | otherView = 295 | this.props.renderError && 296 | this.props.renderError( 297 | errorEvent.domain, 298 | errorEvent.code, 299 | errorEvent.description 300 | ); 301 | } 302 | 303 | const webViewStyles = [styles.container, this.props.style]; 304 | if ( 305 | this.state.viewState === WebViewState.LOADING || 306 | this.state.viewState === WebViewState.ERROR 307 | ) { 308 | // if we're in either LOADING or ERROR states, don't show the webView 309 | webViewStyles.push(styles.hidden); 310 | } 311 | 312 | const source = this.props.source || {}; 313 | if (this.props.html) { 314 | source.html = this.props.html; 315 | } else if (this.props.url) { 316 | source.uri = this.props.url; 317 | } 318 | 319 | const nativeConfig = this.props.nativeConfig || {}; 320 | 321 | const originWhitelist = (this.props.originWhitelist || []).map( 322 | WebViewShared.originWhitelistToRegex 323 | ); 324 | 325 | const NativeWebView = nativeConfig.component || Web3Webview; 326 | 327 | const webView = ( 328 | 370 | ); 371 | 372 | return ( 373 | 374 | {webView} 375 | {otherView} 376 | 377 | ); 378 | } 379 | 380 | goForward = () => { 381 | UIManager.dispatchViewManagerCommand( 382 | this.getWebViewHandle(), 383 | UIManager.Web3Webview.Commands.goForward, 384 | null 385 | ); 386 | }; 387 | 388 | goBack = () => { 389 | UIManager.dispatchViewManagerCommand( 390 | this.getWebViewHandle(), 391 | UIManager.Web3Webview.Commands.goBack, 392 | null 393 | ); 394 | }; 395 | 396 | reload = () => { 397 | this.setState({ 398 | viewState: WebViewState.LOADING 399 | }); 400 | UIManager.dispatchViewManagerCommand( 401 | this.getWebViewHandle(), 402 | UIManager.Web3Webview.Commands.reload, 403 | null 404 | ); 405 | }; 406 | 407 | stopLoading = () => { 408 | UIManager.dispatchViewManagerCommand( 409 | this.getWebViewHandle(), 410 | UIManager.Web3Webview.Commands.stopLoading, 411 | null 412 | ); 413 | }; 414 | 415 | postMessage = data => { 416 | const webViewHandle = this.getWebViewHandle() 417 | if (!webViewHandle) return 418 | UIManager.dispatchViewManagerCommand( 419 | webViewHandle, 420 | UIManager.Web3Webview.Commands.postMessage, 421 | [String(data)] 422 | ); 423 | }; 424 | 425 | /** 426 | * Injects a javascript string into the referenced WebView. Deliberately does not 427 | * return a response because using eval() to return a response breaks this method 428 | * on pages with a Content Security Policy that disallows eval(). If you need that 429 | * functionality, look into postMessage/onMessage. 430 | */ 431 | injectJavaScript = data => { 432 | UIManager.dispatchViewManagerCommand( 433 | this.getWebViewHandle(), 434 | UIManager.Web3Webview.Commands.injectJavaScript, 435 | [data] 436 | ); 437 | }; 438 | 439 | /** 440 | * We return an event with a bunch of fields including: 441 | * url, title, loading, canGoBack, canGoForward 442 | */ 443 | updateNavigationState = event => { 444 | if (this.props.onNavigationStateChange) { 445 | this.props.onNavigationStateChange(event.nativeEvent); 446 | } 447 | }; 448 | 449 | getWebViewHandle = () => findNodeHandle(this.webview); 450 | 451 | onLoadingStart = event => { 452 | const onLoadStart = this.props.onLoadStart; 453 | onLoadStart && onLoadStart(event); 454 | this.updateNavigationState(event); 455 | }; 456 | 457 | onLoadingError = event => { 458 | event.persist(); // persist this event because we need to store it 459 | const { onError, onLoadEnd } = this.props; 460 | onError && onError(event); 461 | onLoadEnd && onLoadEnd(event); 462 | 463 | this.setState({ 464 | lastErrorEvent: event.nativeEvent, 465 | viewState: WebViewState.ERROR 466 | }); 467 | }; 468 | 469 | onLoadingFinish = event => { 470 | const { onLoad, onLoadEnd } = this.props; 471 | onLoad && onLoad(event); 472 | onLoadEnd && onLoadEnd(event); 473 | this.setState({ 474 | viewState: WebViewState.IDLE 475 | }); 476 | this.updateNavigationState(event); 477 | }; 478 | 479 | onProgress = event => { 480 | const { onProgress } = this.props; 481 | onProgress && onProgress(parseFloat(event.nativeEvent.progress / 100)); 482 | }; 483 | 484 | onMessage = event => { 485 | const { onMessage } = this.props; 486 | onMessage && onMessage(event); 487 | }; 488 | 489 | onScroll = event => { 490 | const {onScroll} = this.props; 491 | onScroll && onScroll(event.nativeEvent); 492 | } 493 | } 494 | -------------------------------------------------------------------------------- /Web3Webview.ios.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from "react"; 4 | import PropTypes from "prop-types"; 5 | import ReactNative, { 6 | requireNativeComponent, 7 | EdgeInsetsPropType, 8 | StyleSheet, 9 | UIManager, 10 | View, 11 | ViewPropTypes, 12 | NativeModules, 13 | Text, 14 | ActivityIndicator 15 | } from "react-native"; 16 | 17 | import resolveAssetSource from "react-native/Libraries/Image/resolveAssetSource"; 18 | import deprecatedPropType from "react-native/Libraries/Utilities/deprecatedPropType"; 19 | import invariant from "fbjs/lib/invariant"; 20 | import keyMirror from "fbjs/lib/keyMirror"; 21 | import WebViewShared from "react-native/Libraries/Components/WebView/WebViewShared"; 22 | const RNWeb3WebviewManager = NativeModules.RNWeb3WebviewManager; 23 | 24 | let BGWASH = "rgba(255,255,255,0.8)"; 25 | 26 | const styles = StyleSheet.create({ 27 | container: { 28 | flex: 1 29 | }, 30 | errorContainer: { 31 | flex: 1, 32 | justifyContent: "center", 33 | alignItems: "center", 34 | backgroundColor: BGWASH 35 | }, 36 | errorText: { 37 | fontSize: 14, 38 | textAlign: "center", 39 | marginBottom: 2 40 | }, 41 | errorTextTitle: { 42 | fontSize: 15, 43 | fontWeight: "500", 44 | marginBottom: 10 45 | }, 46 | hidden: { 47 | height: 0, 48 | flex: 0 // disable 'flex:1' when hiding a View 49 | }, 50 | loadingView: { 51 | backgroundColor: BGWASH, 52 | flex: 1, 53 | justifyContent: "center", 54 | alignItems: "center", 55 | height: 100 56 | }, 57 | webView: { 58 | backgroundColor: "#ffffff" // eslint-disable-line 59 | } 60 | }); 61 | 62 | const WebViewState = keyMirror({ 63 | IDLE: null, 64 | LOADING: null, 65 | ERROR: null 66 | }); 67 | 68 | const NavigationType = keyMirror({ 69 | click: true, 70 | formsubmit: true, 71 | backforward: true, 72 | reload: true, 73 | formresubmit: true, 74 | other: true 75 | }); 76 | 77 | const JSNavigationScheme = "react-js-navigation"; 78 | 79 | type ErrorEvent = { 80 | domain: any, 81 | code: any, 82 | description: any 83 | }; 84 | 85 | type Event = Object; 86 | 87 | const defaultRenderLoading = show => 88 | show 89 | ? (( 90 | 91 | 92 | 93 | ): null) 94 | : null; 95 | const defaultRenderError = (errorDomain, errorCode, errorDesc) => ( 96 | 97 | Error loading page 98 | {"Domain: " + errorDomain} 99 | {"Error Code: " + errorCode} 100 | {"Description: " + errorDesc} 101 | 102 | ); 103 | 104 | /** 105 | * Renders a native WebView. 106 | */ 107 | 108 | class Web3Webview extends React.Component { 109 | static JSNavigationScheme = JSNavigationScheme; 110 | static NavigationType = NavigationType; 111 | 112 | static propTypes = { 113 | ...ViewPropTypes, 114 | 115 | html: deprecatedPropType( 116 | PropTypes.string, 117 | "Use the `source` prop instead." 118 | ), 119 | 120 | url: deprecatedPropType( 121 | PropTypes.string, 122 | "Use the `source` prop instead." 123 | ), 124 | 125 | /** 126 | * Loads static html or a uri (with optional headers) in the WebView. 127 | */ 128 | source: PropTypes.oneOfType([ 129 | PropTypes.shape({ 130 | /* 131 | * The URI to load in the WebView. Can be a local or remote file. 132 | */ 133 | uri: PropTypes.string, 134 | /* 135 | * The HTTP Method to use. Defaults to GET if not specified. 136 | * NOTE: On Android, only GET and POST are supported. 137 | */ 138 | method: PropTypes.string, 139 | /* 140 | * Additional HTTP headers to send with the request. 141 | * NOTE: On Android, this can only be used with GET requests. 142 | */ 143 | headers: PropTypes.object, 144 | /* 145 | * The HTTP body to send with the request. This must be a valid 146 | * UTF-8 string, and will be sent exactly as specified, with no 147 | * additional encoding (e.g. URL-escaping or base64) applied. 148 | * NOTE: On Android, this can only be used with POST requests. 149 | */ 150 | body: PropTypes.string 151 | }), 152 | PropTypes.shape({ 153 | /* 154 | * A static HTML page to display in the WebView. 155 | */ 156 | html: PropTypes.string, 157 | /* 158 | * The base URL to be used for any relative links in the HTML. 159 | */ 160 | baseUrl: PropTypes.string 161 | }), 162 | /* 163 | * Used internally by packager. 164 | */ 165 | PropTypes.number 166 | ]), 167 | 168 | /** 169 | * This property specifies how the safe area insets are used to modify the 170 | * content area of the scroll view. The default value of this property is 171 | * "never". Available on iOS 11 and later. 172 | */ 173 | contentInsetAdjustmentBehavior: PropTypes.oneOf([ 174 | "automatic", 175 | "scrollableAxes", 176 | "never", // default 177 | "always" 178 | ]), 179 | 180 | /** 181 | * Function that returns a view to show if there's an error. 182 | */ 183 | renderError: PropTypes.func, // view to show if there's an error 184 | /** 185 | * Function that returns a loading indicator. 186 | */ 187 | renderLoading: PropTypes.func, 188 | /** 189 | * Invoked when load finish 190 | */ 191 | onLoad: PropTypes.func, 192 | /** 193 | * Invoked when load either succeeds or fails 194 | */ 195 | onLoadEnd: PropTypes.func, 196 | /** 197 | * Invoked on load start 198 | */ 199 | onLoadStart: PropTypes.func, 200 | /** 201 | * Invoked when load fails 202 | */ 203 | onError: PropTypes.func, 204 | /** 205 | * Report the progress 206 | */ 207 | onProgress: PropTypes.func, 208 | /** 209 | * A function that is invoked when the webview calls `window.postMessage`. 210 | * Setting this property will inject a `postMessage` global into your 211 | * webview, but will still call pre-existing values of `postMessage`. 212 | * 213 | * `window.postMessage` accepts one argument, `data`, which will be 214 | * available on the event object, `event.nativeEvent.data`. `data` 215 | * must be a string. 216 | */ 217 | onMessage: PropTypes.func, 218 | /** 219 | * Receive scroll events from view 220 | */ 221 | onScroll: PropTypes.func, 222 | /** 223 | * Called when the momentum scroll starts (scroll which occurs as the ScrollView glides to a stop). 224 | */ 225 | onMomentumScrollBegin: PropTypes.func, 226 | /** 227 | * Called when the momentum scroll ends (scroll which occurs as the ScrollView glides to a stop). 228 | */ 229 | onMomentumScrollEnd: PropTypes.func, 230 | /** 231 | * Called when the user begins to drag the scroll view. 232 | */ 233 | onScrollBeginDrag: PropTypes.func, 234 | /** 235 | * Called when the user stops dragging the scroll view and it either stops 236 | * or begins to glide. 237 | */ 238 | onScrollEndDrag: PropTypes.func, 239 | /** 240 | * Receive scroll events from view 241 | */ 242 | onScroll: PropTypes.func, 243 | /** 244 | * This controls how often the scroll event will be fired while scrolling 245 | * (as a time interval in ms). A lower number yields better accuracy for code 246 | * that is tracking the scroll position, but can lead to scroll performance 247 | * problems due to the volume of information being send over the bridge. 248 | * You will not notice a difference between values set between 1-16 as the 249 | * JS run loop is synced to the screen refresh rate. If you do not need precise 250 | * scroll position tracking, set this value higher to limit the information 251 | * being sent across the bridge. The default value is zero, which results in 252 | * the scroll event being sent only once each time the view is scrolled. 253 | * @platform ios 254 | */ 255 | scrollEventThrottle: PropTypes.number, 256 | /** 257 | * @platform ios 258 | */ 259 | bounces: PropTypes.bool, 260 | scrollEnabled: PropTypes.bool, 261 | allowsBackForwardNavigationGestures: PropTypes.bool, 262 | automaticallyAdjustContentInsets: PropTypes.bool, 263 | contentInset: EdgeInsetsPropType, 264 | onNavigationStateChange: PropTypes.func, 265 | scalesPageToFit: PropTypes.bool, 266 | startInLoadingState: PropTypes.bool, 267 | style: ViewPropTypes.style, 268 | /** 269 | * If false injectJavaScript will run both main frame and iframe 270 | * @platform ios 271 | */ 272 | injectJavaScriptForMainFrameOnly: PropTypes.bool, 273 | /** 274 | * If false injectedJavaScript will run both main frame and iframe 275 | * @platform ios 276 | */ 277 | injectedJavaScriptForMainFrameOnly: PropTypes.bool, 278 | /** 279 | * Function that accepts a string that will be passed to the WebView and executed immediately as JavaScript. 280 | */ 281 | injectJavaScript: PropTypes.string, 282 | /** 283 | * Sets the JS to be injected when the webpage loads. 284 | */ 285 | injectedJavaScript: PropTypes.string, 286 | /** 287 | * Sets the JS to be injected when the webpage loads 288 | * (added for compatibility with Android) 289 | */ 290 | injectedOnStartLoadingJavaScript: PropTypes.string, 291 | /** 292 | * Allows custom handling of any webview requests by a JS handler. Return true 293 | * or false from this method to continue loading the request. 294 | * @platform ios 295 | */ 296 | onShouldStartLoadWithRequest: PropTypes.func, 297 | /** 298 | * Copies cookies from sharedHTTPCookieStorage when calling loadRequest. 299 | * Set this to true to emulate behavior of WebView component. 300 | */ 301 | sendCookies: PropTypes.bool, 302 | /** 303 | * If set to true, target="_blank" or window.open will be opened in WebView, instead 304 | * of new window. Default is false to be backward compatible. 305 | */ 306 | openNewWindowInWebView: PropTypes.bool, 307 | /** 308 | * Hide the accessory view when the keyboard is open. Default is false to be 309 | * backward compatible. 310 | */ 311 | hideKeyboardAccessoryView: PropTypes.bool, 312 | /** 313 | * Enable the keyboard to display when focusing an input in a webview programatically 314 | */ 315 | keyboardDisplayRequiresUserAction: PropTypes.bool, 316 | /** 317 | * A Boolean value that determines whether pressing on a link displays a preview of the destination for the link. This props is available on devices that support 3D Touch. In iOS 10 and later, the default value is true; before that, the default value is false. 318 | */ 319 | allowsLinkPreview: PropTypes.bool, 320 | /** 321 | * Sets the customized user agent by using of the RNWeb3Webview 322 | */ 323 | customUserAgent: PropTypes.string, 324 | userAgent: PropTypes.string, 325 | /** 326 | * A Boolean value that determines whether paging is enabled for the scroll view. 327 | */ 328 | pagingEnabled: PropTypes.bool, 329 | /** 330 | * A Boolean value that sets whether diagonal scrolling is allowed. 331 | */ 332 | directionalLockEnabled: PropTypes.bool, 333 | /** 334 | * Show the default loader view 335 | */ 336 | showDefaultLoader: PropTypes.bool 337 | }; 338 | 339 | state = { 340 | viewState: WebViewState.IDLE, 341 | lastErrorEvent: (null: ?ErrorEvent), 342 | startInLoadingState: true 343 | }; 344 | 345 | static defaultProps = { 346 | javaScriptEnabled: true, 347 | messagingEnabled: true, 348 | thirdPartyCookiesEnabled: true, 349 | scalesPageToFit: true, 350 | saveFormDataDisabled: false, 351 | originWhitelist: WebViewShared.defaultOriginWhitelist, 352 | showDefaultLoader: false, 353 | openNewWindowInWebView: true 354 | }; 355 | 356 | static isFileUploadSupported = async () => { 357 | // no native implementation for iOS, depends only on permissions 358 | return true; 359 | } 360 | 361 | 362 | UNSAFE_componentWillMount() { 363 | if (this.props.startInLoadingState) { 364 | this.setState({ viewState: WebViewState.LOADING }); 365 | } 366 | } 367 | 368 | render() { 369 | let otherView = null; 370 | 371 | if (this.state.viewState === WebViewState.LOADING) { 372 | otherView = this.props.renderLoading 373 | ? this.props.renderLoading() 374 | : defaultRenderLoading(this.props.showDefaultLoader); 375 | } else if (this.state.viewState === WebViewState.ERROR) { 376 | const errorEvent = this.state.lastErrorEvent; 377 | invariant( 378 | errorEvent != null, 379 | "lastErrorEvent expected to be non-null" 380 | ); 381 | otherView = (this.props.renderError || defaultRenderError)( 382 | errorEvent.domain, 383 | errorEvent.code, 384 | errorEvent.description 385 | ); 386 | } else if (this.state.viewState !== WebViewState.IDLE) { 387 | console.error( 388 | "RNWeb3Webview invalid state encountered: " + this.state.loading 389 | ); 390 | } 391 | 392 | const webViewStyles = [ 393 | styles.container, 394 | styles.webView, 395 | this.props.style 396 | ]; 397 | if ( 398 | this.state.viewState === WebViewState.LOADING || 399 | this.state.viewState === WebViewState.ERROR 400 | ) { 401 | // if we're in either LOADING or ERROR states, don't show the webView 402 | webViewStyles.push(styles.hidden); 403 | } 404 | 405 | const onShouldStartLoadWithRequest = 406 | this.props.onShouldStartLoadWithRequest && 407 | ((event: Event) => { 408 | const shouldStart = 409 | this.props.onShouldStartLoadWithRequest && 410 | this.props.onShouldStartLoadWithRequest(event.nativeEvent); 411 | RNWeb3WebviewManager.startLoadWithResult( 412 | !!shouldStart, 413 | event.nativeEvent.lockIdentifier 414 | ); 415 | }); 416 | 417 | let source = {...this.props.source} || {}; 418 | if (typeof source == "object") { 419 | if(this.props.sendCookies){ 420 | source.sendCookies = this.props.sendCookies; 421 | } 422 | if(this.props.customUserAgent || this.props.userAgent){ 423 | source.customUserAgent = 424 | this.props.customUserAgent || this.props.userAgent; 425 | } 426 | } 427 | 428 | if (this.props.html) { 429 | source.html = this.props.html; 430 | } else if (this.props.url) { 431 | source.uri = this.props.url; 432 | } 433 | 434 | const messagingEnabled = typeof this.props.onMessage === "function"; 435 | 436 | const webView = ( 437 | { 439 | this.webview = ref; 440 | }} 441 | key="webViewKey" 442 | style={webViewStyles} 443 | contentInsetAdjustmentBehavior={ 444 | this.props.contentInsetAdjustmentBehavior 445 | } 446 | source={resolveAssetSource(source)} 447 | injectJavaScriptForMainFrameOnly={ 448 | this.props.injectJavaScriptForMainFrameOnly 449 | } 450 | injectedJavaScriptForMainFrameOnly={ 451 | this.props.injectedJavaScriptForMainFrameOnly 452 | } 453 | injectJavaScript={ 454 | this.props.injectJavaScript || 455 | this.props.injectedOnStartLoadingJavaScript 456 | } 457 | injectedJavaScript={ this.props.injectedJavaScript } 458 | bounces={this.props.bounces} 459 | scrollEnabled={this.props.scrollEnabled} 460 | contentInset={this.props.contentInset} 461 | allowsBackForwardNavigationGestures={ 462 | this.props.allowsBackForwardNavigationGestures 463 | } 464 | automaticallyAdjustContentInsets={ 465 | this.props.automaticallyAdjustContentInsets 466 | } 467 | openNewWindowInWebView={this.props.openNewWindowInWebView} 468 | hideKeyboardAccessoryView={this.props.hideKeyboardAccessoryView} 469 | keyboardDisplayRequiresUserAction={ 470 | this.props.keyboardDisplayRequiresUserAction 471 | } 472 | allowsLinkPreview={this.props.allowsLinkPreview} 473 | onLoadingStart={this._onLoadingStart} 474 | onLoadingFinish={this._onLoadingFinish} 475 | onLoadingError={this._onLoadingError} 476 | messagingEnabled={messagingEnabled} 477 | onProgress={this._onProgress} 478 | onMessage={this._onMessage} 479 | onScroll={this._onScroll} 480 | onScrollToTop={this._onScrollToTop} 481 | onScrollBeginDrag={this._onScrollBeginDrag} 482 | scrollEventThrottle={this.props.scrollEventThrottle} 483 | onScrollEndDrag={this._onScrollEndDrag} 484 | onMomentumScrollBegin={this._onMomentumScrollBegin} 485 | onMomentumScrollEnd={this._onMomentumScrollEnd} 486 | onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} 487 | pagingEnabled={this.props.pagingEnabled} 488 | directionalLockEnabled={this.props.directionalLockEnabled} 489 | /> 490 | ); 491 | 492 | return ( 493 | 494 | {webView} 495 | {otherView} 496 | 497 | ); 498 | } 499 | 500 | /** 501 | * Go forward one page in the webview's history. 502 | */ 503 | goForward = () => { 504 | UIManager.dispatchViewManagerCommand( 505 | this.getWebViewHandle(), 506 | UIManager.RNWeb3Webview.Commands.goForward, 507 | null 508 | ); 509 | }; 510 | 511 | /** 512 | * Go back one page in the webview's history. 513 | */ 514 | goBack = () => { 515 | UIManager.dispatchViewManagerCommand( 516 | this.getWebViewHandle(), 517 | UIManager.RNWeb3Webview.Commands.goBack, 518 | null 519 | ); 520 | }; 521 | 522 | /** 523 | * Indicating whether there is a back item in the back-forward list that can be navigated to 524 | */ 525 | canGoBack = () => { 526 | return RNWeb3WebviewManager.canGoBack(this.getWebViewHandle()); 527 | }; 528 | 529 | /** 530 | * Indicating whether there is a forward item in the back-forward list that can be navigated to 531 | */ 532 | canGoForward = () => { 533 | return RNWeb3WebviewManager.canGoForward(this.getWebViewHandle()); 534 | }; 535 | 536 | /** 537 | * Reloads the current page. 538 | */ 539 | reload = () => { 540 | this.setState({ viewState: WebViewState.LOADING }); 541 | UIManager.dispatchViewManagerCommand( 542 | this.getWebViewHandle(), 543 | UIManager.RNWeb3Webview.Commands.reload, 544 | null 545 | ); 546 | }; 547 | 548 | /** 549 | * Stop loading the current page. 550 | */ 551 | stopLoading = () => { 552 | UIManager.dispatchViewManagerCommand( 553 | this.getWebViewHandle(), 554 | UIManager.RNWeb3Webview.Commands.stopLoading, 555 | null 556 | ); 557 | }; 558 | 559 | /** 560 | * Posts a message to the web view, which will emit a `message` event. 561 | * Accepts one argument, `data`, which must be a string. 562 | * 563 | * In your webview, you'll need to something like the following. 564 | * 565 | * ```js 566 | * document.addEventListener('message', e => { document.title = e.data; }); 567 | * ``` 568 | */ 569 | postMessage = data => { 570 | const webViewHandle = this.getWebViewHandle() 571 | if (!webViewHandle) return 572 | UIManager.dispatchViewManagerCommand( 573 | webViewHandle, 574 | UIManager.RNWeb3Webview.Commands.postMessage, 575 | [String(data)] 576 | ); 577 | }; 578 | 579 | evaluateJavaScript = js => { 580 | return RNWeb3WebviewManager.evaluateJavaScript( 581 | this.getWebViewHandle(), 582 | js 583 | ); 584 | }; 585 | 586 | /** 587 | * We return an event with a bunch of fields including: 588 | * url, title, loading, canGoBack, canGoForward 589 | */ 590 | _updateNavigationState = (event: Event) => { 591 | if (this.props.onNavigationStateChange) { 592 | this.props.onNavigationStateChange(event.nativeEvent); 593 | } 594 | }; 595 | 596 | /** 597 | * Returns the native webview node. 598 | */ 599 | getWebViewHandle = (): any => { 600 | return ReactNative.findNodeHandle(this.webview); 601 | }; 602 | 603 | _onLoadingStart = (event: Event) => { 604 | const onLoadStart = this.props.onLoadStart; 605 | onLoadStart && onLoadStart(event); 606 | this._updateNavigationState(event); 607 | }; 608 | 609 | _onLoadingError = (event: Event) => { 610 | event.persist(); // persist this event because we need to store it 611 | const { onError, onLoadEnd } = this.props; 612 | onError && onError(event); 613 | onLoadEnd && onLoadEnd(event); 614 | console.warn("Encountered an error loading page", event.nativeEvent); 615 | 616 | this.setState({ 617 | lastErrorEvent: event.nativeEvent, 618 | viewState: WebViewState.ERROR 619 | }); 620 | }; 621 | 622 | _onLoadingFinish = (event: Event) => { 623 | const { onLoad, onLoadEnd } = this.props; 624 | onLoad && onLoad(event); 625 | onLoadEnd && onLoadEnd(event); 626 | this.setState({ 627 | viewState: WebViewState.IDLE 628 | }); 629 | this._updateNavigationState(event); 630 | }; 631 | 632 | _onProgress = (event: Event) => { 633 | const onProgress = this.props.onProgress; 634 | onProgress && onProgress(event.nativeEvent.progress); 635 | }; 636 | 637 | _onMessage = (event: Event) => { 638 | var { onMessage } = this.props; 639 | onMessage && onMessage(event); 640 | }; 641 | 642 | _onScroll = (event: Event) => { 643 | const onScroll = this.props.onScroll; 644 | onScroll && onScroll(event.nativeEvent); 645 | }; 646 | 647 | _onScrollToTop = (event: Event) => { 648 | const onScrollToTop = this.props.onScrollToTop; 649 | onScrollToTop && onScrollToTop(event.nativeEvent); 650 | }; 651 | 652 | _onScrollBeginDrag = (event: Event) => { 653 | const onScrollBeginDrag = this.props.onScrollBeginDrag; 654 | onScrollBeginDrag && onScrollBeginDrag(event.nativeEvent); 655 | }; 656 | 657 | _onScrollEndDrag = (event: Event) => { 658 | const onScrollEndDrag = this.props.onScrollEndDrag; 659 | onScrollEndDrag && onScrollEndDrag(event.nativeEvent); 660 | }; 661 | 662 | _onMomentumScrollBegin = (event: Event) => { 663 | const onMomentumScrollBegin = this.props.onMomentumScrollBegin; 664 | onMomentumScrollBegin && onMomentumScrollBegin(event.nativeEvent); 665 | }; 666 | 667 | _onMomentumScrollEnd = (event: Event) => { 668 | const onMomentumScrollEnd = this.props.onMomentumScrollEnd; 669 | onMomentumScrollEnd && onMomentumScrollEnd(event.nativeEvent); 670 | }; 671 | } 672 | 673 | const RNWeb3Webview = requireNativeComponent("RNWeb3Webview", RNWeb3Webview, { 674 | nativeOnly: { 675 | onLoadingStart: true, 676 | onLoadingError: true, 677 | onLoadingFinish: true, 678 | onMessage: true, 679 | messagingEnabled: PropTypes.bool 680 | } 681 | }); 682 | 683 | export default Web3Webview; 684 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-native-web3-webview 4 | Project react-native-web3-webview created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | google() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.1.4' 10 | } 11 | } 12 | 13 | apply plugin: 'com.android.library' 14 | 15 | android { 16 | compileSdkVersion 27 17 | buildToolsVersion "27.0.3" 18 | 19 | defaultConfig { 20 | minSdkVersion 19 21 | targetSdkVersion 27 22 | versionCode 1 23 | versionName "1.0" 24 | } 25 | lintOptions { 26 | abortOnError false 27 | } 28 | } 29 | 30 | repositories { 31 | mavenCentral() 32 | } 33 | 34 | dependencies { 35 | api 'com.facebook.react:react-native:+' 36 | } 37 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /android/src/main/java/com/web3webview/InputStreamWithInjectedJS.java: -------------------------------------------------------------------------------- 1 | 2 | package com.web3webview; 3 | 4 | import android.content.Context; 5 | import android.os.Build; 6 | import androidx.annotation.RequiresApi; 7 | import android.util.Log; 8 | 9 | import java.io.ByteArrayInputStream; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.nio.charset.Charset; 13 | import java.nio.charset.UnsupportedCharsetException; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | import static java.nio.charset.StandardCharsets.*; 18 | @RequiresApi(api = Build.VERSION_CODES.KITKAT) 19 | public class InputStreamWithInjectedJS extends InputStream { 20 | private InputStream pageIS; 21 | private InputStream scriptIS; 22 | private Charset charset; 23 | private static final String TAG = "InputStreamWithInjectedJS"; 24 | private static Map script = new HashMap<>(); 25 | private boolean hasJS = false; 26 | private boolean headWasFound = false; 27 | private boolean scriptWasInjected = false; 28 | private boolean openingHeadFound = false; 29 | private StringBuffer contentBuffer = new StringBuffer(); 30 | private static Charset getCharset(String charsetName) { 31 | Charset cs = UTF_8; 32 | try { 33 | if (charsetName != null) { 34 | cs = Charset.forName(charsetName); 35 | } 36 | } catch (UnsupportedCharsetException e) { 37 | Log.d("CustomWebview", "wrong charset: " + charsetName); 38 | } 39 | return cs; 40 | } 41 | private static InputStream getScript(Charset charset) { 42 | String js = script.get(charset); 43 | if (js == null) { 44 | String defaultJs = script.get(UTF_8); 45 | js = new String(defaultJs.getBytes(UTF_8), charset); 46 | script.put(charset, js); 47 | } 48 | return new ByteArrayInputStream(js.getBytes(charset)); 49 | } 50 | InputStreamWithInjectedJS(InputStream is, String js, Charset charset, Context c) { 51 | if (js == null) { 52 | this.pageIS = is; 53 | } else { 54 | this.hasJS = true; 55 | this.charset = charset; 56 | Charset cs = UTF_8; 57 | String jsScript = ""; 58 | script.put(cs, jsScript); 59 | this.pageIS = is; 60 | } 61 | } 62 | @Override 63 | public int read() throws IOException { 64 | if (scriptWasInjected || !hasJS) { 65 | return pageIS.read(); 66 | } 67 | 68 | if (!scriptWasInjected && headWasFound) { 69 | int nextByte = scriptIS.read(); 70 | if (nextByte == -1) { 71 | scriptIS.close(); 72 | scriptWasInjected = true; 73 | return pageIS.read(); 74 | } else { 75 | return nextByte; 76 | } 77 | } 78 | if (!headWasFound) { 79 | int nextByte = pageIS.read(); 80 | char nextByteStr = (char) nextByte; 81 | contentBuffer.append(nextByteStr); 82 | int bufferLength = contentBuffer.length(); 83 | String headString = "= 5) { 92 | String stringToMatch = contentBuffer.substring(bufferLength - 5).toLowerCase(); 93 | if (stringToMatch.contains(headString)) { 94 | openingHeadFound = true; 95 | } 96 | } 97 | } 98 | return nextByte; 99 | } 100 | return pageIS.read(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /android/src/main/java/com/web3webview/ProgressEvent.java: -------------------------------------------------------------------------------- 1 | package com.web3webview; 2 | 3 | import com.facebook.react.bridge.Arguments; 4 | import com.facebook.react.bridge.WritableMap; 5 | import com.facebook.react.uimanager.events.Event; 6 | import com.facebook.react.uimanager.events.RCTEventEmitter; 7 | 8 | public class ProgressEvent extends Event { 9 | 10 | public static final String EVENT_NAME = "progress"; 11 | 12 | private final int mProgress; 13 | 14 | public ProgressEvent(int viewId, int progress) { 15 | super(viewId); 16 | mProgress = progress; 17 | } 18 | 19 | @Override 20 | public String getEventName() { 21 | return EVENT_NAME; 22 | } 23 | 24 | @Override 25 | public void dispatch(RCTEventEmitter rctEventEmitter) { 26 | WritableMap data = Arguments.createMap(); 27 | data.putInt("progress", mProgress); 28 | rctEventEmitter.receiveEvent(getViewTag(), EVENT_NAME, data); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /android/src/main/java/com/web3webview/Web3WebviewFileProvider.java: -------------------------------------------------------------------------------- 1 | package com.web3webview; 2 | import androidx.core.content.FileProvider; 3 | 4 | /** 5 | * Providing a custom {@code FileProvider} prevents manifest {@code } name collisions. 6 | *

7 | * See https://developer.android.com/guide/topics/manifest/provider-element.html for details. 8 | */ 9 | public class Web3WebviewFileProvider extends FileProvider { 10 | 11 | // This class intentionally left blank. 12 | 13 | } -------------------------------------------------------------------------------- /android/src/main/java/com/web3webview/Web3WebviewManager.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package com.web3webview; 9 | 10 | import android.annotation.TargetApi; 11 | import android.content.ActivityNotFoundException; 12 | import android.content.Context; 13 | import android.content.Intent; 14 | import android.graphics.Bitmap; 15 | import android.graphics.Picture; 16 | import android.net.Uri; 17 | import android.os.Build; 18 | import androidx.annotation.RequiresApi; 19 | import androidx.annotation.NonNull; 20 | import android.text.TextUtils; 21 | import android.view.View; 22 | import android.view.ViewGroup.LayoutParams; 23 | import android.webkit.ConsoleMessage; 24 | import android.webkit.CookieManager; 25 | import android.webkit.GeolocationPermissions; 26 | import android.webkit.JavascriptInterface; 27 | import android.webkit.ServiceWorkerClient; 28 | import android.webkit.ServiceWorkerController; 29 | import android.webkit.ValueCallback; 30 | import android.webkit.WebChromeClient; 31 | import android.webkit.WebResourceRequest; 32 | import android.webkit.WebResourceResponse; 33 | import android.webkit.WebSettings; 34 | import android.webkit.WebView; 35 | import android.webkit.WebViewClient; 36 | 37 | import com.facebook.common.logging.FLog; 38 | import com.facebook.react.bridge.Arguments; 39 | import com.facebook.react.bridge.LifecycleEventListener; 40 | import com.facebook.react.bridge.ReactApplicationContext; 41 | import com.facebook.react.bridge.ReactContext; 42 | import com.facebook.react.bridge.ReadableArray; 43 | import com.facebook.react.bridge.ReadableMap; 44 | import com.facebook.react.bridge.ReadableMapKeySetIterator; 45 | import com.facebook.react.bridge.WritableMap; 46 | import com.facebook.react.common.MapBuilder; 47 | import com.facebook.react.common.ReactConstants; 48 | import com.facebook.react.common.build.ReactBuildConfig; 49 | import com.facebook.react.module.annotations.ReactModule; 50 | import com.facebook.react.uimanager.SimpleViewManager; 51 | import com.facebook.react.uimanager.ThemedReactContext; 52 | import com.facebook.react.uimanager.UIManagerModule; 53 | import com.facebook.react.uimanager.annotations.ReactProp; 54 | import com.facebook.react.uimanager.events.ContentSizeChangeEvent; 55 | import com.facebook.react.uimanager.events.Event; 56 | import com.facebook.react.uimanager.events.EventDispatcher; 57 | import com.facebook.react.views.scroll.OnScrollDispatchHelper; 58 | import com.facebook.react.views.scroll.ScrollEvent; 59 | import com.facebook.react.views.scroll.ScrollEventType; 60 | import com.facebook.react.views.webview.WebViewConfig; 61 | import com.facebook.react.views.webview.events.TopLoadingErrorEvent; 62 | import com.facebook.react.views.webview.events.TopLoadingFinishEvent; 63 | import com.facebook.react.views.webview.events.TopLoadingStartEvent; 64 | import com.facebook.react.views.webview.events.TopMessageEvent; 65 | 66 | import org.json.JSONException; 67 | import org.json.JSONObject; 68 | 69 | import java.io.IOException; 70 | import java.io.InputStream; 71 | import java.io.UnsupportedEncodingException; 72 | import java.net.HttpURLConnection; 73 | import java.net.URLEncoder; 74 | import java.nio.charset.Charset; 75 | import java.util.HashMap; 76 | import java.util.LinkedList; 77 | import java.util.List; 78 | import java.util.Locale; 79 | import java.util.Map; 80 | import java.util.regex.Pattern; 81 | 82 | import okhttp3.MediaType; 83 | import okhttp3.OkHttpClient; 84 | import okhttp3.OkHttpClient.Builder; 85 | import okhttp3.Request; 86 | import okhttp3.Response; 87 | 88 | import static okhttp3.internal.Util.UTF_8; 89 | 90 | /** 91 | * Manages instances of {@link WebView} 92 | * 93 | * Can accept following commands: 94 | * - GO_BACK 95 | * - GO_FORWARD 96 | * - RELOAD 97 | * 98 | * {@link WebView} instances could emit following direct events: 99 | * - topLoadingFinish 100 | * - topLoadingStart 101 | * - topLoadingError 102 | * 103 | * Each event will carry the following properties: 104 | * - target - view's react tag 105 | * - url - url set for the webview 106 | * - loading - whether webview is in a loading state 107 | * - title - title of the current page 108 | * - canGoBack - boolean, whether there is anything on a history stack to go back 109 | * - canGoForward - boolean, whether it is possible to request GO_FORWARD command 110 | */ 111 | @ReactModule(name = Web3WebviewManager.REACT_CLASS) 112 | public class Web3WebviewManager extends SimpleViewManager { 113 | 114 | protected static final String REACT_CLASS = "Web3Webview"; 115 | 116 | public final static String HEADER_CONTENT_TYPE = "content-type"; 117 | private static final String MIME_TEXT_HTML = "text/html"; 118 | private static final String MIME_UNKNOWN = "application/octet-stream"; 119 | protected static final String HTML_ENCODING = "UTF-8"; 120 | protected static final String HTML_MIME_TYPE = "text/html"; 121 | 122 | protected static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE"; 123 | private OkHttpClient httpClient; 124 | 125 | protected static final String HTTP_METHOD_POST = "POST"; 126 | 127 | public static final int COMMAND_GO_BACK = 1; 128 | public static final int COMMAND_GO_FORWARD = 2; 129 | public static final int COMMAND_RELOAD = 3; 130 | public static final int COMMAND_STOP_LOADING = 4; 131 | public static final int COMMAND_POST_MESSAGE = 5; 132 | public static final int COMMAND_INJECT_JAVASCRIPT = 6; 133 | public static final int COMMAND_LOAD_URL = 7; 134 | 135 | // Use `webView.loadUrl("about:blank")` to reliably reset the view 136 | // state and release page resources (including any running JavaScript). 137 | protected static final String BLANK_URL = "about:blank"; 138 | 139 | protected WebViewConfig mWebViewConfig; 140 | protected WebSettings mWebviewSettings; 141 | private static ReactApplicationContext reactNativeContext; 142 | private static boolean debug; 143 | private Web3WebviewPackage pkg; 144 | protected @NonNull WebView.PictureListener mPictureListener; 145 | 146 | protected class Web3WebviewClient extends WebViewClient { 147 | 148 | protected boolean mLastLoadFailed = false; 149 | protected @NonNull ReadableArray mUrlPrefixesForDefaultIntent; 150 | protected @NonNull List mOriginWhitelist; 151 | 152 | 153 | @Override 154 | public void onPageFinished(WebView webView, String url) { 155 | super.onPageFinished(webView, url); 156 | 157 | if (!mLastLoadFailed) { 158 | Web3Webview Web3Webview = (Web3Webview) webView; 159 | Web3Webview.callInjectedJavaScript(); 160 | Web3Webview.setVerticalScrollBarEnabled(true); 161 | Web3Webview.setHorizontalScrollBarEnabled(true); 162 | webView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 163 | emitFinishEvent(webView, url); 164 | Web3Webview.linkBridge(); 165 | } 166 | } 167 | 168 | 169 | 170 | @Override 171 | public void onPageStarted(final WebView webView, String url, Bitmap favicon) { 172 | super.onPageStarted(webView, url, favicon); 173 | 174 | mLastLoadFailed = false; 175 | dispatchEvent( 176 | webView, 177 | new TopLoadingStartEvent( 178 | webView.getId(), 179 | createWebViewEvent(webView, url))); 180 | Web3Webview Web3Webview = (Web3Webview) webView; 181 | } 182 | 183 | 184 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 185 | @Override 186 | public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { 187 | if (request == null || view == null) { 188 | return false; 189 | } 190 | 191 | 192 | String url = request.getUrl().toString(); 193 | // Disabling the URL schemes that cause problems 194 | String[] blacklistedUrls = { "intent:#Intent;action=com.ledger.android.u2f.bridge.AUTHENTICATE" }; 195 | for(int i=0; i< blacklistedUrls.length; i++){ 196 | String badUrl = blacklistedUrls[i]; 197 | if(url.contains(badUrl)){ 198 | return true; 199 | } 200 | } 201 | 202 | // This works only for API 24+ 203 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 204 | if (request.isForMainFrame() && request.isRedirect()) { 205 | 206 | view.loadUrl(url); 207 | return true; 208 | } 209 | } 210 | 211 | return super.shouldOverrideUrlLoading(view, request); 212 | } 213 | 214 | 215 | 216 | private void launchIntent(Context context, String url) { 217 | try { 218 | Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); 219 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 220 | intent.addCategory(Intent.CATEGORY_BROWSABLE); 221 | context.startActivity(intent); 222 | } catch (ActivityNotFoundException e) { 223 | FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e); 224 | } 225 | } 226 | 227 | private boolean shouldHandleURL(List originWhitelist, String url) { 228 | Uri uri = Uri.parse(url); 229 | String scheme = uri.getScheme() != null ? uri.getScheme() : ""; 230 | String authority = uri.getAuthority() != null ? uri.getAuthority() : ""; 231 | String urlToCheck = scheme + "://" + authority; 232 | for (Pattern pattern : originWhitelist) { 233 | if (pattern.matcher(urlToCheck).matches()) { 234 | return true; 235 | } 236 | } 237 | return false; 238 | } 239 | 240 | @Override 241 | public void onReceivedError( 242 | WebView webView, 243 | int errorCode, 244 | String description, 245 | String failingUrl) { 246 | super.onReceivedError(webView, errorCode, description, failingUrl); 247 | mLastLoadFailed = true; 248 | 249 | emitFinishEvent(webView, failingUrl); 250 | 251 | WritableMap eventData = createWebViewEvent(webView, failingUrl); 252 | eventData.putDouble("code", errorCode); 253 | eventData.putString("description", description); 254 | 255 | dispatchEvent( 256 | webView, 257 | new TopLoadingErrorEvent(webView.getId(), eventData)); 258 | } 259 | 260 | @Override 261 | public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload) { 262 | super.doUpdateVisitedHistory(webView, url, isReload); 263 | dispatchEvent( 264 | webView, 265 | new TopLoadingStartEvent( 266 | webView.getId(), 267 | createWebViewEvent(webView, url))); 268 | } 269 | 270 | protected void emitFinishEvent(WebView webView, String url) { 271 | dispatchEvent( 272 | webView, 273 | new TopLoadingFinishEvent( 274 | webView.getId(), 275 | createWebViewEvent(webView, url))); 276 | } 277 | 278 | protected WritableMap createWebViewEvent(WebView webView, String url) { 279 | WritableMap event = Arguments.createMap(); 280 | event.putDouble("target", webView.getId()); 281 | event.putString("url", url); 282 | event.putBoolean("loading", !mLastLoadFailed && webView.getProgress() != 100); 283 | event.putString("title", webView.getTitle()); 284 | event.putBoolean("canGoBack", webView.canGoBack()); 285 | event.putBoolean("canGoForward", webView.canGoForward()); 286 | return event; 287 | } 288 | 289 | @Override 290 | public WebResourceResponse shouldInterceptRequest(WebView view, String url) { 291 | return null; 292 | } 293 | @Override 294 | public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { 295 | WebResourceResponse response = null; 296 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { 297 | response = Web3WebviewManager.this.shouldInterceptRequest(request, true, (Web3Webview) view); 298 | if (response != null) { 299 | return response; 300 | } 301 | } 302 | 303 | return super.shouldInterceptRequest(view, request); 304 | } 305 | 306 | 307 | public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) { 308 | mUrlPrefixesForDefaultIntent = specialUrls; 309 | } 310 | 311 | public void setOriginWhitelist(List originWhitelist) { 312 | mOriginWhitelist = originWhitelist; 313 | } 314 | } 315 | 316 | /** 317 | * Subclass of {@link WebView} that implements {@link LifecycleEventListener} interface in order 318 | * to call {@link WebView#destroy} on activity destroy event and also to clear the client 319 | */ 320 | protected static class Web3Webview extends WebView implements LifecycleEventListener { 321 | protected @NonNull String injectedJS; 322 | protected @NonNull String injectedOnStartLoadingJS; 323 | protected boolean messagingEnabled = false; 324 | protected @NonNull Web3WebviewClient mWeb3WebviewClient; 325 | private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper(); 326 | 327 | protected class Web3WebviewBridge { 328 | Web3Webview mContext; 329 | 330 | Web3WebviewBridge(Web3Webview c) { 331 | mContext = c; 332 | } 333 | 334 | @JavascriptInterface 335 | public void postMessage(String message) { 336 | mContext.onMessage(message); 337 | } 338 | } 339 | 340 | /** 341 | * WebView must be created with an context of the current activity 342 | * 343 | * Activity Context is required for creation of dialogs internally by WebView 344 | * Reactive Native needed for access to ReactNative internal system functionality 345 | * 346 | */ 347 | public Web3Webview(ThemedReactContext reactContext) { 348 | super(reactContext); 349 | } 350 | 351 | @Override 352 | public void onHostResume() { 353 | // do nothing 354 | } 355 | 356 | @Override 357 | public void onHostPause() { 358 | // do nothing 359 | } 360 | 361 | @Override 362 | public void onHostDestroy() { 363 | cleanupCallbacksAndDestroy(); 364 | } 365 | 366 | 367 | @Override 368 | public void setWebViewClient(WebViewClient client) { 369 | super.setWebViewClient(client); 370 | mWeb3WebviewClient = (Web3WebviewClient)client; 371 | } 372 | 373 | public @NonNull Web3WebviewClient getWeb3WebviewClient() { 374 | return mWeb3WebviewClient; 375 | } 376 | 377 | public void setInjectedJavaScript(@NonNull String js) { 378 | injectedJS = js; 379 | } 380 | 381 | public void setInjectedOnStartLoadingJavaScript(@NonNull String js) { 382 | injectedOnStartLoadingJS = js; 383 | } 384 | 385 | protected Web3WebviewBridge createWeb3WebviewBridge(Web3Webview webView) { 386 | return new Web3WebviewBridge(webView); 387 | } 388 | 389 | public void setMessagingEnabled(boolean enabled) { 390 | if (messagingEnabled == enabled) { 391 | return; 392 | } 393 | 394 | messagingEnabled = enabled; 395 | if (enabled) { 396 | addJavascriptInterface(createWeb3WebviewBridge(this), BRIDGE_NAME); 397 | } else { 398 | removeJavascriptInterface(BRIDGE_NAME); 399 | } 400 | } 401 | 402 | protected void evaluateJavascriptWithFallback(String script) { 403 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 404 | evaluateJavascript(script, null); 405 | return; 406 | } 407 | 408 | try { 409 | loadUrl("javascript:" + URLEncoder.encode(script, "UTF-8")); 410 | } catch (UnsupportedEncodingException e) { 411 | // UTF-8 should always be supported 412 | throw new RuntimeException(e); 413 | } 414 | } 415 | 416 | 417 | public void callInjectedJavaScript() { 418 | if (getSettings().getJavaScriptEnabled() && 419 | injectedJS != null && 420 | !TextUtils.isEmpty(injectedJS)) { 421 | evaluateJavascriptWithFallback("(function() {\n" + injectedJS + ";\n})();"); 422 | } 423 | } 424 | 425 | public void linkBridge() { 426 | if (messagingEnabled) { 427 | String script = "(" + 428 | "window.postMessageToNative = function(data) {"+ 429 | BRIDGE_NAME + ".postMessage(JSON.stringify(data));"+ 430 | "}"+ 431 | ")"; 432 | evaluateJavascriptWithFallback(script); 433 | 434 | } 435 | } 436 | 437 | public void unlinkBridge() { 438 | this.loadUrl("about:blank"); 439 | } 440 | 441 | public void onMessage(String message) { 442 | dispatchEvent(this, new TopMessageEvent(this.getId(), message)); 443 | } 444 | 445 | 446 | 447 | protected void onScrollChanged(int x, int y, int oldX, int oldY) { 448 | super.onScrollChanged(x, y, oldX, oldY); 449 | if (mOnScrollDispatchHelper.onScrollChanged(x, y)) { 450 | ScrollEvent event = ScrollEvent.obtain( 451 | this.getId(), 452 | ScrollEventType.SCROLL, 453 | x, 454 | y, 455 | mOnScrollDispatchHelper.getXFlingVelocity(), 456 | mOnScrollDispatchHelper.getYFlingVelocity(), 457 | this.computeHorizontalScrollRange(), 458 | this.computeVerticalScrollRange(), 459 | this.getWidth(), 460 | this.getHeight()); 461 | dispatchEvent(this, event); 462 | } 463 | } 464 | 465 | protected void cleanupCallbacksAndDestroy() { 466 | setWebViewClient(null); 467 | destroy(); 468 | } 469 | } 470 | 471 | public Web3WebviewManager(ReactApplicationContext context, com.web3webview.Web3WebviewPackage pkg) { 472 | this.reactNativeContext = context; 473 | this.pkg = pkg; 474 | Builder b = new Builder(); 475 | httpClient = b 476 | .followRedirects(false) 477 | .followSslRedirects(false) 478 | .build(); 479 | mWebViewConfig = new WebViewConfig() { 480 | public void configWebView(WebView webView) { 481 | } 482 | }; 483 | } 484 | 485 | @Override 486 | public String getName() { 487 | return REACT_CLASS; 488 | } 489 | 490 | public static Boolean urlStringLooksInvalid(String urlString) { 491 | return urlString == null || 492 | urlString.trim().equals("") || 493 | !(urlString.startsWith("http") && !urlString.startsWith("www")) || 494 | urlString.contains("|"); 495 | } 496 | 497 | public static Boolean responseRequiresJSInjection(Response response) { 498 | if (response.isRedirect()) { 499 | return false; 500 | } 501 | final String contentTypeAndCharset = response.header(HEADER_CONTENT_TYPE, MIME_UNKNOWN); 502 | return contentTypeAndCharset.startsWith(MIME_TEXT_HTML); 503 | } 504 | 505 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 506 | public WebResourceResponse shouldInterceptRequest(WebResourceRequest request, Boolean onlyMainFrame, Web3Webview webView) { 507 | Uri url = request.getUrl(); 508 | String urlStr = url.toString(); 509 | if (onlyMainFrame && !request.isForMainFrame()) { 510 | return null; 511 | } 512 | if (Web3WebviewManager.urlStringLooksInvalid(urlStr)) { 513 | return null; 514 | } 515 | try { 516 | String ua = mWebviewSettings.getUserAgentString(); 517 | 518 | Request req = new Request.Builder() 519 | .header("User-Agent", ua) 520 | .url(urlStr) 521 | .build(); 522 | Response response = httpClient.newCall(req).execute(); 523 | if (!Web3WebviewManager.responseRequiresJSInjection(response)) { 524 | return null; 525 | } 526 | InputStream is = response.body().byteStream(); 527 | MediaType contentType = response.body().contentType(); 528 | Charset charset = contentType != null ? contentType.charset(UTF_8) : UTF_8; 529 | if (response.code() < HttpURLConnection.HTTP_MULT_CHOICE || response.code() >= HttpURLConnection.HTTP_BAD_REQUEST) { 530 | is = new InputStreamWithInjectedJS(is, webView.injectedOnStartLoadingJS, charset, webView.getContext()); 531 | } 532 | return new WebResourceResponse("text/html", charset.name(), is); 533 | } catch (IOException e) { 534 | return null; 535 | } 536 | } 537 | 538 | 539 | 540 | protected Web3Webview createWeb3WebviewInstance(ThemedReactContext reactContext) { 541 | return new Web3Webview(reactContext); 542 | } 543 | 544 | @Override 545 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 546 | protected WebView createViewInstance(final ThemedReactContext reactContext) { 547 | final Web3Webview webView = createWeb3WebviewInstance(reactContext); 548 | 549 | 550 | webView.setWebChromeClient(new WebChromeClient() { 551 | @Override 552 | public boolean onConsoleMessage(ConsoleMessage message) { 553 | if (ReactBuildConfig.DEBUG) { 554 | return super.onConsoleMessage(message); 555 | } 556 | // Ignore console logs in non debug builds. 557 | return true; 558 | } 559 | public void onProgressChanged(WebView view, int progress) { 560 | dispatchEvent(view, new ProgressEvent(view.getId(), progress)); 561 | 562 | if(webView.getProgress() >= 10){ 563 | webView.linkBridge(); 564 | } 565 | } 566 | 567 | @Override 568 | public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { 569 | callback.invoke(origin, true, false); 570 | } 571 | 572 | protected void openFileChooser(ValueCallback filePathCallback, String acceptType) { 573 | getModule(reactContext).startPhotoPickerIntent(filePathCallback, acceptType); 574 | } 575 | 576 | protected void openFileChooser(ValueCallback filePathCallback) { 577 | getModule(reactContext).startPhotoPickerIntent(filePathCallback, ""); 578 | } 579 | 580 | protected void openFileChooser(ValueCallback filePathCallback, String acceptType, String capture) { 581 | getModule(reactContext).startPhotoPickerIntent(filePathCallback, acceptType); 582 | } 583 | 584 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 585 | @Override 586 | public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { 587 | String[] acceptTypes = fileChooserParams.getAcceptTypes(); 588 | boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE; 589 | Intent intent = fileChooserParams.createIntent(); 590 | return getModule(reactContext).startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple); 591 | } 592 | 593 | 594 | }); 595 | reactContext.addLifecycleEventListener(webView); 596 | mWebViewConfig.configWebView(webView); 597 | WebSettings settings = webView.getSettings(); 598 | settings.setBuiltInZoomControls(true); 599 | settings.setDisplayZoomControls(false); 600 | settings.setDomStorageEnabled(true); 601 | settings.setAllowFileAccess(true); 602 | settings.setAppCacheEnabled (true); 603 | settings.setLoadWithOverviewMode(true); 604 | settings.setAllowContentAccess(true); 605 | settings.setLoadsImagesAutomatically(true); 606 | settings.setBlockNetworkImage(false); 607 | settings.setBlockNetworkLoads(false); 608 | 609 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 610 | settings.setAllowFileAccessFromFileURLs(false); 611 | setAllowUniversalAccessFromFileURLs(webView, false); 612 | } 613 | setMixedContentMode(webView, "never"); 614 | 615 | // Fixes broken full-screen modals/galleries due to body height being 0. 616 | webView.setLayoutParams( 617 | new LayoutParams(LayoutParams.MATCH_PARENT, 618 | LayoutParams.MATCH_PARENT)); 619 | 620 | setGeolocationEnabled(webView, false); 621 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 622 | WebView.setWebContentsDebuggingEnabled(true); 623 | } 624 | 625 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 626 | ServiceWorkerController swController = ServiceWorkerController.getInstance(); 627 | swController.setServiceWorkerClient(new ServiceWorkerClient() { 628 | @Override 629 | public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { 630 | WebResourceResponse response = Web3WebviewManager.this.shouldInterceptRequest(request, false, webView); 631 | if (response != null) { 632 | return response; 633 | } 634 | return super.shouldInterceptRequest(request); 635 | } 636 | }); 637 | } 638 | 639 | mWebviewSettings = settings; 640 | 641 | return webView; 642 | } 643 | 644 | @ReactProp(name = "javaScriptEnabled") 645 | public void setJavaScriptEnabled(WebView view, boolean enabled) { 646 | view.getSettings().setJavaScriptEnabled(enabled); 647 | } 648 | 649 | @ReactProp(name = "thirdPartyCookiesEnabled") 650 | public void setThirdPartyCookiesEnabled(WebView view, boolean enabled) { 651 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 652 | CookieManager.getInstance().setAcceptThirdPartyCookies(view, enabled); 653 | } 654 | } 655 | 656 | @ReactProp(name = "scalesPageToFit") 657 | public void setScalesPageToFit(WebView view, boolean enabled) { 658 | view.getSettings().setUseWideViewPort(!enabled); 659 | } 660 | 661 | @ReactProp(name = "domStorageEnabled") 662 | public void setDomStorageEnabled(WebView view, boolean enabled) { 663 | view.getSettings().setDomStorageEnabled(enabled); 664 | } 665 | 666 | @ReactProp(name = "userAgent") 667 | public void setUserAgent(WebView view, @NonNull String userAgent) { 668 | if (userAgent != null) { 669 | view.getSettings().setUserAgentString(userAgent); 670 | } 671 | } 672 | 673 | @ReactProp(name = "mediaPlaybackRequiresUserAction") 674 | public void setMediaPlaybackRequiresUserAction(WebView view, boolean requires) { 675 | if(Build.VERSION.SDK_INT >= 17) { 676 | view.getSettings().setMediaPlaybackRequiresUserGesture(requires); 677 | } 678 | } 679 | 680 | @ReactProp(name = "allowUniversalAccessFromFileURLs") 681 | public void setAllowUniversalAccessFromFileURLs(WebView view, boolean allow) { 682 | view.getSettings().setAllowUniversalAccessFromFileURLs(allow); 683 | } 684 | 685 | @ReactProp(name = "saveFormDataDisabled") 686 | public void setSaveFormDataDisabled(WebView view, boolean disable) { 687 | view.getSettings().setSaveFormData(!disable); 688 | } 689 | 690 | @ReactProp(name = "injectedJavaScript") 691 | public void setInjectedJavaScript(WebView view, @NonNull String injectedJavaScript) { 692 | ((Web3Webview) view).setInjectedJavaScript(injectedJavaScript); 693 | } 694 | 695 | @ReactProp(name = "injectedOnStartLoadingJavaScript") 696 | public void setInjectedOnStartLoadingJavaScript(WebView view, @NonNull String injectedJavaScript) { 697 | ((Web3Webview) view).setInjectedOnStartLoadingJavaScript(injectedJavaScript); 698 | } 699 | 700 | @ReactProp(name = "messagingEnabled") 701 | public void setMessagingEnabled(WebView view, boolean enabled) { 702 | ((Web3Webview) view).setMessagingEnabled(enabled); 703 | } 704 | 705 | @ReactProp(name = "source") 706 | public void setSource(WebView view, @NonNull ReadableMap source) { 707 | if (source != null) { 708 | if (source.hasKey("html")) { 709 | String html = source.getString("html"); 710 | if (source.hasKey("baseUrl")) { 711 | view.loadDataWithBaseURL( 712 | source.getString("baseUrl"), html, HTML_MIME_TYPE, HTML_ENCODING, null); 713 | } else { 714 | view.loadData(html, HTML_MIME_TYPE, HTML_ENCODING); 715 | } 716 | return; 717 | } 718 | if (source.hasKey("uri")) { 719 | String url = source.getString("uri"); 720 | String previousUrl = view.getUrl(); 721 | if (source.hasKey("method")) { 722 | String method = source.getString("method"); 723 | if (method.equals(HTTP_METHOD_POST)) { 724 | byte[] postData = null; 725 | if (source.hasKey("body")) { 726 | String body = source.getString("body"); 727 | try { 728 | postData = body.getBytes("UTF-8"); 729 | } catch (UnsupportedEncodingException e) { 730 | postData = body.getBytes(); 731 | } 732 | } 733 | if (postData == null) { 734 | postData = new byte[0]; 735 | } 736 | view.postUrl(url, postData); 737 | return; 738 | } 739 | } 740 | HashMap headerMap = new HashMap<>(); 741 | if (source.hasKey("headers")) { 742 | ReadableMap headers = source.getMap("headers"); 743 | ReadableMapKeySetIterator iter = headers.keySetIterator(); 744 | while (iter.hasNextKey()) { 745 | String key = iter.nextKey(); 746 | if ("user-agent".equals(key.toLowerCase(Locale.ENGLISH))) { 747 | if (view.getSettings() != null) { 748 | view.getSettings().setUserAgentString(headers.getString(key)); 749 | } 750 | } else { 751 | headerMap.put(key, headers.getString(key)); 752 | } 753 | } 754 | } 755 | view.loadUrl(url, headerMap); 756 | return; 757 | } 758 | } 759 | view.loadUrl(BLANK_URL); 760 | } 761 | 762 | @ReactProp(name = "onContentSizeChange") 763 | public void setOnContentSizeChange(WebView view, boolean sendContentSizeChangeEvents) { 764 | if (sendContentSizeChangeEvents) { 765 | view.setPictureListener(getPictureListener()); 766 | } else { 767 | view.setPictureListener(null); 768 | } 769 | } 770 | 771 | @ReactProp(name = "mixedContentMode") 772 | public void setMixedContentMode(WebView view, @NonNull String mixedContentMode) { 773 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 774 | if (mixedContentMode == null || "never".equals(mixedContentMode)) { 775 | view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); 776 | } else if ("always".equals(mixedContentMode)) { 777 | view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); 778 | } else if ("compatibility".equals(mixedContentMode)) { 779 | view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); 780 | } 781 | } 782 | } 783 | 784 | @ReactProp(name = "urlPrefixesForDefaultIntent") 785 | public void setUrlPrefixesForDefaultIntent( 786 | WebView view, 787 | @NonNull ReadableArray urlPrefixesForDefaultIntent) { 788 | Web3WebviewClient client = ((Web3Webview) view).getWeb3WebviewClient(); 789 | if (client != null && urlPrefixesForDefaultIntent != null) { 790 | client.setUrlPrefixesForDefaultIntent(urlPrefixesForDefaultIntent); 791 | } 792 | } 793 | 794 | @ReactProp(name = "geolocationEnabled") 795 | public void setGeolocationEnabled( 796 | WebView view, 797 | @NonNull Boolean isGeolocationEnabled) { 798 | view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled); 799 | } 800 | 801 | @ReactProp(name = "originWhitelist") 802 | public void setOriginWhitelist( 803 | WebView view, 804 | @NonNull ReadableArray originWhitelist) { 805 | Web3WebviewClient client = ((Web3Webview) view).getWeb3WebviewClient(); 806 | if (client != null && originWhitelist != null) { 807 | List whiteList = new LinkedList<>(); 808 | for (int i = 0 ; i < originWhitelist.size() ; i++) { 809 | whiteList.add(Pattern.compile(originWhitelist.getString(i))); 810 | } 811 | client.setOriginWhitelist(whiteList); 812 | } 813 | } 814 | 815 | @Override 816 | protected void addEventEmitters(ThemedReactContext reactContext, WebView view) { 817 | view.setWebViewClient(new Web3WebviewClient()); 818 | } 819 | 820 | @Override 821 | public @NonNull Map getCommandsMap() { 822 | return MapBuilder.of( 823 | "goBack", COMMAND_GO_BACK, 824 | "goForward", COMMAND_GO_FORWARD, 825 | "reload", COMMAND_RELOAD, 826 | "stopLoading", COMMAND_STOP_LOADING, 827 | "postMessage", COMMAND_POST_MESSAGE, 828 | "injectJavaScript", COMMAND_INJECT_JAVASCRIPT 829 | ); 830 | } 831 | 832 | @Override 833 | public void receiveCommand(WebView root, int commandId, @NonNull ReadableArray args) { 834 | switch (commandId) { 835 | case COMMAND_GO_BACK: 836 | root.goBack(); 837 | break; 838 | case COMMAND_GO_FORWARD: 839 | root.goForward(); 840 | break; 841 | case COMMAND_RELOAD: 842 | root.reload(); 843 | break; 844 | case COMMAND_STOP_LOADING: 845 | root.stopLoading(); 846 | break; 847 | case COMMAND_POST_MESSAGE: 848 | try { 849 | Web3Webview webView = (Web3Webview) root; 850 | JSONObject eventInitDict = new JSONObject(); 851 | eventInitDict.put("data", args.getString(0)); 852 | webView.evaluateJavascriptWithFallback("(function () {" + 853 | "var event;" + 854 | "var data = " + eventInitDict.toString() + ";" + 855 | "try {" + 856 | "event = new MessageEvent('message', data);" + 857 | "} catch (e) {" + 858 | "event = document.createEvent('MessageEvent');" + 859 | "event.initMessageEvent('message', true, true, data.data, data.origin, data.lastEventId, data.source);" + 860 | "}" + 861 | "document.dispatchEvent(event);" + 862 | "})();"); 863 | } catch (JSONException e) { 864 | throw new RuntimeException(e); 865 | } 866 | break; 867 | case COMMAND_INJECT_JAVASCRIPT: 868 | Web3Webview webView = (Web3Webview) root; 869 | webView.evaluateJavascriptWithFallback(args.getString(0)); 870 | break; 871 | } 872 | } 873 | 874 | @Override 875 | public void onDropViewInstance(WebView webView) { 876 | super.onDropViewInstance(webView); 877 | Web3Webview w = (Web3Webview) webView; 878 | ((ThemedReactContext) webView.getContext()).removeLifecycleEventListener(w); 879 | w.cleanupCallbacksAndDestroy(); 880 | } 881 | 882 | public static Web3WebviewModule getModule(ReactContext reactContext) { 883 | return reactContext.getNativeModule(Web3WebviewModule.class); 884 | } 885 | 886 | protected WebView.PictureListener getPictureListener() { 887 | if (mPictureListener == null) { 888 | mPictureListener = new WebView.PictureListener() { 889 | @Override 890 | public void onNewPicture(WebView webView, Picture picture) { 891 | dispatchEvent( 892 | webView, 893 | new ContentSizeChangeEvent( 894 | webView.getId(), 895 | webView.getWidth(), 896 | webView.getContentHeight())); 897 | } 898 | }; 899 | } 900 | return mPictureListener; 901 | } 902 | 903 | protected static void dispatchEvent(WebView webView, Event event) { 904 | ReactContext reactContext = (ReactContext) webView.getContext(); 905 | EventDispatcher eventDispatcher = 906 | reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); 907 | eventDispatcher.dispatchEvent(event); 908 | } 909 | 910 | @Override 911 | public @NonNull Map getExportedCustomBubblingEventTypeConstants() { 912 | return MapBuilder.builder() 913 | .put("progress", 914 | MapBuilder.of( 915 | "phasedRegistrationNames", 916 | MapBuilder.of("bubbled", "onProgress"))) 917 | .build(); 918 | } 919 | 920 | 921 | } 922 | -------------------------------------------------------------------------------- /android/src/main/java/com/web3webview/Web3WebviewModule.java: -------------------------------------------------------------------------------- 1 | package com.web3webview; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.app.DownloadManager; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.pm.PackageManager; 9 | import android.net.Uri; 10 | import android.os.Build; 11 | import android.os.Environment; 12 | import android.os.Parcelable; 13 | import android.provider.MediaStore; 14 | import androidx.annotation.RequiresApi; 15 | import androidx.core.content.ContextCompat; 16 | import androidx.core.content.FileProvider; 17 | import android.util.Log; 18 | import android.webkit.MimeTypeMap; 19 | import android.webkit.ValueCallback; 20 | import android.webkit.WebChromeClient; 21 | import android.widget.Toast; 22 | 23 | import com.facebook.react.bridge.ActivityEventListener; 24 | import com.facebook.react.bridge.Promise; 25 | import com.facebook.react.bridge.ReactApplicationContext; 26 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 27 | import com.facebook.react.bridge.ReactMethod; 28 | import com.facebook.react.module.annotations.ReactModule; 29 | import com.facebook.react.modules.core.PermissionAwareActivity; 30 | import com.facebook.react.modules.core.PermissionListener; 31 | 32 | import java.io.File; 33 | import java.io.IOException; 34 | import java.util.ArrayList; 35 | 36 | import static android.app.Activity.RESULT_OK; 37 | 38 | @ReactModule(name = Web3WebviewModule.MODULE_NAME) 39 | public class Web3WebviewModule extends ReactContextBaseJavaModule implements ActivityEventListener { 40 | public static final String MODULE_NAME = "RNCWebView"; 41 | private static final int PICKER = 1; 42 | private static final int PICKER_LEGACY = 3; 43 | private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1; 44 | final String DEFAULT_MIME_TYPES = "*/*"; 45 | private ValueCallback filePathCallbackLegacy; 46 | private ValueCallback filePathCallback; 47 | private Uri outputFileUri; 48 | private DownloadManager.Request downloadRequest; 49 | private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() { 50 | @Override 51 | public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 52 | switch (requestCode) { 53 | case FILE_DOWNLOAD_PERMISSION_REQUEST: { 54 | // If request is cancelled, the result arrays are empty. 55 | if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 56 | if (downloadRequest != null) { 57 | downloadFile(); 58 | } 59 | } else { 60 | Toast.makeText(getCurrentActivity().getApplicationContext(), "Cannot download files as permission was denied. Please provide permission to write to storage, in order to download files.", Toast.LENGTH_LONG).show(); 61 | } 62 | return true; 63 | } 64 | } 65 | return false; 66 | } 67 | }; 68 | 69 | public Web3WebviewModule(ReactApplicationContext reactContext) { 70 | super(reactContext); 71 | reactContext.addActivityEventListener(this); 72 | } 73 | 74 | @Override 75 | public String getName() { 76 | return MODULE_NAME; 77 | } 78 | 79 | @ReactMethod 80 | public void isFileUploadSupported(final Promise promise) { 81 | Boolean result = false; 82 | int current = Build.VERSION.SDK_INT; 83 | if (current >= Build.VERSION_CODES.LOLLIPOP) { 84 | result = true; 85 | } 86 | if (current >= Build.VERSION_CODES.JELLY_BEAN && current <= Build.VERSION_CODES.JELLY_BEAN_MR2) { 87 | result = true; 88 | } 89 | promise.resolve(result); 90 | } 91 | 92 | public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { 93 | 94 | if (filePathCallback == null && filePathCallbackLegacy == null) { 95 | return; 96 | } 97 | 98 | // based off of which button was pressed, we get an activity result and a file 99 | // the camera activity doesn't properly return the filename* (I think?) so we use 100 | // this filename instead 101 | switch (requestCode) { 102 | case PICKER: 103 | if (resultCode != RESULT_OK) { 104 | if (filePathCallback != null) { 105 | filePathCallback.onReceiveValue(null); 106 | } 107 | } else { 108 | Uri result[] = this.getSelectedFiles(data, resultCode); 109 | if (result != null) { 110 | filePathCallback.onReceiveValue(result); 111 | } else { 112 | filePathCallback.onReceiveValue(new Uri[]{outputFileUri}); 113 | } 114 | } 115 | break; 116 | case PICKER_LEGACY: 117 | Uri result = resultCode != Activity.RESULT_OK ? null : data == null ? outputFileUri : data.getData(); 118 | filePathCallbackLegacy.onReceiveValue(result); 119 | break; 120 | 121 | } 122 | filePathCallback = null; 123 | filePathCallbackLegacy = null; 124 | outputFileUri = null; 125 | } 126 | 127 | public void onNewIntent(Intent intent) { 128 | } 129 | 130 | private Uri[] getSelectedFiles(Intent data, int resultCode) { 131 | if (data == null) { 132 | return null; 133 | } 134 | 135 | // we have one file selected 136 | if (data.getData() != null) { 137 | if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 138 | return WebChromeClient.FileChooserParams.parseResult(resultCode, data); 139 | } else { 140 | return null; 141 | } 142 | } 143 | 144 | // we have multiple files selected 145 | if (data.getClipData() != null) { 146 | final int numSelectedFiles = data.getClipData().getItemCount(); 147 | Uri[] result = new Uri[numSelectedFiles]; 148 | for (int i = 0; i < numSelectedFiles; i++) { 149 | result[i] = data.getClipData().getItemAt(i).getUri(); 150 | } 151 | return result; 152 | } 153 | return null; 154 | } 155 | 156 | public void startPhotoPickerIntent(ValueCallback filePathCallback, String acceptType) { 157 | filePathCallbackLegacy = filePathCallback; 158 | 159 | Intent fileChooserIntent = getFileChooserIntent(acceptType); 160 | Intent chooserIntent = Intent.createChooser(fileChooserIntent, ""); 161 | 162 | ArrayList extraIntents = new ArrayList<>(); 163 | if (acceptsImages(acceptType)) { 164 | extraIntents.add(getPhotoIntent()); 165 | } 166 | if (acceptsVideo(acceptType)) { 167 | extraIntents.add(getVideoIntent()); 168 | } 169 | chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{})); 170 | 171 | if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) { 172 | getCurrentActivity().startActivityForResult(chooserIntent, PICKER_LEGACY); 173 | } else { 174 | Log.w("Web3WevbiewModule", "there is no Activity to handle this Intent"); 175 | } 176 | } 177 | 178 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 179 | public boolean startPhotoPickerIntent(final ValueCallback callback, final Intent intent, final String[] acceptTypes, final boolean allowMultiple) { 180 | filePathCallback = callback; 181 | 182 | ArrayList extraIntents = new ArrayList<>(); 183 | if (acceptsImages(acceptTypes)) { 184 | extraIntents.add(getPhotoIntent()); 185 | } 186 | if (acceptsVideo(acceptTypes)) { 187 | extraIntents.add(getVideoIntent()); 188 | } 189 | 190 | Intent fileSelectionIntent = getFileChooserIntent(acceptTypes, allowMultiple); 191 | 192 | Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); 193 | chooserIntent.putExtra(Intent.EXTRA_INTENT, fileSelectionIntent); 194 | chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{})); 195 | 196 | if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) { 197 | getCurrentActivity().startActivityForResult(chooserIntent, PICKER); 198 | } else { 199 | Log.w("Web3WevbiewModule", "there is no Activity to handle this Intent"); 200 | } 201 | 202 | return true; 203 | } 204 | 205 | public void setDownloadRequest(DownloadManager.Request request) { 206 | this.downloadRequest = request; 207 | } 208 | 209 | public void downloadFile() { 210 | DownloadManager dm = (DownloadManager) getCurrentActivity().getBaseContext().getSystemService(Context.DOWNLOAD_SERVICE); 211 | String downloadMessage = "Downloading"; 212 | 213 | dm.enqueue(this.downloadRequest); 214 | 215 | Toast.makeText(getCurrentActivity().getApplicationContext(), downloadMessage, Toast.LENGTH_LONG).show(); 216 | } 217 | 218 | public boolean grantFileDownloaderPermissions() { 219 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 220 | return true; 221 | } 222 | 223 | boolean result = true; 224 | if (ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 225 | result = false; 226 | } 227 | 228 | if (!result) { 229 | PermissionAwareActivity activity = getPermissionAwareActivity(); 230 | activity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, FILE_DOWNLOAD_PERMISSION_REQUEST, webviewFileDownloaderPermissionListener); 231 | } 232 | 233 | return result; 234 | } 235 | 236 | private Intent getPhotoIntent() { 237 | Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 238 | outputFileUri = getOutputUri(MediaStore.ACTION_IMAGE_CAPTURE); 239 | intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri); 240 | return intent; 241 | } 242 | 243 | private Intent getVideoIntent() { 244 | Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 245 | // @todo from experience, for Videos we get the data onActivityResult 246 | // so there's no need to store the Uri 247 | Uri outputVideoUri = getOutputUri(MediaStore.ACTION_VIDEO_CAPTURE); 248 | intent.putExtra(MediaStore.EXTRA_OUTPUT, outputVideoUri); 249 | return intent; 250 | } 251 | 252 | private Intent getFileChooserIntent(String acceptTypes) { 253 | String _acceptTypes = acceptTypes; 254 | if (acceptTypes.isEmpty()) { 255 | _acceptTypes = DEFAULT_MIME_TYPES; 256 | } 257 | if (acceptTypes.matches("\\.\\w+")) { 258 | _acceptTypes = getMimeTypeFromExtension(acceptTypes.replace(".", "")); 259 | } 260 | Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 261 | intent.addCategory(Intent.CATEGORY_OPENABLE); 262 | intent.setType(_acceptTypes); 263 | return intent; 264 | } 265 | 266 | private Intent getFileChooserIntent(String[] acceptTypes, boolean allowMultiple) { 267 | Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 268 | intent.addCategory(Intent.CATEGORY_OPENABLE); 269 | intent.setType("*/*"); 270 | intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes)); 271 | intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); 272 | return intent; 273 | } 274 | 275 | private Boolean acceptsImages(String types) { 276 | String mimeType = types; 277 | if (types.matches("\\.\\w+")) { 278 | mimeType = getMimeTypeFromExtension(types.replace(".", "")); 279 | } 280 | return mimeType.isEmpty() || mimeType.toLowerCase().contains("image"); 281 | } 282 | 283 | private Boolean acceptsImages(String[] types) { 284 | String[] mimeTypes = getAcceptedMimeType(types); 285 | return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "image"); 286 | } 287 | 288 | private Boolean acceptsVideo(String types) { 289 | String mimeType = types; 290 | if (types.matches("\\.\\w+")) { 291 | mimeType = getMimeTypeFromExtension(types.replace(".", "")); 292 | } 293 | return mimeType.isEmpty() || mimeType.toLowerCase().contains("video"); 294 | } 295 | 296 | private Boolean acceptsVideo(String[] types) { 297 | String[] mimeTypes = getAcceptedMimeType(types); 298 | return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "video"); 299 | } 300 | 301 | private Boolean arrayContainsString(String[] array, String pattern) { 302 | for (String content : array) { 303 | if (content.contains(pattern)) { 304 | return true; 305 | } 306 | } 307 | return false; 308 | } 309 | 310 | private String[] getAcceptedMimeType(String[] types) { 311 | if (isArrayEmpty(types)) { 312 | return new String[]{DEFAULT_MIME_TYPES}; 313 | } 314 | String[] mimeTypes = new String[types.length]; 315 | for (int i = 0; i < types.length; i++) { 316 | String t = types[i]; 317 | // convert file extensions to mime types 318 | if (t.matches("\\.\\w+")) { 319 | String mimeType = getMimeTypeFromExtension(t.replace(".", "")); 320 | mimeTypes[i] = mimeType; 321 | } else { 322 | mimeTypes[i] = t; 323 | } 324 | } 325 | return mimeTypes; 326 | } 327 | 328 | private String getMimeTypeFromExtension(String extension) { 329 | String type = null; 330 | if (extension != null) { 331 | type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 332 | } 333 | return type; 334 | } 335 | 336 | private Uri getOutputUri(String intentType) { 337 | File capturedFile = null; 338 | try { 339 | capturedFile = getCapturedFile(intentType); 340 | } catch (IOException e) { 341 | Log.e("CREATE FILE", "Error occurred while creating the File", e); 342 | e.printStackTrace(); 343 | } 344 | 345 | // for versions below 6.0 (23) we use the old File creation & permissions model 346 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 347 | return Uri.fromFile(capturedFile); 348 | } 349 | 350 | // for versions 6.0+ (23) we use the FileProvider to avoid runtime permissions 351 | String packageName = getReactApplicationContext().getPackageName(); 352 | return FileProvider.getUriForFile(getReactApplicationContext(), packageName + ".fileprovider", capturedFile); 353 | } 354 | 355 | private File getCapturedFile(String intentType) throws IOException { 356 | String prefix = ""; 357 | String suffix = ""; 358 | String dir = ""; 359 | String filename = ""; 360 | 361 | if (intentType.equals(MediaStore.ACTION_IMAGE_CAPTURE)) { 362 | prefix = "image-"; 363 | suffix = ".jpg"; 364 | dir = Environment.DIRECTORY_PICTURES; 365 | } else if (intentType.equals(MediaStore.ACTION_VIDEO_CAPTURE)) { 366 | prefix = "video-"; 367 | suffix = ".mp4"; 368 | dir = Environment.DIRECTORY_MOVIES; 369 | } 370 | 371 | filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix; 372 | 373 | // for versions below 6.0 (23) we use the old File creation & permissions model 374 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 375 | // only this Directory works on all tested Android versions 376 | // ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21) 377 | File storageDir = Environment.getExternalStoragePublicDirectory(dir); 378 | return new File(storageDir, filename); 379 | } 380 | 381 | File storageDir = getReactApplicationContext().getExternalFilesDir(null); 382 | return File.createTempFile(filename, suffix, storageDir); 383 | } 384 | 385 | private Boolean isArrayEmpty(String[] arr) { 386 | // when our array returned from getAcceptTypes() has no values set from the webview 387 | // i.e. , without any "accept" attr 388 | // will be an array with one empty string element, afaik 389 | return arr.length == 0 || (arr.length == 1 && arr[0].length() == 0); 390 | } 391 | 392 | private PermissionAwareActivity getPermissionAwareActivity() { 393 | Activity activity = getCurrentActivity(); 394 | if (activity == null) { 395 | throw new IllegalStateException("Tried to use permissions API while not attached to an Activity."); 396 | } else if (!(activity instanceof PermissionAwareActivity)) { 397 | throw new IllegalStateException("Tried to use permissions API but the host Activity doesn't implement PermissionAwareActivity."); 398 | } 399 | return (PermissionAwareActivity) activity; 400 | } 401 | } -------------------------------------------------------------------------------- /android/src/main/java/com/web3webview/Web3WebviewPackage.java: -------------------------------------------------------------------------------- 1 | package com.web3webview; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.NativeModule; 5 | import com.facebook.react.bridge.ReactApplicationContext; 6 | import com.facebook.react.uimanager.ViewManager; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | public class Web3WebviewPackage implements ReactPackage { 14 | 15 | private Web3WebviewModule module; 16 | 17 | @Override 18 | public List createViewManagers( 19 | ReactApplicationContext reactContext) { 20 | return Collections.singletonList( 21 | new Web3WebviewManager(reactContext,this) 22 | ); 23 | } 24 | 25 | @Override 26 | public List createNativeModules(ReactApplicationContext reactContext) { 27 | List modulesList = new ArrayList<>(); 28 | module = new Web3WebviewModule(reactContext); 29 | modulesList.add(module); 30 | return modulesList; 31 | } 32 | 33 | public Web3WebviewModule getModule() { 34 | return module; 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /android/src/main/res/xml/file_provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Web3Webview from './Web3Webview'; 4 | 5 | export default Web3Webview; 6 | -------------------------------------------------------------------------------- /ios/RNWeb3Webview.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import 4 | 5 | @class RNWeb3Webview; 6 | 7 | /** 8 | * Special scheme used to pass messages to the injectedJavaScript 9 | * code without triggering a page load. Usage: 10 | * 11 | * window.location.href = RCTJSNavigationScheme + '://hello' 12 | */ 13 | extern NSString *const RCTJSNavigationScheme; 14 | 15 | @protocol RNWeb3WebviewDelegate 16 | 17 | - (BOOL)webView:(RNWeb3Webview *)webView 18 | shouldStartLoadForRequest:(NSMutableDictionary *)request 19 | withCallback:(RCTDirectEventBlock)callback; 20 | 21 | @end 22 | 23 | @interface RNWeb3Webview : RCTView 24 | 25 | - (instancetype)initWithProcessPool:(WKProcessPool *)processPool; 26 | 27 | @property (nonatomic, weak) id delegate; 28 | 29 | @property (nonatomic, copy) NSDictionary *source; 30 | @property (nonatomic, assign) UIEdgeInsets contentInset; 31 | @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; 32 | @property (nonatomic, assign) BOOL messagingEnabled; 33 | @property (nonatomic, assign) BOOL allowsLinkPreview; 34 | @property (nonatomic, assign) BOOL openNewWindowInWebView; 35 | @property (nonatomic, assign) BOOL injectJavaScriptForMainFrameOnly; 36 | @property (nonatomic, assign) BOOL injectedJavaScriptForMainFrameOnly; 37 | @property (nonatomic, copy) NSString *injectJavaScript; 38 | @property (nonatomic, copy) NSString *injectedJavaScript; 39 | @property (nonatomic, assign) BOOL hideKeyboardAccessoryView; 40 | @property (nonatomic, assign) BOOL keyboardDisplayRequiresUserAction; 41 | 42 | 43 | - (void)goForward; 44 | - (void)goBack; 45 | - (BOOL)canGoBack; 46 | - (BOOL)canGoForward; 47 | - (void)reload; 48 | - (void)stopLoading; 49 | - (void)postMessage:(NSString *)message; 50 | - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *error))completionHandler; 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /ios/RNWeb3Webview.m: -------------------------------------------------------------------------------- 1 | #import "RNWeb3Webview.h" 2 | 3 | #import "WeakScriptMessageDelegate.h" 4 | 5 | #import 6 | 7 | #import 8 | #import 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | 15 | #import 16 | 17 | // runtime trick to remove WKWebView keyboard default toolbar 18 | // see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279 19 | @interface _SwizzleHelperWK : NSObject @end 20 | @implementation _SwizzleHelperWK 21 | -(id)inputAccessoryView 22 | { 23 | return nil; 24 | } 25 | @end 26 | 27 | @interface RNWeb3Webview () 28 | 29 | @property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; 30 | @property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; 31 | @property (nonatomic, copy) RCTDirectEventBlock onLoadingError; 32 | @property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest; 33 | @property (nonatomic, copy) RCTDirectEventBlock onProgress; 34 | @property (nonatomic, copy) RCTDirectEventBlock onMessage; 35 | // NOTE: currently these event props are only declared so we can export the 36 | // event names to JS - we don't call the blocks directly because scroll events 37 | // need to be coalesced before sending, for performance reasons. 38 | @property (nonatomic, copy) RCTDirectEventBlock onScroll; 39 | @property (nonatomic, copy) RCTDirectEventBlock onScrollToTop; 40 | @property (nonatomic, copy) RCTDirectEventBlock onScrollBeginDrag; 41 | @property (nonatomic, copy) RCTDirectEventBlock onScrollEndDrag; 42 | @property (nonatomic, copy) RCTDirectEventBlock onMomentumScrollBegin; 43 | @property (nonatomic, copy) RCTDirectEventBlock onMomentumScrollEnd; 44 | 45 | @property (assign) BOOL sendCookies; 46 | @property (nonatomic, strong) WKUserScript *atStartScript; 47 | @property (nonatomic, strong) WKUserScript *atEndScript; 48 | @property (nonatomic, assign) NSTimeInterval scrollEventThrottle; 49 | 50 | @end 51 | 52 | @implementation RNWeb3Webview 53 | { 54 | WKWebView *_webView; 55 | BOOL _injectJavaScriptForMainFrameOnly; 56 | BOOL _injectedJavaScriptForMainFrameOnly; 57 | NSString *_injectJavaScript; 58 | NSString *_injectedJavaScript; 59 | BOOL _allowNextScrollNoMatterWhat; 60 | NSTimeInterval _lastScrollDispatchTime; 61 | int _BOTTOMBAR_HEIGHT; 62 | } 63 | 64 | - (instancetype)initWithFrame:(CGRect)frame 65 | { 66 | return self = [super initWithFrame:frame]; 67 | } 68 | 69 | RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) 70 | 71 | - (instancetype)initWithProcessPool:(WKProcessPool *)processPool 72 | { 73 | if(self = [self initWithFrame:CGRectZero]) 74 | { 75 | super.backgroundColor = [UIColor clearColor]; 76 | _automaticallyAdjustContentInsets = YES; 77 | _contentInset = UIEdgeInsetsZero; 78 | _scrollEventThrottle = 0.0; 79 | 80 | _BOTTOMBAR_HEIGHT = (int)[[UIScreen mainScreen] nativeBounds].size.height > 1334 ? 74 : 48; 81 | 82 | 83 | WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init]; 84 | config.processPool = processPool; 85 | WKUserContentController* userController = [[WKUserContentController alloc]init]; 86 | [userController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"reactNative"]; 87 | 88 | config.userContentController = userController; 89 | 90 | _webView = [[WKWebView alloc] initWithFrame:self.bounds configuration:config]; 91 | _webView.UIDelegate = self; 92 | _webView.navigationDelegate = self; 93 | _webView.scrollView.delegate = self; 94 | 95 | 96 | #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ 97 | // `contentInsetAdjustmentBehavior` is only available since iOS 11. 98 | // We set the default behavior to "never" so that iOS 99 | // doesn't do weird things to UIScrollView insets automatically 100 | // and keeps it as an opt-in behavior. 101 | if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { 102 | _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; 103 | } 104 | #endif 105 | [self setupPostMessageScript]; 106 | [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil]; 107 | [self addSubview:_webView]; 108 | } 109 | return self; 110 | } 111 | 112 | - (void)setInjectJavaScript:(NSString *)injectJavaScript { 113 | _injectJavaScript = injectJavaScript; 114 | self.atStartScript = [[WKUserScript alloc] initWithSource:injectJavaScript 115 | injectionTime:WKUserScriptInjectionTimeAtDocumentStart 116 | forMainFrameOnly:_injectJavaScriptForMainFrameOnly]; 117 | [self resetupScripts]; 118 | } 119 | 120 | - (void)setInjectedJavaScript:(NSString *)script { 121 | _injectedJavaScript = script; 122 | self.atEndScript = [[WKUserScript alloc] initWithSource:script 123 | injectionTime:WKUserScriptInjectionTimeAtDocumentEnd 124 | forMainFrameOnly:_injectedJavaScriptForMainFrameOnly]; 125 | [self resetupScripts]; 126 | } 127 | 128 | - (void)setInjectedJavaScriptForMainFrameOnly:(BOOL)injectedJavaScriptForMainFrameOnly { 129 | _injectedJavaScriptForMainFrameOnly = injectedJavaScriptForMainFrameOnly; 130 | if (_injectedJavaScript != nil) { 131 | [self setInjectedJavaScript:_injectedJavaScript]; 132 | } 133 | } 134 | 135 | - (void)setInjectJavaScriptForMainFrameOnly:(BOOL)injectJavaScriptForMainFrameOnly { 136 | _injectJavaScriptForMainFrameOnly = injectJavaScriptForMainFrameOnly; 137 | if (_injectJavaScript != nil) { 138 | [self setInjectJavaScript:_injectJavaScript]; 139 | } 140 | } 141 | 142 | - (void)setMessagingEnabled:(BOOL)messagingEnabled { 143 | _messagingEnabled = messagingEnabled; 144 | [self setupPostMessageScript]; 145 | } 146 | 147 | - (void)resetupScripts { 148 | [_webView.configuration.userContentController removeAllUserScripts]; 149 | [self setupPostMessageScript]; 150 | if (self.atStartScript) { 151 | [_webView.configuration.userContentController addUserScript:self.atStartScript]; 152 | } 153 | if (self.atEndScript) { 154 | [_webView.configuration.userContentController addUserScript:self.atEndScript]; 155 | } 156 | } 157 | 158 | - (void)setupPostMessageScript { 159 | if (_messagingEnabled) { 160 | NSString *source=@"window.postMessageToNative = function (data) { window.webkit.messageHandlers.reactNative.postMessage(data); }"; 161 | WKUserScript *script = [[WKUserScript alloc] initWithSource:source 162 | injectionTime:WKUserScriptInjectionTimeAtDocumentStart 163 | forMainFrameOnly:_injectedJavaScriptForMainFrameOnly]; 164 | [_webView.configuration.userContentController addUserScript:script]; 165 | } 166 | } 167 | 168 | - (void)loadRequest:(NSURLRequest *)request 169 | { 170 | if (request.URL && _sendCookies) { 171 | NSDictionary *cookies = [NSHTTPCookie requestHeaderFieldsWithCookies:[[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL]]; 172 | if ([cookies objectForKey:@"Cookie"]) { 173 | NSMutableURLRequest *mutableRequest = request.mutableCopy; 174 | [mutableRequest addValue:cookies[@"Cookie"] forHTTPHeaderField:@"Cookie"]; 175 | request = mutableRequest; 176 | } 177 | } 178 | 179 | [_webView loadRequest:request]; 180 | } 181 | 182 | -(void)setAllowsLinkPreview:(BOOL)allowsLinkPreview 183 | { 184 | if ([_webView respondsToSelector:@selector(allowsLinkPreview)]) { 185 | _webView.allowsLinkPreview = allowsLinkPreview; 186 | } 187 | } 188 | 189 | -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView 190 | { 191 | if (!hideKeyboardAccessoryView) { 192 | return; 193 | } 194 | 195 | UIView* subview; 196 | for (UIView* view in _webView.scrollView.subviews) { 197 | if([[view.class description] hasPrefix:@"WKContent"]) 198 | subview = view; 199 | } 200 | 201 | if(subview == nil) return; 202 | 203 | NSString* name = [NSString stringWithFormat:@"%@_SwizzleHelperWK", subview.class.superclass]; 204 | Class newClass = NSClassFromString(name); 205 | 206 | if(newClass == nil) 207 | { 208 | newClass = objc_allocateClassPair(subview.class, [name cStringUsingEncoding:NSASCIIStringEncoding], 0); 209 | if(!newClass) return; 210 | 211 | Method method = class_getInstanceMethod([_SwizzleHelperWK class], @selector(inputAccessoryView)); 212 | class_addMethod(newClass, @selector(inputAccessoryView), method_getImplementation(method), method_getTypeEncoding(method)); 213 | 214 | objc_registerClassPair(newClass); 215 | } 216 | 217 | object_setClass(subview, newClass); 218 | } 219 | 220 | // https://github.com/Telerik-Verified-Plugins/WKWebView/commit/04e8296adeb61f289f9c698045c19b62d080c7e3 221 | // https://stackoverflow.com/a/48623286/3297914 222 | -(void)setKeyboardDisplayRequiresUserAction:(BOOL)keyboardDisplayRequiresUserAction 223 | { 224 | if (!keyboardDisplayRequiresUserAction) { 225 | Class class = NSClassFromString(@"WKContentView"); 226 | NSOperatingSystemVersion iOS_11_3_0 = (NSOperatingSystemVersion){11, 3, 0}; 227 | 228 | if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_11_3_0]) { 229 | SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:"); 230 | Method method = class_getInstanceMethod(class, selector); 231 | IMP original = method_getImplementation(method); 232 | IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) { 233 | ((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4); 234 | }); 235 | method_setImplementation(method, override); 236 | } else { 237 | SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:"); 238 | Method method = class_getInstanceMethod(class, selector); 239 | IMP original = method_getImplementation(method); 240 | IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, id arg3) { 241 | ((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3); 242 | }); 243 | method_setImplementation(method, override); 244 | } 245 | } 246 | } 247 | 248 | #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ 249 | - (void)setContentInsetAdjustmentBehavior:(UIScrollViewContentInsetAdjustmentBehavior)behavior 250 | { 251 | // `contentInsetAdjustmentBehavior` is available since iOS 11. 252 | if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { 253 | CGPoint contentOffset = _webView.scrollView.contentOffset; 254 | _webView.scrollView.contentInsetAdjustmentBehavior = behavior; 255 | _webView.scrollView.contentOffset = contentOffset; 256 | } 257 | } 258 | #endif 259 | 260 | - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message 261 | { 262 | if (_onMessage) { 263 | NSMutableDictionary *event = [self baseEvent]; 264 | [event addEntriesFromDictionary: @{ 265 | @"data": message.body, 266 | @"name": message.name 267 | }]; 268 | _onMessage(event); 269 | } 270 | } 271 | 272 | - (void)goForward 273 | { 274 | [_webView goForward]; 275 | } 276 | 277 | - (void)evaluateJavaScript:(NSString *)javaScriptString 278 | completionHandler:(void (^)(id, NSError *error))completionHandler 279 | { 280 | [_webView evaluateJavaScript:javaScriptString completionHandler:completionHandler]; 281 | } 282 | 283 | - (void)postMessage:(NSString *)message 284 | { 285 | NSDictionary *eventInitDict = @{ 286 | @"data": message, 287 | }; 288 | NSString *source = [NSString 289 | stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));", 290 | RCTJSONStringify(eventInitDict, NULL) 291 | ]; 292 | [_webView evaluateJavaScript:source completionHandler:nil]; 293 | } 294 | 295 | 296 | - (void)goBack 297 | { 298 | [_webView goBack]; 299 | } 300 | 301 | - (BOOL)canGoBack 302 | { 303 | return [_webView canGoBack]; 304 | } 305 | 306 | - (BOOL)canGoForward 307 | { 308 | return [_webView canGoForward]; 309 | } 310 | 311 | - (void)reload 312 | { 313 | NSURLRequest *request = [RCTConvert NSURLRequest:self.source]; 314 | if (request.URL && !_webView.URL.absoluteString.length) { 315 | [self loadRequest:request]; 316 | } 317 | else { 318 | [_webView reload]; 319 | } 320 | } 321 | 322 | - (void)stopLoading 323 | { 324 | [_webView stopLoading]; 325 | } 326 | 327 | - (void)setSource:(NSDictionary *)source 328 | { 329 | if (![_source isEqualToDictionary:source]) { 330 | _source = [source copy]; 331 | _sendCookies = [source[@"sendCookies"] boolValue]; 332 | if ([source[@"customUserAgent"] length] != 0 && [_webView respondsToSelector:@selector(setCustomUserAgent:)]) { 333 | [_webView setCustomUserAgent:source[@"customUserAgent"]]; 334 | } 335 | 336 | // Allow loading local files: 337 | // 338 | // Only works for iOS 9+. So iOS 8 will simply ignore those two values 339 | NSString *file = [RCTConvert NSString:source[@"file"]]; 340 | NSString *allowingReadAccessToURL = [RCTConvert NSString:source[@"allowingReadAccessToURL"]]; 341 | 342 | if (file && [_webView respondsToSelector:@selector(loadFileURL:allowingReadAccessToURL:)]) { 343 | NSURL *fileURL = [RCTConvert NSURL:file]; 344 | NSURL *baseURL = [RCTConvert NSURL:allowingReadAccessToURL]; 345 | [_webView loadFileURL:fileURL allowingReadAccessToURL:baseURL]; 346 | return; 347 | } 348 | 349 | // Check for a static html source first 350 | NSString *html = [RCTConvert NSString:source[@"html"]]; 351 | if (html) { 352 | NSURL *baseURL = [RCTConvert NSURL:source[@"baseUrl"]]; 353 | if (!baseURL) { 354 | baseURL = [NSURL URLWithString:@"about:blank"]; 355 | } 356 | [_webView loadHTMLString:html baseURL:baseURL]; 357 | return; 358 | } 359 | 360 | NSURLRequest *request = [RCTConvert NSURLRequest:source]; 361 | // Because of the way React works, as pages redirect, we actually end up 362 | // passing the redirect urls back here, so we ignore them if trying to load 363 | // the same url. We'll expose a call to 'reload' to allow a user to load 364 | // the existing page. 365 | if ([request.URL isEqual:_webView.URL]) { 366 | return; 367 | } 368 | if (!request.URL) { 369 | // Clear the webview 370 | [_webView loadHTMLString:@"" baseURL:nil]; 371 | return; 372 | } 373 | [self loadRequest:request]; 374 | } 375 | } 376 | 377 | - (void)layoutSubviews 378 | { 379 | [super layoutSubviews]; 380 | _webView.frame = self.bounds; 381 | } 382 | 383 | - (void)setContentInset:(UIEdgeInsets)contentInset 384 | { 385 | _contentInset = contentInset; 386 | [RCTView autoAdjustInsetsForView:self 387 | withScrollView:_webView.scrollView 388 | updateOffset:NO]; 389 | } 390 | 391 | - (void)setBackgroundColor:(UIColor *)backgroundColor 392 | { 393 | CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor); 394 | self.opaque = _webView.opaque = _webView.scrollView.opaque = (alpha == 1.0); 395 | _webView.backgroundColor = _webView.scrollView.backgroundColor = backgroundColor; 396 | } 397 | 398 | - (UIColor *)backgroundColor 399 | { 400 | return _webView.backgroundColor; 401 | } 402 | 403 | - (NSMutableDictionary *)baseEvent 404 | { 405 | NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary:@{ 406 | @"url": _webView.URL.absoluteString ?: @"", 407 | @"loading" : @(_webView.loading), 408 | @"title": _webView.title, 409 | @"canGoBack": @(_webView.canGoBack), 410 | @"canGoForward" : @(_webView.canGoForward), 411 | }]; 412 | 413 | return event; 414 | } 415 | 416 | - (void)refreshContentInset 417 | { 418 | [RCTView autoAdjustInsetsForView:self 419 | withScrollView:_webView.scrollView 420 | updateOffset:YES]; 421 | } 422 | 423 | - (void)observeValueForKeyPath:(NSString *)keyPath 424 | ofObject:(id)object 425 | change:(NSDictionary *)change 426 | context:(void *)context 427 | { 428 | if ([keyPath isEqualToString:@"estimatedProgress"]) { 429 | if (!_onProgress) { 430 | return; 431 | } 432 | _onProgress(@{@"progress": [change objectForKey:NSKeyValueChangeNewKey]}); 433 | } 434 | } 435 | 436 | - (void)dealloc 437 | { 438 | [_webView removeObserver:self forKeyPath:@"estimatedProgress"]; 439 | _webView.navigationDelegate = nil; 440 | _webView.UIDelegate = nil; 441 | _webView.scrollView.delegate = nil; 442 | } 443 | 444 | - (NSDictionary *)getEventInfo: (UIScrollView *)scrollView { 445 | return @{ 446 | @"contentOffset": @{ 447 | @"x": @(scrollView.contentOffset.x), 448 | @"y": @(scrollView.contentOffset.y) 449 | }, 450 | @"contentInset": @{ 451 | @"top": @(scrollView.contentInset.top), 452 | @"left": @(scrollView.contentInset.left), 453 | @"bottom": @(scrollView.contentInset.bottom), 454 | @"right": @(scrollView.contentInset.right) 455 | }, 456 | @"contentSize": @{ 457 | @"width": @(scrollView.contentSize.width), 458 | @"height": @(scrollView.contentSize.height) 459 | }, 460 | @"layoutMeasurement": @{ 461 | @"width": @(scrollView.frame.size.width), 462 | @"height": @(scrollView.frame.size.height) 463 | }, 464 | @"zoomScale": @(scrollView.zoomScale ?: 1), 465 | }; 466 | } 467 | 468 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView 469 | { 470 | 471 | [self updateClippedSubviews]; 472 | 473 | if (!scrollView.scrollEnabled) { 474 | scrollView.bounds = _webView.bounds; 475 | return; 476 | } 477 | 478 | NSTimeInterval now = CACurrentMediaTime(); 479 | 480 | if (_allowNextScrollNoMatterWhat || 481 | (_scrollEventThrottle > 0 && _scrollEventThrottle < (now - _lastScrollDispatchTime))) { 482 | 483 | _onScroll([self getEventInfo:scrollView]); 484 | // Update dispatch time 485 | _lastScrollDispatchTime = now; 486 | _allowNextScrollNoMatterWhat = NO; 487 | } 488 | 489 | } 490 | 491 | - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView 492 | { 493 | _onScrollBeginDrag([self getEventInfo:scrollView]); 494 | } 495 | 496 | - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView 497 | { 498 | _onScrollEndDrag([self getEventInfo:scrollView]); 499 | } 500 | 501 | - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView 502 | { 503 | 504 | // Fire the being deceleration event 505 | _onMomentumScrollBegin([self getEventInfo:scrollView]); 506 | } 507 | 508 | - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView 509 | { 510 | [self scrollViewDidScroll:scrollView]; 511 | 512 | // Fire the end deceleration event 513 | _onMomentumScrollEnd([self getEventInfo:scrollView]); 514 | } 515 | 516 | - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView 517 | { 518 | [self scrollViewDidScroll:scrollView]; 519 | 520 | // Fire the end deceleration event 521 | _onMomentumScrollEnd([self getEventInfo:scrollView]); 522 | } 523 | 524 | - (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView 525 | { 526 | _onScrollToTop([self getEventInfo:scrollView]); 527 | } 528 | 529 | #pragma mark - WKNavigationDelegate methods 530 | 531 | #if DEBUG 532 | - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { 533 | NSURLCredential * credential = [[NSURLCredential alloc] initWithTrust:[challenge protectionSpace].serverTrust]; 534 | completionHandler(NSURLSessionAuthChallengeUseCredential, credential); 535 | } 536 | #endif 537 | 538 | - (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 539 | { 540 | static NSDictionary *navigationTypes; 541 | static dispatch_once_t onceToken; 542 | 543 | dispatch_once(&onceToken, ^{ 544 | navigationTypes = @{ 545 | @(WKNavigationTypeLinkActivated): @"click", 546 | @(WKNavigationTypeFormSubmitted): @"formsubmit", 547 | @(WKNavigationTypeBackForward): @"backforward", 548 | @(WKNavigationTypeReload): @"reload", 549 | @(WKNavigationTypeFormResubmitted): @"formresubmit", 550 | @(WKNavigationTypeOther): @"other", 551 | }; 552 | }); 553 | 554 | WKNavigationType navigationType = navigationAction.navigationType; 555 | NSURLRequest *request = navigationAction.request; 556 | 557 | if (_onShouldStartLoadWithRequest) { 558 | NSMutableDictionary *event = [self baseEvent]; 559 | [event addEntriesFromDictionary: @{ 560 | @"url": (request.URL).absoluteString, 561 | @"mainDocumentURL": (request.mainDocumentURL).absoluteString, 562 | @"navigationType": navigationTypes[@(navigationType)] 563 | }]; 564 | if (![self.delegate webView:self 565 | shouldStartLoadForRequest:event 566 | withCallback:_onShouldStartLoadWithRequest]) { 567 | decisionHandler(WKNavigationActionPolicyCancel); 568 | return; 569 | } 570 | } 571 | 572 | if (_onLoadingStart) { 573 | // We have this check to filter out iframe requests and whatnot 574 | BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; 575 | if (isTopFrame) { 576 | NSMutableDictionary *event = [self baseEvent]; 577 | [event addEntriesFromDictionary: @{ 578 | @"url": (request.URL).absoluteString, 579 | @"navigationType": navigationTypes[@(navigationType)] 580 | }]; 581 | _onLoadingStart(event); 582 | } 583 | } 584 | 585 | // Allow all navigation by default 586 | 587 | 588 | NSString* scheme = request.URL.scheme; 589 | NSArray *blacklistedSchemes = @[@"u2f"]; 590 | if ([blacklistedSchemes containsObject:scheme]) { 591 | decisionHandler(WKNavigationActionPolicyCancel); 592 | } else { 593 | decisionHandler(WKNavigationActionPolicyAllow); 594 | } 595 | } 596 | 597 | - (void)webView:(__unused WKWebView *)webView didFailProvisionalNavigation:(__unused WKNavigation *)navigation withError:(NSError *)error 598 | { 599 | if (_onLoadingError) { 600 | if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { 601 | // NSURLErrorCancelled is reported when a page has a redirect OR if you load 602 | // a new URL in the WebView before the previous one came back. We can just 603 | // ignore these since they aren't real errors. 604 | // http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os 605 | return; 606 | } 607 | 608 | NSMutableDictionary *event = [self baseEvent]; 609 | [event addEntriesFromDictionary:@{ 610 | @"domain": error.domain, 611 | @"code": @(error.code), 612 | @"description": error.localizedDescription, 613 | }]; 614 | _onLoadingError(event); 615 | } 616 | } 617 | 618 | - (void)webView:(WKWebView *)webView didFinishNavigation:(__unused WKNavigation *)navigation 619 | { 620 | // we only need the final 'finishLoad' call so only fire the event when we're actually done loading. 621 | if (_onLoadingFinish && !webView.loading && ![webView.URL.absoluteString isEqualToString:@"about:blank"]) { 622 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 623 | if(webView.scrollView.contentSize.height <= webView.scrollView.frame.size.height){ 624 | webView.scrollView.contentInset = UIEdgeInsetsMake(0, 0, _BOTTOMBAR_HEIGHT, 0); 625 | } else { 626 | webView.scrollView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0); 627 | } 628 | }); 629 | 630 | _onLoadingFinish([self baseEvent]); 631 | } 632 | } 633 | 634 | #pragma mark - WKUIDelegate 635 | 636 | - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { 637 | // We need a delay here because we have a lot of modal views (signing, connect, etc) that might not be dismissed 638 | // by the time we have to present an alert. This delays prevents the app from crashing in those scenarios 639 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.7 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 640 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert]; 641 | [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { 642 | completionHandler(); 643 | }]]; 644 | UIViewController *presentingController = RCTPresentedViewController(); 645 | [presentingController presentViewController:alertController animated:YES completion:nil]; 646 | }); 647 | 648 | } 649 | 650 | - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler { 651 | 652 | // TODO We have to think message to confirm "YES" 653 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert]; 654 | [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 655 | completionHandler(YES); 656 | }]]; 657 | [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { 658 | completionHandler(NO); 659 | }]]; 660 | UIViewController *presentingController = RCTPresentedViewController(); 661 | [presentingController presentViewController:alertController animated:YES completion:nil]; 662 | } 663 | 664 | - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler { 665 | 666 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:nil preferredStyle:UIAlertControllerStyleAlert]; 667 | [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { 668 | textField.text = defaultText; 669 | }]; 670 | 671 | [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 672 | NSString *input = ((UITextField *)alertController.textFields.firstObject).text; 673 | completionHandler(input); 674 | }]]; 675 | 676 | [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { 677 | completionHandler(nil); 678 | }]]; 679 | UIViewController *presentingController = RCTPresentedViewController(); 680 | [presentingController presentViewController:alertController animated:YES completion:nil]; 681 | } 682 | 683 | - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures 684 | { 685 | NSString *scheme = navigationAction.request.URL.scheme; 686 | if ((navigationAction.targetFrame.isMainFrame || _openNewWindowInWebView) && ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"])) { 687 | [webView loadRequest:navigationAction.request]; 688 | } else { 689 | UIApplication *app = [UIApplication sharedApplication]; 690 | NSURL *url = navigationAction.request.URL; 691 | if ([app canOpenURL:url]) { 692 | [app openURL:url]; 693 | } 694 | } 695 | return nil; 696 | } 697 | 698 | - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView 699 | { 700 | RCTLogWarn(@"Webview Process Terminated"); 701 | } 702 | 703 | @end 704 | -------------------------------------------------------------------------------- /ios/RNWeb3Webview.podspec: -------------------------------------------------------------------------------- 1 | 2 | Pod::Spec.new do |s| 3 | s.name = "RNWeb3Webview" 4 | s.version = "1.0.0" 5 | s.summary = "RNWeb3Webview" 6 | s.description = <<-DESC 7 | RNWeb3Webview 8 | DESC 9 | s.homepage = "" 10 | s.license = "MIT" 11 | # s.license = { :type => "MIT", :file => "FILE_LICENSE" } 12 | s.author = { "author" => "author@domain.cn" } 13 | s.platform = :ios, "7.0" 14 | s.source = { :git => "https://github.com/author/RNWeb3Webview.git", :tag => "master" } 15 | s.source_files = "RNWeb3Webview/**/*.{h,m}" 16 | s.requires_arc = true 17 | 18 | 19 | s.dependency "React" 20 | #s.dependency "others" 21 | 22 | end 23 | 24 | -------------------------------------------------------------------------------- /ios/RNWeb3Webview.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 151AF0C8213A70E300CBCC36 /* RNWeb3Webview.m in Sources */ = {isa = PBXBuildFile; fileRef = 151AF0C0213A70E200CBCC36 /* RNWeb3Webview.m */; }; 11 | 151AF0C9213A70E300CBCC36 /* WKProcessPool+SharedProcessPool.m in Sources */ = {isa = PBXBuildFile; fileRef = 151AF0C1213A70E200CBCC36 /* WKProcessPool+SharedProcessPool.m */; }; 12 | 151AF0CA213A70E300CBCC36 /* WeakScriptMessageDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 151AF0C2213A70E200CBCC36 /* WeakScriptMessageDelegate.m */; }; 13 | 151AF0CB213A70E300CBCC36 /* RNWeb3WebviewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 151AF0C7213A70E300CBCC36 /* RNWeb3WebviewManager.m */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXCopyFilesBuildPhase section */ 17 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 18 | isa = PBXCopyFilesBuildPhase; 19 | buildActionMask = 2147483647; 20 | dstPath = "include/$(PRODUCT_NAME)"; 21 | dstSubfolderSpec = 16; 22 | files = ( 23 | ); 24 | runOnlyForDeploymentPostprocessing = 0; 25 | }; 26 | /* End PBXCopyFilesBuildPhase section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | 134814201AA4EA6300B7C361 /* libRNWeb3Webview.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNWeb3Webview.a; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | 151AF0C0213A70E200CBCC36 /* RNWeb3Webview.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNWeb3Webview.m; sourceTree = ""; }; 31 | 151AF0C1213A70E200CBCC36 /* WKProcessPool+SharedProcessPool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "WKProcessPool+SharedProcessPool.m"; sourceTree = ""; }; 32 | 151AF0C2213A70E200CBCC36 /* WeakScriptMessageDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WeakScriptMessageDelegate.m; sourceTree = ""; }; 33 | 151AF0C3213A70E200CBCC36 /* RNWeb3Webview.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNWeb3Webview.h; sourceTree = ""; }; 34 | 151AF0C4213A70E300CBCC36 /* WeakScriptMessageDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WeakScriptMessageDelegate.h; sourceTree = ""; }; 35 | 151AF0C5213A70E300CBCC36 /* WKProcessPool+SharedProcessPool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "WKProcessPool+SharedProcessPool.h"; sourceTree = ""; }; 36 | 151AF0C6213A70E300CBCC36 /* RNWeb3WebviewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNWeb3WebviewManager.h; sourceTree = ""; }; 37 | 151AF0C7213A70E300CBCC36 /* RNWeb3WebviewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNWeb3WebviewManager.m; sourceTree = ""; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | /* End PBXFrameworksBuildPhase section */ 49 | 50 | /* Begin PBXGroup section */ 51 | 134814211AA4EA7D00B7C361 /* Products */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | 134814201AA4EA6300B7C361 /* libRNWeb3Webview.a */, 55 | ); 56 | name = Products; 57 | sourceTree = ""; 58 | }; 59 | 58B511D21A9E6C8500147676 = { 60 | isa = PBXGroup; 61 | children = ( 62 | 151AF0C3213A70E200CBCC36 /* RNWeb3Webview.h */, 63 | 151AF0C0213A70E200CBCC36 /* RNWeb3Webview.m */, 64 | 151AF0C6213A70E300CBCC36 /* RNWeb3WebviewManager.h */, 65 | 151AF0C7213A70E300CBCC36 /* RNWeb3WebviewManager.m */, 66 | 151AF0C4213A70E300CBCC36 /* WeakScriptMessageDelegate.h */, 67 | 151AF0C2213A70E200CBCC36 /* WeakScriptMessageDelegate.m */, 68 | 151AF0C5213A70E300CBCC36 /* WKProcessPool+SharedProcessPool.h */, 69 | 151AF0C1213A70E200CBCC36 /* WKProcessPool+SharedProcessPool.m */, 70 | 134814211AA4EA7D00B7C361 /* Products */, 71 | ); 72 | sourceTree = ""; 73 | }; 74 | /* End PBXGroup section */ 75 | 76 | /* Begin PBXNativeTarget section */ 77 | 58B511DA1A9E6C8500147676 /* RNWeb3Webview */ = { 78 | isa = PBXNativeTarget; 79 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNWeb3Webview" */; 80 | buildPhases = ( 81 | 58B511D71A9E6C8500147676 /* Sources */, 82 | 58B511D81A9E6C8500147676 /* Frameworks */, 83 | 58B511D91A9E6C8500147676 /* CopyFiles */, 84 | ); 85 | buildRules = ( 86 | ); 87 | dependencies = ( 88 | ); 89 | name = RNWeb3Webview; 90 | productName = RCTDataManager; 91 | productReference = 134814201AA4EA6300B7C361 /* libRNWeb3Webview.a */; 92 | productType = "com.apple.product-type.library.static"; 93 | }; 94 | /* End PBXNativeTarget section */ 95 | 96 | /* Begin PBXProject section */ 97 | 58B511D31A9E6C8500147676 /* Project object */ = { 98 | isa = PBXProject; 99 | attributes = { 100 | LastUpgradeCheck = 0830; 101 | ORGANIZATIONNAME = Facebook; 102 | TargetAttributes = { 103 | 58B511DA1A9E6C8500147676 = { 104 | CreatedOnToolsVersion = 6.1.1; 105 | }; 106 | }; 107 | }; 108 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNWeb3Webview" */; 109 | compatibilityVersion = "Xcode 3.2"; 110 | developmentRegion = English; 111 | hasScannedForEncodings = 0; 112 | knownRegions = ( 113 | en, 114 | ); 115 | mainGroup = 58B511D21A9E6C8500147676; 116 | productRefGroup = 58B511D21A9E6C8500147676; 117 | projectDirPath = ""; 118 | projectRoot = ""; 119 | targets = ( 120 | 58B511DA1A9E6C8500147676 /* RNWeb3Webview */, 121 | ); 122 | }; 123 | /* End PBXProject section */ 124 | 125 | /* Begin PBXSourcesBuildPhase section */ 126 | 58B511D71A9E6C8500147676 /* Sources */ = { 127 | isa = PBXSourcesBuildPhase; 128 | buildActionMask = 2147483647; 129 | files = ( 130 | 151AF0C9213A70E300CBCC36 /* WKProcessPool+SharedProcessPool.m in Sources */, 131 | 151AF0CB213A70E300CBCC36 /* RNWeb3WebviewManager.m in Sources */, 132 | 151AF0C8213A70E300CBCC36 /* RNWeb3Webview.m in Sources */, 133 | 151AF0CA213A70E300CBCC36 /* WeakScriptMessageDelegate.m in Sources */, 134 | ); 135 | runOnlyForDeploymentPostprocessing = 0; 136 | }; 137 | /* End PBXSourcesBuildPhase section */ 138 | 139 | /* Begin XCBuildConfiguration section */ 140 | 58B511ED1A9E6C8500147676 /* Debug */ = { 141 | isa = XCBuildConfiguration; 142 | buildSettings = { 143 | ALWAYS_SEARCH_USER_PATHS = NO; 144 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 145 | CLANG_CXX_LIBRARY = "libc++"; 146 | CLANG_ENABLE_MODULES = YES; 147 | CLANG_ENABLE_OBJC_ARC = YES; 148 | CLANG_WARN_BOOL_CONVERSION = YES; 149 | CLANG_WARN_CONSTANT_CONVERSION = YES; 150 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 151 | CLANG_WARN_EMPTY_BODY = YES; 152 | CLANG_WARN_ENUM_CONVERSION = YES; 153 | CLANG_WARN_INFINITE_RECURSION = YES; 154 | CLANG_WARN_INT_CONVERSION = YES; 155 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 156 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 157 | CLANG_WARN_UNREACHABLE_CODE = YES; 158 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 159 | COPY_PHASE_STRIP = NO; 160 | ENABLE_STRICT_OBJC_MSGSEND = YES; 161 | ENABLE_TESTABILITY = YES; 162 | GCC_C_LANGUAGE_STANDARD = gnu99; 163 | GCC_DYNAMIC_NO_PIC = NO; 164 | GCC_NO_COMMON_BLOCKS = YES; 165 | GCC_OPTIMIZATION_LEVEL = 0; 166 | GCC_PREPROCESSOR_DEFINITIONS = ( 167 | "DEBUG=1", 168 | "$(inherited)", 169 | ); 170 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 171 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 172 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 173 | GCC_WARN_UNDECLARED_SELECTOR = YES; 174 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 175 | GCC_WARN_UNUSED_FUNCTION = YES; 176 | GCC_WARN_UNUSED_VARIABLE = YES; 177 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 178 | MTL_ENABLE_DEBUG_INFO = YES; 179 | ONLY_ACTIVE_ARCH = YES; 180 | SDKROOT = iphoneos; 181 | }; 182 | name = Debug; 183 | }; 184 | 58B511EE1A9E6C8500147676 /* Release */ = { 185 | isa = XCBuildConfiguration; 186 | buildSettings = { 187 | ALWAYS_SEARCH_USER_PATHS = NO; 188 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 189 | CLANG_CXX_LIBRARY = "libc++"; 190 | CLANG_ENABLE_MODULES = YES; 191 | CLANG_ENABLE_OBJC_ARC = YES; 192 | CLANG_WARN_BOOL_CONVERSION = YES; 193 | CLANG_WARN_CONSTANT_CONVERSION = YES; 194 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 195 | CLANG_WARN_EMPTY_BODY = YES; 196 | CLANG_WARN_ENUM_CONVERSION = YES; 197 | CLANG_WARN_INFINITE_RECURSION = YES; 198 | CLANG_WARN_INT_CONVERSION = YES; 199 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 200 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 201 | CLANG_WARN_UNREACHABLE_CODE = YES; 202 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 203 | COPY_PHASE_STRIP = YES; 204 | ENABLE_NS_ASSERTIONS = NO; 205 | ENABLE_STRICT_OBJC_MSGSEND = YES; 206 | GCC_C_LANGUAGE_STANDARD = gnu99; 207 | GCC_NO_COMMON_BLOCKS = YES; 208 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 209 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 210 | GCC_WARN_UNDECLARED_SELECTOR = YES; 211 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 212 | GCC_WARN_UNUSED_FUNCTION = YES; 213 | GCC_WARN_UNUSED_VARIABLE = YES; 214 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 215 | MTL_ENABLE_DEBUG_INFO = NO; 216 | SDKROOT = iphoneos; 217 | VALIDATE_PRODUCT = YES; 218 | }; 219 | name = Release; 220 | }; 221 | 58B511F01A9E6C8500147676 /* Debug */ = { 222 | isa = XCBuildConfiguration; 223 | buildSettings = { 224 | HEADER_SEARCH_PATHS = ( 225 | "$(inherited)", 226 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 227 | "$(SRCROOT)/../../../React/**", 228 | "$(SRCROOT)/../../react-native/React/**", 229 | ); 230 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 231 | OTHER_LDFLAGS = "-ObjC"; 232 | PRODUCT_NAME = RNWeb3Webview; 233 | SKIP_INSTALL = YES; 234 | }; 235 | name = Debug; 236 | }; 237 | 58B511F11A9E6C8500147676 /* Release */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | HEADER_SEARCH_PATHS = ( 241 | "$(inherited)", 242 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 243 | "$(SRCROOT)/../../../React/**", 244 | "$(SRCROOT)/../../react-native/React/**", 245 | ); 246 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 247 | OTHER_LDFLAGS = "-ObjC"; 248 | PRODUCT_NAME = RNWeb3Webview; 249 | SKIP_INSTALL = YES; 250 | }; 251 | name = Release; 252 | }; 253 | /* End XCBuildConfiguration section */ 254 | 255 | /* Begin XCConfigurationList section */ 256 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNWeb3Webview" */ = { 257 | isa = XCConfigurationList; 258 | buildConfigurations = ( 259 | 58B511ED1A9E6C8500147676 /* Debug */, 260 | 58B511EE1A9E6C8500147676 /* Release */, 261 | ); 262 | defaultConfigurationIsVisible = 0; 263 | defaultConfigurationName = Release; 264 | }; 265 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNWeb3Webview" */ = { 266 | isa = XCConfigurationList; 267 | buildConfigurations = ( 268 | 58B511F01A9E6C8500147676 /* Debug */, 269 | 58B511F11A9E6C8500147676 /* Release */, 270 | ); 271 | defaultConfigurationIsVisible = 0; 272 | defaultConfigurationName = Release; 273 | }; 274 | /* End XCConfigurationList section */ 275 | }; 276 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 277 | } 278 | -------------------------------------------------------------------------------- /ios/RNWeb3Webview.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | 3 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/RNWeb3WebviewManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface RCTConvert (UIScrollView) 5 | 6 | @end 7 | 8 | @interface RNWeb3WebviewManager : RCTViewManager 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /ios/RNWeb3WebviewManager.m: -------------------------------------------------------------------------------- 1 | #import "RNWeb3WebviewManager.h" 2 | 3 | #import "RNWeb3Webview.h" 4 | #import "WKProcessPool+SharedProcessPool.h" 5 | #import 6 | #import 7 | #import 8 | #import 9 | #import 10 | 11 | #import 12 | 13 | @implementation RCTConvert (UIScrollView) 14 | 15 | #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ 16 | RCT_ENUM_CONVERTER(UIScrollViewContentInsetAdjustmentBehavior, (@{ 17 | @"automatic": @(UIScrollViewContentInsetAdjustmentAutomatic), 18 | @"scrollableAxes": @(UIScrollViewContentInsetAdjustmentScrollableAxes), 19 | @"never": @(UIScrollViewContentInsetAdjustmentNever), 20 | @"always": @(UIScrollViewContentInsetAdjustmentAlways), 21 | }), UIScrollViewContentInsetAdjustmentNever, integerValue) 22 | #endif 23 | 24 | @end 25 | 26 | @interface RNWeb3WebviewManager () 27 | 28 | @end 29 | 30 | @implementation RNWeb3WebviewManager 31 | { 32 | NSConditionLock *_shouldStartLoadLock; 33 | BOOL _shouldStartLoad; 34 | } 35 | 36 | RCT_EXPORT_MODULE() 37 | 38 | - (UIView *)view 39 | { 40 | RNWeb3Webview *webView = [[RNWeb3Webview alloc] initWithProcessPool:[WKProcessPool sharedProcessPool]]; 41 | webView.delegate = self; 42 | return webView; 43 | } 44 | 45 | RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary) 46 | RCT_REMAP_VIEW_PROPERTY(bounces, _webView.scrollView.bounces, BOOL) 47 | RCT_REMAP_VIEW_PROPERTY(pagingEnabled, _webView.scrollView.pagingEnabled, BOOL) 48 | RCT_REMAP_VIEW_PROPERTY(scrollEnabled, _webView.scrollView.scrollEnabled, BOOL) 49 | RCT_REMAP_VIEW_PROPERTY(directionalLockEnabled, _webView.scrollView.directionalLockEnabled, BOOL) 50 | RCT_REMAP_VIEW_PROPERTY(allowsBackForwardNavigationGestures, _webView.allowsBackForwardNavigationGestures, BOOL) 51 | RCT_EXPORT_VIEW_PROPERTY(injectJavaScriptForMainFrameOnly, BOOL) 52 | RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptForMainFrameOnly, BOOL) 53 | RCT_EXPORT_VIEW_PROPERTY(injectJavaScript, NSString) 54 | RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString) 55 | RCT_EXPORT_VIEW_PROPERTY(openNewWindowInWebView, BOOL) 56 | RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets) 57 | RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL) 58 | RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock) 59 | RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock) 60 | RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock) 61 | RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock) 62 | RCT_EXPORT_VIEW_PROPERTY(onProgress, RCTDirectEventBlock) 63 | RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock) 64 | RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock) 65 | RCT_EXPORT_VIEW_PROPERTY(onScrollToTop, RCTDirectEventBlock) 66 | RCT_EXPORT_VIEW_PROPERTY(onScrollBeginDrag, RCTDirectEventBlock) 67 | RCT_EXPORT_VIEW_PROPERTY(onScrollEndDrag, RCTDirectEventBlock) 68 | RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollBegin, RCTDirectEventBlock) 69 | RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollEnd, RCTDirectEventBlock) 70 | RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval) 71 | RCT_EXPORT_VIEW_PROPERTY(hideKeyboardAccessoryView, BOOL) 72 | RCT_EXPORT_VIEW_PROPERTY(keyboardDisplayRequiresUserAction, BOOL) 73 | RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL) 74 | RCT_EXPORT_VIEW_PROPERTY(allowsLinkPreview, BOOL) 75 | #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ 76 | RCT_EXPORT_VIEW_PROPERTY(contentInsetAdjustmentBehavior, UIScrollViewContentInsetAdjustmentBehavior) 77 | #endif 78 | 79 | RCT_EXPORT_METHOD(goBack:(nonnull NSNumber *)reactTag) 80 | { 81 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 82 | RNWeb3Webview *view = viewRegistry[reactTag]; 83 | if (![view isKindOfClass:[RNWeb3Webview class]]) { 84 | RCTLogError(@"Invalid view returned from registry, expecting RNWeb3Webview, got: %@", view); 85 | } else { 86 | [view goBack]; 87 | } 88 | }]; 89 | } 90 | 91 | RCT_EXPORT_METHOD(goForward:(nonnull NSNumber *)reactTag) 92 | { 93 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 94 | RNWeb3Webview *view = viewRegistry[reactTag]; 95 | if (![view isKindOfClass:[RNWeb3Webview class]]) { 96 | RCTLogError(@"Invalid view returned from registry, expecting RNWeb3Webview, got: %@", view); 97 | } else { 98 | [view goForward]; 99 | } 100 | }]; 101 | } 102 | 103 | RCT_EXPORT_METHOD(canGoBack:(nonnull NSNumber *)reactTag 104 | resolver:(RCTPromiseResolveBlock)resolve 105 | rejecter:(RCTPromiseRejectBlock)reject) 106 | { 107 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 108 | RNWeb3Webview *view = viewRegistry[reactTag]; 109 | 110 | resolve([NSNumber numberWithBool:[view canGoBack]]); 111 | }]; 112 | } 113 | 114 | RCT_EXPORT_METHOD(canGoForward:(nonnull NSNumber *)reactTag 115 | resolver:(RCTPromiseResolveBlock)resolve 116 | rejecter:(RCTPromiseRejectBlock)reject) 117 | { 118 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 119 | RNWeb3Webview *view = viewRegistry[reactTag]; 120 | 121 | resolve([NSNumber numberWithBool:[view canGoForward]]); 122 | }]; 123 | } 124 | 125 | RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag) 126 | { 127 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 128 | RNWeb3Webview *view = viewRegistry[reactTag]; 129 | if (![view isKindOfClass:[RNWeb3Webview class]]) { 130 | RCTLogError(@"Invalid view returned from registry, expecting RNWeb3Webview, got: %@", view); 131 | } else { 132 | [view reload]; 133 | } 134 | }]; 135 | } 136 | 137 | RCT_EXPORT_METHOD(stopLoading:(nonnull NSNumber *)reactTag) 138 | { 139 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 140 | RNWeb3Webview *view = viewRegistry[reactTag]; 141 | if (![view isKindOfClass:[RNWeb3Webview class]]) { 142 | RCTLogError(@"Invalid view returned from registry, expecting RNWeb3Webview, got: %@", view); 143 | } else { 144 | [view stopLoading]; 145 | } 146 | }]; 147 | } 148 | 149 | RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message) 150 | { 151 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 152 | RNWeb3Webview *view = viewRegistry[reactTag]; 153 | if (![view isKindOfClass:[RNWeb3Webview class]]) { 154 | RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view); 155 | } else { 156 | [view postMessage:message]; 157 | } 158 | }]; 159 | } 160 | 161 | RCT_EXPORT_METHOD(evaluateJavaScript:(nonnull NSNumber *)reactTag 162 | js:(NSString *)js 163 | resolver:(RCTPromiseResolveBlock)resolve 164 | rejecter:(RCTPromiseRejectBlock)reject) 165 | { 166 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 167 | RNWeb3Webview *view = viewRegistry[reactTag]; 168 | if (![view isKindOfClass:[RNWeb3Webview class]]) { 169 | RCTLogError(@"Invalid view returned from registry, expecting RNWeb3Webview, got: %@", view); 170 | } else { 171 | [view evaluateJavaScript:js completionHandler:^(id result, NSError *error) { 172 | if (error) { 173 | reject(@"js_error", @"Error occurred while evaluating Javascript", error); 174 | } else { 175 | resolve(result); 176 | } 177 | }]; 178 | } 179 | }]; 180 | } 181 | 182 | #pragma mark - Exported synchronous methods 183 | 184 | - (BOOL)webView:(__unused RNWeb3Webview *)webView 185 | shouldStartLoadForRequest:(NSMutableDictionary *)request 186 | withCallback:(RCTDirectEventBlock)callback 187 | { 188 | _shouldStartLoadLock = [[NSConditionLock alloc] initWithCondition:arc4random()]; 189 | _shouldStartLoad = YES; 190 | request[@"lockIdentifier"] = @(_shouldStartLoadLock.condition); 191 | callback(request); 192 | 193 | // Block the main thread for a maximum of 250ms until the JS thread returns 194 | if ([_shouldStartLoadLock lockWhenCondition:0 beforeDate:[NSDate dateWithTimeIntervalSinceNow:.25]]) { 195 | BOOL returnValue = _shouldStartLoad; 196 | [_shouldStartLoadLock unlock]; 197 | _shouldStartLoadLock = nil; 198 | return returnValue; 199 | } else { 200 | RCTLogWarn(@"Did not receive response to shouldStartLoad in time, defaulting to YES"); 201 | return YES; 202 | } 203 | } 204 | 205 | RCT_EXPORT_METHOD(startLoadWithResult:(BOOL)result lockIdentifier:(NSInteger)lockIdentifier) 206 | { 207 | if ([_shouldStartLoadLock tryLockWhenCondition:lockIdentifier]) { 208 | _shouldStartLoad = result; 209 | [_shouldStartLoadLock unlockWithCondition:0]; 210 | } else { 211 | RCTLogWarn(@"startLoadWithResult invoked with invalid lockIdentifier: " 212 | "got %zd, expected %zd", lockIdentifier, _shouldStartLoadLock.condition); 213 | } 214 | } 215 | 216 | @end 217 | -------------------------------------------------------------------------------- /ios/WKProcessPool+SharedProcessPool.h: -------------------------------------------------------------------------------- 1 | @interface WKProcessPool (SharedProcessPool) 2 | + (WKProcessPool*)sharedProcessPool; 3 | @end 4 | -------------------------------------------------------------------------------- /ios/WKProcessPool+SharedProcessPool.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "WKProcessPool+SharedProcessPool.h" 4 | 5 | @implementation WKProcessPool (SharedProcessPool) 6 | 7 | + (WKProcessPool*)sharedProcessPool { 8 | static WKProcessPool* _sharedProcessPool; 9 | static dispatch_once_t onceToken; 10 | dispatch_once(&onceToken, ^{ 11 | _sharedProcessPool = [[WKProcessPool alloc] init]; 12 | }); 13 | return _sharedProcessPool; 14 | } 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /ios/WeakScriptMessageDelegate.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | #import 4 | 5 | // Trampoline object to avoid retain cycle with the script message handler 6 | @interface WeakScriptMessageDelegate : NSObject 7 | 8 | @property (nonatomic, weak) id scriptDelegate; 9 | 10 | - (instancetype)initWithDelegate:(id)scriptDelegate; 11 | 12 | @end 13 | 14 | -------------------------------------------------------------------------------- /ios/WeakScriptMessageDelegate.m: -------------------------------------------------------------------------------- 1 | 2 | #import "WeakScriptMessageDelegate.h" 3 | 4 | @implementation WeakScriptMessageDelegate 5 | 6 | - (instancetype)initWithDelegate:(id)scriptDelegate 7 | { 8 | self = [super init]; 9 | if (self) { 10 | _scriptDelegate = scriptDelegate; 11 | } 12 | return self; 13 | } 14 | 15 | - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message 16 | { 17 | [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message]; 18 | } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-web3-webview", 3 | "version": "2.1.4", 4 | "description": "A react native webview optimized for a web3 dApp browser application", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "react", 11 | "native", 12 | "wkwebview", 13 | "webview", 14 | "ios", 15 | "android", 16 | "web3", 17 | "ethereum" 18 | ], 19 | "author": { 20 | "name": "Bruno Barbieri", 21 | "email": "brunobar79@gmail.com" 22 | }, 23 | "homepage": "https://github.com/brunobar79/react-native-web3-webview", 24 | "bugs": { 25 | "url": "https://github.com/brunobar79/react-native-web3-webview/issues" 26 | }, 27 | "license": "MIT", 28 | "dependencies": { 29 | "fbjs": "^0.8.3" 30 | }, 31 | "peerDependencies": { 32 | "prop-types": "^15.6.0", 33 | "react": "^16.0.0", 34 | "react-native": "^0.56.0" 35 | } 36 | } 37 | --------------------------------------------------------------------------------