├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── WebView.android.js ├── WebView.ios.js ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── im │ └── shimo │ └── react │ └── webview │ ├── AdvancedWebViewManager.java │ └── AdvancedWebViewPackage.java ├── example ├── .babelrc ├── .buckconfig ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .watchmanconfig ├── App.js ├── android │ ├── app │ │ ├── BUCK │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── keystores │ │ ├── BUCK │ │ └── debug.keystore.properties │ └── settings.gradle ├── app.json ├── index.js ├── ios │ ├── example-tvOS │ │ └── Info.plist │ ├── example-tvOSTests │ │ └── Info.plist │ ├── example.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ ├── example-tvOS.xcscheme │ │ │ └── example.xcscheme │ ├── example │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Base.lproj │ │ │ └── LaunchScreen.xib │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── main.m │ └── exampleTests │ │ ├── Info.plist │ │ └── exampleTests.m ├── package.json └── yarn.lock ├── ios ├── RNAdvancedWebView.h ├── RNAdvancedWebView.m ├── RNAdvancedWebView.xcodeproj │ └── project.pbxproj ├── RNAdvancedWebViewManager.h ├── RNAdvancedWebViewManager.m ├── RNCachingURLProtocol │ ├── NSString+Sha1.h │ ├── NSString+Sha1.m │ ├── README.md │ ├── RNCachingURLProtocol.h │ ├── RNCachingURLProtocol.m │ ├── Reachability.h │ └── Reachability.m ├── UIScrollView+adjustedContentInset.h └── UIScrollView+adjustedContentInset.m └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_style = space 11 | indent_size = 4 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | android/ 3 | Example/android/ 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "es6": true, 5 | "node": true 6 | }, 7 | 8 | "ecmaFeatures": { 9 | "modules": true, 10 | "jsx": true, 11 | "flow": true 12 | }, 13 | 14 | "plugins": [ 15 | "react" 16 | ], 17 | 18 | // Map from global var to bool specifying if it can be redefined 19 | "globals": { 20 | "__DEV__": true, 21 | "__dirname": false, 22 | "__fbBatchedBridgeConfig": false, 23 | "cancelAnimationFrame": false, 24 | "clearImmediate": true, 25 | "clearInterval": false, 26 | "clearTimeout": false, 27 | "console": false, 28 | "document": false, 29 | "escape": false, 30 | "exports": false, 31 | "fetch": false, 32 | "global": false, 33 | "jest": false, 34 | "Map": true, 35 | "module": false, 36 | "navigator": false, 37 | "process": false, 38 | "Promise": true, 39 | "requestAnimationFrame": true, 40 | "require": false, 41 | "Set": true, 42 | "setImmediate": true, 43 | "setInterval": false, 44 | "setTimeout": false, 45 | "window": false, 46 | "XMLHttpRequest": false, 47 | "alert": true, 48 | "pit": false 49 | }, 50 | 51 | "rules": { 52 | "comma-dangle": 0, // disallow trailing commas in object literals 53 | "no-cond-assign": 1, // disallow assignment in conditional expressions 54 | "no-console": 0, // disallow use of console (off by default in the node environment) 55 | "no-constant-condition": 0, // disallow use of constant expressions in conditions 56 | "no-control-regex": 1, // disallow control characters in regular expressions 57 | "no-debugger": 1, // disallow use of debugger 58 | "no-dupe-keys": 1, // disallow duplicate keys when creating object literals 59 | "no-empty": 0, // disallow empty statements 60 | "no-ex-assign": 1, // disallow assigning to the exception in a catch block 61 | "no-extra-boolean-cast": 1, // disallow double-negation boolean casts in a boolean context 62 | "no-extra-parens": 0, // disallow unnecessary parentheses (off by default) 63 | "no-extra-semi": 1, // disallow unnecessary semicolons 64 | "no-func-assign": 1, // disallow overwriting functions written as function declarations 65 | "no-inner-declarations": 0, // disallow function or variable declarations in nested blocks 66 | "no-invalid-regexp": 1, // disallow invalid regular expression strings in the RegExp constructor 67 | "no-negated-in-lhs": 1, // disallow negation of the left operand of an in expression 68 | "no-obj-calls": 1, // disallow the use of object properties of the global object (Math and JSON) as functions 69 | "no-regex-spaces": 1, // disallow multiple spaces in a regular expression literal 70 | "no-reserved-keys": 0, // disallow reserved words being used as object literal keys (off by default) 71 | "no-sparse-arrays": 1, // disallow sparse arrays 72 | "no-unreachable": 1, // disallow unreachable statements after a return, throw, continue, or break statement 73 | "use-isnan": 1, // disallow comparisons with the value NaN 74 | "valid-jsdoc": 0, // Ensure JSDoc comments are valid (off by default) 75 | "valid-typeof": 1, // Ensure that the results of typeof are compared against a valid string 76 | 77 | // Best Practices 78 | // These are rules designed to prevent you from making mistakes. They either prescribe a better way of doing something or help you avoid footguns. 79 | 80 | "block-scoped-var": 0, // treat var statements as if they were block scoped (off by default) 81 | "complexity": 0, // specify the maximum cyclomatic complexity allowed in a program (off by default) 82 | "consistent-return": 0, // require return statements to either always or never specify values 83 | "curly": 1, // specify curly brace conventions for all control statements 84 | "default-case": 0, // require default case in switch statements (off by default) 85 | "dot-notation": 1, // encourages use of dot notation whenever possible 86 | "eqeqeq": 1, // require the use of === and !== 87 | "guard-for-in": 0, // make sure for-in loops have an if statement (off by default) 88 | "no-alert": 0, // disallow the use of alert, confirm, and prompt 89 | "no-caller": 1, // disallow use of arguments.caller or arguments.callee 90 | "no-div-regex": 1, // disallow division operators explicitly at beginning of regular expression (off by default) 91 | "no-else-return": 0, // disallow else after a return in an if (off by default) 92 | "no-eq-null": 0, // disallow comparisons to null without a type-checking operator (off by default) 93 | "no-eval": 1, // disallow use of eval() 94 | "no-extend-native": 1, // disallow adding to native types 95 | "no-extra-bind": 1, // disallow unnecessary function binding 96 | "no-fallthrough": 1, // disallow fallthrough of case statements 97 | "no-floating-decimal": 1, // disallow the use of leading or trailing decimal points in numeric literals (off by default) 98 | "no-implied-eval": 1, // disallow use of eval()-like methods 99 | "no-labels": 1, // disallow use of labeled statements 100 | "no-iterator": 1, // disallow usage of __iterator__ property 101 | "no-lone-blocks": 1, // disallow unnecessary nested blocks 102 | "no-loop-func": 0, // disallow creation of functions within loops 103 | "no-multi-str": 0, // disallow use of multiline strings 104 | "no-native-reassign": 0, // disallow reassignments of native objects 105 | "no-new": 1, // disallow use of new operator when not part of the assignment or comparison 106 | "no-new-func": 1, // disallow use of new operator for Function object 107 | "no-new-wrappers": 1, // disallows creating new instances of String,Number, and Boolean 108 | "no-octal": 1, // disallow use of octal literals 109 | "no-octal-escape": 1, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251"; 110 | "no-proto": 1, // disallow usage of __proto__ property 111 | "no-redeclare": 0, // disallow declaring the same variable more then once 112 | "no-return-assign": 1, // disallow use of assignment in return statement 113 | "no-script-url": 1, // disallow use of javascript: urls. 114 | "no-self-compare": 1, // disallow comparisons where both sides are exactly the same (off by default) 115 | "no-sequences": 1, // disallow use of comma operator 116 | "no-unused-expressions": 0, // disallow usage of expressions in statement position 117 | "no-void": 1, // disallow use of void operator (off by default) 118 | "no-warning-comments": 0, // disallow usage of configurable warning terms in comments": 1, // e.g. TODO or FIXME (off by default) 119 | "no-with": 1, // disallow use of the with statement 120 | "radix": 1, // require use of the second argument for parseInt() (off by default) 121 | "semi-spacing": 1, // require a space after a semi-colon 122 | "vars-on-top": 0, // requires to declare all vars on top of their containing scope (off by default) 123 | "wrap-iife": 0, // require immediate function invocation to be wrapped in parentheses (off by default) 124 | "yoda": 1, // require or disallow Yoda conditions 125 | 126 | // Strict Mode 127 | // These rules relate to using strict mode. 128 | 129 | //"no-extra-strict": 1, // disallow unnecessary use of "use strict"; when already in strict mode 130 | "strict": 0, // require that all functions are run in strict mode 131 | 132 | // Variables 133 | // These rules have to do with variable declarations. 134 | 135 | "no-catch-shadow": 1, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment) 136 | "no-delete-var": 1, // disallow deletion of variables 137 | "no-label-var": 1, // disallow labels that share a name with a variable 138 | "no-shadow": 1, // disallow declaration of variables already declared in the outer scope 139 | "no-shadow-restricted-names": 1, // disallow shadowing of names such as arguments 140 | "no-undef": 2, // disallow use of undeclared variables unless mentioned in a /*global */ block 141 | "no-undefined": 0, // disallow use of undefined variable (off by default) 142 | "no-undef-init": 1, // disallow use of undefined when initializing variables 143 | "no-unused-vars": [1, {"vars": "all", "args": "none"}], // disallow declaration of variables that are not used in the code 144 | "no-use-before-define": 0, // disallow use of variables before they are defined 145 | 146 | // Node.js 147 | // These rules are specific to JavaScript running on Node.js. 148 | 149 | "handle-callback-err": 1, // enforces error handling in callbacks (off by default) (on by default in the node environment) 150 | "no-mixed-requires": 1, // disallow mixing regular variable and require declarations (off by default) (on by default in the node environment) 151 | "no-new-require": 1, // disallow use of new operator with the require function (off by default) (on by default in the node environment) 152 | "no-path-concat": 1, // disallow string concatenation with __dirname and __filename (off by default) (on by default in the node environment) 153 | "no-process-exit": 0, // disallow process.exit() (on by default in the node environment) 154 | "no-restricted-modules": 1, // restrict usage of specified node modules (off by default) 155 | "no-sync": 0, // disallow use of synchronous methods (off by default) 156 | 157 | // Stylistic Issues 158 | // These rules are purely matters of style and are quite subjective. 159 | 160 | "key-spacing": 0, 161 | "comma-spacing": 0, 162 | "no-multi-spaces": 0, 163 | "brace-style": 0, // enforce one true brace style (off by default) 164 | "camelcase": 0, // require camel case names 165 | "consistent-this": [1, "self"], // enforces consistent naming when capturing the current execution context (off by default) 166 | "eol-last": 1, // enforce newline at the end of file, with no multiple empty lines 167 | "func-names": 0, // require function expressions to have a name (off by default) 168 | "func-style": 0, // enforces use of function declarations or expressions (off by default) 169 | "new-cap": 0, // require a capital letter for constructors 170 | "new-parens": 1, // disallow the omission of parentheses when invoking a constructor with no arguments 171 | "no-nested-ternary": 0, // disallow nested ternary expressions (off by default) 172 | "no-array-constructor": 1, // disallow use of the Array constructor 173 | "no-lonely-if": 0, // disallow if as the only statement in an else block (off by default) 174 | "no-new-object": 1, // disallow use of the Object constructor 175 | "no-spaced-func": 1, // disallow space between function identifier and application 176 | //"no-space-before-semi": 1, // disallow space before semicolon 177 | "no-ternary": 0, // disallow the use of ternary operators (off by default) 178 | "no-trailing-spaces": 0, // disallow trailing whitespace at the end of lines 179 | "no-underscore-dangle": 0, // disallow dangling underscores in identifiers 180 | //"no-wrap-func": 1, // disallow wrapping of non-IIFE statements in parens 181 | "no-mixed-spaces-and-tabs": 1, // disallow mixed spaces and tabs for indentation 182 | "quotes": [1, "single", "avoid-escape"], // specify whether double or single quotes should be used 183 | "quote-props": 0, // require quotes around object literal property names (off by default) 184 | "semi": 1, // require or disallow use of semicolons instead of ASI 185 | "sort-vars": 0, // sort variables within the same declaration block (off by default) 186 | "keyword-spacing": 1, // require a space after certain keywords (off by default) 187 | "space-in-brackets": 0, // require or disallow spaces inside brackets (off by default) 188 | "space-in-parens": 0, // require or disallow spaces inside parentheses (off by default) 189 | "space-infix-ops": 1, // require spaces around operators 190 | "space-unary-ops": [1, { "words": true, "nonwords": false }], // require or disallow spaces before/after unary operators (words on by default, nonwords off by default) 191 | "max-nested-callbacks": 0, // specify the maximum depth callbacks can be nested (off by default) 192 | "one-var": 0, // allow just one var statement per function (off by default) 193 | "wrap-regex": 0, // require regex literals to be wrapped in parentheses (off by default) 194 | 195 | // Legacy 196 | // The following rules are included for compatibility with JSHint and JSLint. While the names of the rules may not match up with the JSHint/JSLint counterpart, the functionality is the same. 197 | 198 | "max-depth": 0, // specify the maximum depth that blocks can be nested (off by default) 199 | "max-len": 0, // specify the maximum length of a line in your program (off by default) 200 | "max-params": 0, // limits the number of parameters that can be used in the function declaration. (off by default) 201 | "max-statements": 0, // specify the maximum number of statement allowed in a function (off by default) 202 | "no-bitwise": 1, // disallow use of bitwise operators (off by default) 203 | "no-plusplus": 0, // disallow use of unary operators, ++ and -- (off by default) 204 | 205 | "react/display-name": 0, 206 | "react/jsx-boolean-value": 0, 207 | "react/jsx-no-undef": 1, 208 | "react/jsx-sort-props": 0, 209 | "react/jsx-uses-react": 1, 210 | "react/jsx-uses-vars": 1, 211 | "react/no-did-mount-set-state": 1, 212 | "react/no-did-update-set-state": 1, 213 | "react/no-multi-comp": 0, 214 | "react/no-unknown-property": 0, 215 | "react/prop-types": 0, 216 | "react/react-in-jsx-scope": 0, 217 | "react/self-closing-comp": 1, 218 | "react/wrap-multilines": 0 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata 16 | *.xccheckout 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | gradle 22 | project.xcworkspace 23 | 24 | # Android/IJ 25 | # 26 | .idea 27 | .gradle 28 | local.properties 29 | build 30 | 31 | # node.js 32 | # 33 | node_modules 34 | npm-debug.log 35 | 36 | 37 | # webstorm 38 | # 39 | *.iml 40 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | example/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015 shimo 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-offline-cache-webview 2 | 3 | ## Features 4 | Offline cache enabled WebView, works for Android and iOS. 5 | 6 | iOS | Android 7 | ------- | ---- 8 | | 9 | 10 | 11 | ## Install 12 | 13 | ```bash 14 | npm i react-native-offline-cache-webview --save 15 | react-native link react-native-offline-cache-webview 16 | ``` 17 | 18 | ## Usage 19 | ```javascript 20 | import OfflineCacheWebView from 'react-native-offline-cache-webview'; 21 | export default class webview extends Component { 22 | render() { 23 | return ( 24 | 28 | ); 29 | } 30 | } 31 | ``` 32 | ### Example 33 | * Check it [here](https://github.com/zhangtaii/react-native-offline-cache-webview/tree/master/example) 34 | 35 | 36 | ### How does this work? 37 | 38 | For iOS, the offline cache is implemented with `RNCachingURLProtocol` 39 | > **RNCachingURLProtocol** is a simple shim for the HTTP protocol (that’s not nearly as scary as it sounds). Anytime a URL is downloaded, the response is cached to disk. Anytime a URL is requested, if we’re online then things proceed normally. If we’re offline, then we retrieve the cached version. 40 | 41 | For Android, it's provided by `CacheWebView` 42 | > **CacheWebView** is a custom implement of Android WebView resource interceptor. It beyond system WebView cache space limit, let cache config more simple ,fast and flexible. Visit website by offline. 43 | 44 | This library is built on top of `react-native-advanced-webview`, the architecture leveraged for customizing *React Naitve* **WebView** 45 | ### TODO 46 | 47 | * `offlineCacheMode` property to control `on/off` for cache usage 48 | * `prefetch` method for caching url by advance 49 | * `clearCache` method 50 | 51 | 52 | ### References 53 | * [react-native-create-library](https://github.com/frostney/react-native-create-library) 54 | * [react-native-advanced-webview](https://github.com/magicismight/react-native-advanced-webview) 55 | * [CacheWebView](https://github.com/yale8848/CacheWebView) 56 | * [RNCachingURLProtocol](https://github.com/rnapier/RNCachingURLProtocol) 57 | -------------------------------------------------------------------------------- /WebView.android.js: -------------------------------------------------------------------------------- 1 | import React, { cloneElement } from 'react'; 2 | import { WebView, UIManager, requireNativeComponent } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | 5 | export default class extends WebView { 6 | 7 | static displayName = 'AdvancedWebView'; 8 | 9 | static propTypes = { 10 | ...WebView.propTypes, 11 | keyboardDisplayRequiresUserAction: PropTypes.bool, 12 | allowFileAccessFromFileURLs: PropTypes.bool, 13 | hideAccessory: PropTypes.bool, 14 | webviewDebugEnabledWhenDev: PropTypes.number 15 | }; 16 | 17 | goForward = () => { 18 | UIManager.dispatchViewManagerCommand( 19 | this.getWebViewHandle(), 20 | UIManager.RNAdvancedWebView.Commands.goForward, 21 | null 22 | ); 23 | }; 24 | 25 | goBack = () => { 26 | UIManager.dispatchViewManagerCommand( 27 | this.getWebViewHandle(), 28 | UIManager.RNAdvancedWebView.Commands.goBack, 29 | null 30 | ); 31 | }; 32 | 33 | reload = () => { 34 | UIManager.dispatchViewManagerCommand( 35 | this.getWebViewHandle(), 36 | UIManager.RNAdvancedWebView.Commands.reload, 37 | null 38 | ); 39 | }; 40 | 41 | stopLoading = () => { 42 | UIManager.dispatchViewManagerCommand( 43 | this.getWebViewHandle(), 44 | UIManager.RNAdvancedWebView.Commands.stopLoading, 45 | null 46 | ); 47 | }; 48 | 49 | postMessage = (data) => { 50 | UIManager.dispatchViewManagerCommand( 51 | this.getWebViewHandle(), 52 | UIManager.RNAdvancedWebView.Commands.postMessage, 53 | [String(data)] 54 | ); 55 | }; 56 | 57 | injectJavaScript = (data) => { 58 | UIManager.dispatchViewManagerCommand( 59 | this.getWebViewHandle(), 60 | UIManager.RNAdvancedWebView.Commands.injectJavaScript, 61 | [data] 62 | ); 63 | }; 64 | 65 | _onLoadingError = (event) => { 66 | event.persist(); // persist this event because we need to store it 67 | var {onError, onLoadEnd} = this.props; 68 | var result = onError && onError(event); 69 | onLoadEnd && onLoadEnd(event); 70 | console.warn('Encountered an error loading page', event.nativeEvent); 71 | 72 | result !== false && this.setState({ 73 | lastErrorEvent: event.nativeEvent, 74 | viewState: 'ERROR' 75 | }); 76 | }; 77 | 78 | onLoadingError = (event) => { 79 | this._onLoadingError(event) 80 | }; 81 | 82 | render() { 83 | const wrapper = super.render(); 84 | const [webview,...children] = wrapper.props.children; 85 | const { hideAccessory, allowFileAccessFromFileURLs, keyboardDisplayRequiresUserAction,webviewDebugEnabledWhenDev} = this.props; 86 | 87 | const advancedWebview = ( 88 | 96 | ); 97 | 98 | return cloneElement(wrapper, wrapper.props, advancedWebview, ...children); 99 | } 100 | } 101 | 102 | const RNAdvancedWebView = requireNativeComponent('RNAdvancedWebView', null, { 103 | nativeOnly: { 104 | allowFileAccessFromFileURLs: true, 105 | hideAccessory: true, 106 | keyboardDisplayRequiresUserAction: true, 107 | webviewDebugEnabledWhenDev: true 108 | } 109 | }) 110 | -------------------------------------------------------------------------------- /WebView.ios.js: -------------------------------------------------------------------------------- 1 | import React, { cloneElement } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { WebView, UIManager, requireNativeComponent } from 'react-native'; 4 | 5 | export default class extends WebView { 6 | 7 | static displayName = 'AdvancedWebView'; 8 | 9 | static propTypes = { 10 | ...WebView.propTypes, 11 | keyboardDisplayRequiresUserAction: PropTypes.bool, 12 | allowFileAccessFromFileURLs: PropTypes.bool, 13 | hideAccessory: PropTypes.bool, 14 | validSchemes: PropTypes.array, 15 | disableKeyboardAdjust: PropTypes.bool, 16 | contentInsetAdjustmentBehavior: PropTypes.number, 17 | userAgent: PropTypes.string 18 | }; 19 | 20 | goForward = () => { 21 | UIManager.dispatchViewManagerCommand( 22 | this.getWebViewHandle(), 23 | UIManager.RNAdvancedWebView.Commands.goForward, 24 | null 25 | ); 26 | }; 27 | 28 | goBack = () => { 29 | UIManager.dispatchViewManagerCommand( 30 | this.getWebViewHandle(), 31 | UIManager.RNAdvancedWebView.Commands.goBack, 32 | null 33 | ); 34 | }; 35 | 36 | reload = () => { 37 | UIManager.dispatchViewManagerCommand( 38 | this.getWebViewHandle(), 39 | UIManager.RNAdvancedWebView.Commands.reload, 40 | null 41 | ); 42 | }; 43 | 44 | stopLoading = () => { 45 | UIManager.dispatchViewManagerCommand( 46 | this.getWebViewHandle(), 47 | UIManager.RNAdvancedWebView.Commands.stopLoading, 48 | null 49 | ); 50 | }; 51 | 52 | postMessage = (data) => { 53 | UIManager.dispatchViewManagerCommand( 54 | this.getWebViewHandle(), 55 | UIManager.RNAdvancedWebView.Commands.postMessage, 56 | [String(data)] 57 | ); 58 | }; 59 | 60 | injectJavaScript = (data) => { 61 | UIManager.dispatchViewManagerCommand( 62 | this.getWebViewHandle(), 63 | UIManager.RNAdvancedWebView.Commands.injectJavaScript, 64 | [data] 65 | ); 66 | }; 67 | 68 | _onLoadingError = (event) => { 69 | event.persist(); // persist this event because we need to store it 70 | var { onError, onLoadEnd } = this.props; 71 | var result = onError && onError(event); 72 | onLoadEnd && onLoadEnd(event); 73 | console.warn('Encountered an error loading page', event.nativeEvent); 74 | 75 | result !== false && this.setState({ 76 | lastErrorEvent: event.nativeEvent, 77 | viewState: 'ERROR' 78 | }); 79 | }; 80 | 81 | onLoadingError = (event) => { 82 | this._onLoadingError(event); 83 | }; 84 | 85 | render() { 86 | const wrapper = super.render(); 87 | const [webview, ...children] = wrapper.props.children; 88 | const { hideAccessory, allowFileAccessFromFileURLs, keyboardDisplayRequiresUserAction, 89 | disableKeyboardAdjust, contentInsetAdjustmentBehavior, userAgent } = this.props; 90 | 91 | const advancedWebview = ( 92 | 102 | ); 103 | 104 | return cloneElement(wrapper, wrapper.props, advancedWebview, ...children); 105 | } 106 | } 107 | 108 | const RNAdvancedWebView = requireNativeComponent('RNAdvancedWebView', null, { 109 | nativeOnly: { 110 | allowFileAccessFromFileURLs: true, 111 | hideAccessory: true, 112 | keyboardDisplayRequiresUserAction: true, 113 | contentInsetAdjustmentBehavior: true, 114 | userAgent: true 115 | } 116 | }) 117 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:2.3.3' 8 | } 9 | } 10 | 11 | apply plugin: 'com.android.library' 12 | 13 | android { 14 | compileSdkVersion = 26 15 | buildToolsVersion = "26.0.3" 16 | 17 | defaultConfig { 18 | minSdkVersion 16 19 | targetSdkVersion 26 20 | versionCode 1 21 | versionName "1.0" 22 | } 23 | lintOptions { 24 | abortOnError false 25 | } 26 | } 27 | 28 | repositories { 29 | mavenLocal() 30 | jcenter() 31 | maven { 32 | 33 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 34 | url "$projectDir/../../../node_modules/react-native/android" 35 | 36 | } 37 | maven { 38 | url "https://dl.bintray.com/yale8848/maven/" 39 | } 40 | } 41 | 42 | dependencies { 43 | compile 'com.facebook.react:react-native:+' 44 | compile 'ren.yale.android:cachewebviewlib:2.0.2' 45 | compile fileTree( dir: "libs", includes: ['*.jar'] ) 46 | } 47 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/java/im/shimo/react/webview/AdvancedWebViewManager.java: -------------------------------------------------------------------------------- 1 | package im.shimo.react.webview; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.support.annotation.Nullable; 7 | import android.text.InputType; 8 | import android.view.ActionMode; 9 | import android.view.KeyEvent; 10 | import android.view.Menu; 11 | import android.view.MenuItem; 12 | import android.view.ViewGroup; 13 | import android.view.inputmethod.BaseInputConnection; 14 | import android.view.inputmethod.EditorInfo; 15 | import android.view.inputmethod.InputConnection; 16 | import android.view.inputmethod.InputConnectionWrapper; 17 | import android.view.inputmethod.InputMethodManager; 18 | import android.webkit.GeolocationPermissions; 19 | import android.webkit.JavascriptInterface; 20 | import android.webkit.WebChromeClient; 21 | import android.webkit.WebResourceRequest; 22 | import android.webkit.WebResourceResponse; 23 | import android.webkit.WebSettings; 24 | import android.webkit.WebView; 25 | import android.webkit.WebViewClient; 26 | 27 | import com.facebook.react.bridge.Arguments; 28 | import com.facebook.react.bridge.ReactMethod; 29 | import com.facebook.react.bridge.ReadableArray; 30 | import com.facebook.react.bridge.ReadableMap; 31 | import com.facebook.react.bridge.ReadableMapKeySetIterator; 32 | import com.facebook.react.bridge.WritableMap; 33 | import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; 34 | import com.facebook.react.uimanager.NativeViewHierarchyManager; 35 | import com.facebook.react.uimanager.ThemedReactContext; 36 | import com.facebook.react.uimanager.UIBlock; 37 | import com.facebook.react.uimanager.UIManagerModule; 38 | import com.facebook.react.uimanager.annotations.ReactProp; 39 | import com.facebook.react.views.view.ReactViewGroup; 40 | import com.facebook.react.views.webview.ReactWebViewManager; 41 | import com.facebook.react.views.webview.WebViewConfig; 42 | 43 | import org.json.JSONException; 44 | import org.json.JSONObject; 45 | 46 | import java.util.ArrayList; 47 | import java.util.LinkedHashMap; 48 | import java.util.LinkedList; 49 | 50 | import ren.yale.android.cachewebviewlib.WebViewCacheInterceptorInst; 51 | 52 | 53 | 54 | 55 | public class AdvancedWebViewManager extends ReactWebViewManager { 56 | 57 | private static final String REACT_CLASS = "RNAdvancedWebView"; 58 | private static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE"; 59 | 60 | /** 61 | * 便于调用销毁方法 62 | */ 63 | private static AdvancedWebViewManager INSTANCE; 64 | /** 65 | * 存储从一个webview内新启的另一些webview,按顺序存储 66 | */ 67 | private LinkedList mWebviews; 68 | 69 | private static final String URL_A = "javascript:" + 70 | "(function () {" + 71 | " if (window.originalPostMessage) {return;}" + 72 | " window.originalPostMessage = window.postMessage," + 73 | " window.postMessage = function(data) {"; 74 | private static final String URL_B = ".postMessage(String(data));" + 75 | " };" + 76 | " document.dispatchEvent(new CustomEvent('ReactNativeContextReady'));" + 77 | "})()"; 78 | private static String URL_KEYBOARD_A = "javascript:" + 79 | "(function () {" + 80 | " function isDescendant(parent, child) {" + 81 | " var node = child.parentNode;" + 82 | " while (node) {" + 83 | " if (node == parent) {" + 84 | " return true;" + 85 | " }" + 86 | " node = node.parentNode;" + 87 | " }" + 88 | " return false;" + 89 | " }" + 90 | " var focus = HTMLElement.prototype.focus;" + 91 | " HTMLElement.prototype.focus = function() {" + 92 | " focus.call(this);" + 93 | " var selection = document.getSelection();" + 94 | " var anchorNode = selection && selection.anchorNode;" + 95 | " if (document.activeElement !== document.body && anchorNode && (isDescendant(document.activeElement, anchorNode) || this === anchorNode)) {"; 96 | private static String URL_KEYBOARD_B = ".showKeyboard();" + // Show soft input manually, can't show soft input via javascript 97 | " }" + 98 | " };" + 99 | " var blur = HTMLElement.prototype.blur;" + 100 | " HTMLElement.prototype.blur = function() {" + 101 | " if (isDescendant(document.activeElement, this) || this === document.activeElement) {"; 102 | private static String URL_KEYBOARD_C = ".hideKeyboard();" + 103 | " }" + 104 | " blur.call(this);" + 105 | " };" + 106 | " document.dispatchEvent(new CustomEvent('ReactNativeContextReady'));" + 107 | "})()"; 108 | private LinkedHashMap mMenuIdTitles; 109 | private ArrayList mWhiteList; 110 | /** 111 | * 手动打开debug的标记,当遇到prop设置时,以prop设置为准 112 | */ 113 | public static boolean DEBUG_MODEL = false; 114 | /** 115 | * prop设置参数 116 | */ 117 | private int mEnvValue; 118 | 119 | public AdvancedWebViewManager() { 120 | super(); 121 | mWebViewConfig = new WebViewConfig() { 122 | public void configWebView(WebView webView) { 123 | } 124 | }; 125 | mWebviews = new LinkedList<>(); 126 | INSTANCE = this; 127 | } 128 | 129 | public static AdvancedWebViewManager getInstance() { 130 | return SingHolder.INSTANCE; 131 | } 132 | 133 | public LinkedHashMap getMenuIdTitles() { 134 | return mMenuIdTitles; 135 | } 136 | 137 | public ArrayList getMenuIdTitlesWhiteList() { 138 | return mWhiteList; 139 | } 140 | 141 | private static class SingHolder { 142 | private static final AdvancedWebViewManager INSTANCE = AdvancedWebViewManager.INSTANCE; 143 | } 144 | 145 | 146 | @SuppressLint("ViewConstructor") 147 | protected static class AdvancedWebView extends ReactWebView { 148 | private static final String ACTION_MENU_SELECTED = "actionMenuSelected"; 149 | private final RCTDeviceEventEmitter mEventEmitter; 150 | private ActionMode mActionMode; 151 | private boolean mMessagingEnabled = false; 152 | private boolean mKeyboardDisplayRequiresUserAction = false; 153 | private InputMethodManager mInputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 154 | private UIManagerModule mNativeModule; 155 | 156 | public AdvancedWebView(ThemedReactContext reactContext) { 157 | super(reactContext); 158 | mNativeModule = reactContext.getNativeModule(UIManagerModule.class); 159 | mEventEmitter = reactContext.getJSModule(RCTDeviceEventEmitter.class); 160 | } 161 | 162 | public WebViewClient getWebViewClient() { 163 | return mReactWebViewClient; 164 | } 165 | 166 | /** 167 | * 解决4.4以下图片无法删除的bug,其实就是把键盘的删除事件拦截以后自己处理 168 | * 169 | * @param outAttrs 170 | * @return 171 | */ 172 | @Override 173 | public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 174 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { 175 | outAttrs.actionLabel = null; 176 | outAttrs.inputType = InputType.TYPE_NULL; 177 | final InputConnection baseConnet = new BaseInputConnection(this, false); 178 | final InputConnection orignalConnet = super.onCreateInputConnection(outAttrs); 179 | if(orignalConnet!=null) { 180 | InputConnectionWrapper inputConnectionWrapper = new InputConnectionWrapper( 181 | super.onCreateInputConnection(outAttrs), true) { 182 | @Override 183 | public boolean deleteSurroundingText(int beforeLength, int afterLength) { 184 | if (beforeLength == 1 && afterLength == 0) { 185 | return this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) 186 | && this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)); 187 | } 188 | return super.deleteSurroundingText(beforeLength, afterLength); 189 | } 190 | 191 | @Override 192 | public boolean sendKeyEvent(KeyEvent event) { 193 | if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { 194 | return baseConnet.sendKeyEvent(event); 195 | } else { 196 | return super.sendKeyEvent(event); 197 | } 198 | } 199 | 200 | @Override 201 | public boolean finishComposingText() { 202 | try { 203 | return super.finishComposingText(); 204 | } catch (Exception e) { 205 | return true; 206 | } finally { 207 | if (baseConnet != null) { 208 | return baseConnet.finishComposingText(); 209 | } 210 | } 211 | } 212 | }; 213 | return inputConnectionWrapper; 214 | } else { 215 | return orignalConnet; 216 | } 217 | } else { 218 | return super.onCreateInputConnection(outAttrs); 219 | } 220 | } 221 | 222 | private class ReactWebViewBridge { 223 | ReactWebView mContext; 224 | 225 | ReactWebViewBridge(ReactWebView c) { 226 | mContext = c; 227 | } 228 | 229 | @JavascriptInterface 230 | public void postMessage(String message) { 231 | mContext.onMessage(message); 232 | } 233 | 234 | @JavascriptInterface 235 | public void showKeyboard() { 236 | mNativeModule.addUIBlock(new UIBlock() { 237 | @Override 238 | public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) { 239 | AdvancedWebView.this.requestFocus(); 240 | mInputMethodManager.showSoftInput(AdvancedWebView.this, InputMethodManager.SHOW_IMPLICIT); 241 | } 242 | }); 243 | } 244 | 245 | @JavascriptInterface 246 | public void hideKeyboard() { 247 | mNativeModule.addUIBlock(new UIBlock() { 248 | @Override 249 | public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) { 250 | AdvancedWebView.this.requestFocus(); 251 | mInputMethodManager.hideSoftInputFromWindow(AdvancedWebView.this.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); 252 | } 253 | }); 254 | 255 | } 256 | } 257 | 258 | public void setKeyboardDisplayRequiresUserAction(boolean keyboardDisplayRequiresUserAction) { 259 | mKeyboardDisplayRequiresUserAction = keyboardDisplayRequiresUserAction; 260 | } 261 | 262 | 263 | @Override 264 | public void setMessagingEnabled(boolean enabled) { 265 | if (mMessagingEnabled == enabled) { 266 | return; 267 | } 268 | 269 | mMessagingEnabled = enabled; 270 | if (enabled) { 271 | addJavascriptInterface(new AdvancedWebView.ReactWebViewBridge(this), BRIDGE_NAME); 272 | } else { 273 | removeJavascriptInterface(BRIDGE_NAME); 274 | } 275 | } 276 | 277 | 278 | @Override 279 | public void linkBridge() { 280 | if (getOriginalUrl().equals(BLANK_URL)) { 281 | return; 282 | } 283 | if (mMessagingEnabled) { 284 | loadUrl(URL_A + BRIDGE_NAME + URL_B); 285 | } 286 | if (!mKeyboardDisplayRequiresUserAction) { 287 | loadUrl(URL_KEYBOARD_A + BRIDGE_NAME + URL_KEYBOARD_B + BRIDGE_NAME + URL_KEYBOARD_C); 288 | } 289 | } 290 | 291 | 292 | /** 293 | * 是否显示在界面上 294 | */ 295 | private int mVisibility = -1; 296 | 297 | 298 | @Override 299 | protected void onWindowVisibilityChanged(int visibility) { 300 | super.onWindowVisibilityChanged(visibility); 301 | if (visibility != mVisibility) { 302 | if (visibility == VISIBLE) { 303 | //切入前台恢复使用cpu 304 | onResume(); 305 | resumeTimers(); 306 | getSettings().setJavaScriptEnabled(true); 307 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 308 | if (getInstance().mEnvValue == 1 || DEBUG_MODEL || BuildConfig.DEBUG) { 309 | WebView.setWebContentsDebuggingEnabled(true); 310 | } else { 311 | WebView.setWebContentsDebuggingEnabled(false); 312 | } 313 | } 314 | } else { 315 | //切入后台释放cpu 316 | onPause(); 317 | pauseTimers(); 318 | getSettings().setJavaScriptEnabled(false); 319 | } 320 | mVisibility = visibility; 321 | } 322 | } 323 | 324 | @Override 325 | public ActionMode startActionMode(ActionMode.Callback callback) { 326 | ActionMode actionMode = super.startActionMode(callback); 327 | if (getInstance().getMenuIdTitles() == null && getInstance().getMenuIdTitlesWhiteList() == null) { 328 | //既没有设置自定义item也没有设置白名单就不用处理 329 | return actionMode; 330 | } 331 | return resolveActionMode(actionMode); 332 | } 333 | 334 | @Override 335 | public ActionMode startActionMode(ActionMode.Callback callback, int type) { 336 | ActionMode actionMode = super.startActionMode(callback, type); 337 | if (getInstance().getMenuIdTitles() == null && getInstance().getMenuIdTitlesWhiteList() == null) { 338 | //既没有设置自定义item也没有设置白名单就不用处理 339 | return actionMode; 340 | } 341 | return resolveActionMode(actionMode); 342 | } 343 | 344 | /** 345 | * 处理item,处理点击 346 | * 347 | * @param actionMode 348 | */ 349 | private ActionMode resolveActionMode(ActionMode actionMode) { 350 | if (actionMode != null) { 351 | final Menu menu = actionMode.getMenu(); 352 | //删除系统自带item 353 | deleteOtherItem(actionMode); 354 | //配置点击事件 355 | configMenuItem(menu); 356 | } 357 | mActionMode = actionMode; 358 | return actionMode; 359 | } 360 | 361 | private void deleteOtherItem(ActionMode actionMode) { 362 | ArrayList whiteLists = getInstance().getMenuIdTitlesWhiteList(); 363 | Menu oldMenu = actionMode.getMenu(); 364 | if (whiteLists == null || whiteLists.isEmpty()) { 365 | //没有设置白名单或者白名单本身就是空数据,系统的全删除 366 | oldMenu.clear(); 367 | } else { 368 | for (int i = 0; i < oldMenu.size(); i++) { 369 | MenuItem item = oldMenu.getItem(i); 370 | String titleStr = item.getTitle().toString(); 371 | if (isToDelete(whiteLists, titleStr)) { 372 | oldMenu.removeItem(item.getItemId()); 373 | i--; 374 | } 375 | } 376 | } 377 | } 378 | 379 | private void configMenuItem(Menu menu) { 380 | int index = 0; 381 | LinkedHashMap menuIdTitles = getInstance().getMenuIdTitles(); 382 | if (menuIdTitles == null || menuIdTitles.isEmpty()) { 383 | //没有添加自定义item 384 | return; 385 | } 386 | for (Integer id : menuIdTitles.keySet()) { 387 | menu.add(0, id, index, menuIdTitles.get(id)); 388 | MenuItem menuItem = menu.getItem(index); 389 | menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { 390 | @Override 391 | public boolean onMenuItemClick(MenuItem item) { 392 | //事件传递 393 | sendEvent(item); 394 | releaseAction(); 395 | return true; 396 | } 397 | }); 398 | index++; 399 | } 400 | } 401 | 402 | 403 | /** 404 | * 白名单的保留 405 | * 406 | * @param whiteLists 407 | * @param titleStr 408 | */ 409 | private boolean isToDelete(ArrayList whiteLists, String titleStr) { 410 | if (whiteLists.indexOf(titleStr) >= 0) { 411 | return false; 412 | } else { 413 | return true; 414 | } 415 | } 416 | 417 | private void sendEvent(MenuItem menuItem) { 418 | WritableMap params = Arguments.createMap(); 419 | params.putString("menuId", String.valueOf(menuItem.getItemId())); 420 | params.putString("menuTitle", menuItem.getTitle().toString()); 421 | mEventEmitter.emit(ACTION_MENU_SELECTED, params); 422 | } 423 | 424 | /** 425 | * 隐藏消失Action 426 | */ 427 | private void releaseAction() { 428 | if (mActionMode != null) { 429 | mActionMode.finish(); 430 | mActionMode = null; 431 | } 432 | } 433 | 434 | } 435 | 436 | @ReactProp(name = "webviewDebugEnabledWhenDev", defaultInt = 0) 437 | public void setWebviewDebugModel(WebView view, int env) { 438 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 439 | if (env == 1) { 440 | WebView.setWebContentsDebuggingEnabled(true); 441 | DEBUG_MODEL = true; 442 | } else { 443 | if (mEnvValue == 1) { 444 | //从Dev环境切回别的环境,这个值有可能会变化 445 | WebView.setWebContentsDebuggingEnabled(false); 446 | DEBUG_MODEL = false; 447 | } 448 | } 449 | } 450 | mEnvValue = env; 451 | } 452 | 453 | 454 | /** 455 | * 此方法在退出web界面时或回退到上一个webview界面时调用,应用程序在前台显示 456 | * 457 | * @param webView 458 | */ 459 | @Override 460 | public void onDropViewInstance(WebView webView) { 461 | if (mWebviews.size() == 1) { 462 | //全部退出了,只剩下默认webview,休眠并保留它 463 | dumpWebView(webView); 464 | } else if (mWebviews.size() > 1) { 465 | //多页面覆盖方式,所以需要把默认的角标从最上层移至下一层 466 | //先取出最上层 467 | mWebviews.remove(webView); 468 | //将最上层销毁 469 | dumpWebView(webView); 470 | ((AdvancedWebViewClient) ((AdvancedWebView) webView).getWebViewClient()).mPendingMessages = new ArrayList<>(); 471 | super.onDropViewInstance(webView); 472 | //设置下一层为默认webview 473 | //唤醒下一层 474 | resumeWebview(mWebviews.getLast()); 475 | } else { 476 | dumpWebView(webView); 477 | super.onDropViewInstance(webView); 478 | } 479 | } 480 | 481 | @SuppressLint("SetJavaScriptEnabled") 482 | private void resumeWebview(WebView webView) { 483 | webView.onResume(); 484 | webView.resumeTimers(); 485 | webView.getSettings().setJavaScriptEnabled(true); 486 | } 487 | 488 | private void pauseBefores() { 489 | for (int i = 0; i < mWebviews.size(); i++) { 490 | final WebView view = mWebviews.get(i); 491 | view.onPause(); 492 | view.getSettings().setJavaScriptEnabled(false); 493 | } 494 | } 495 | 496 | 497 | /** 498 | * 此方法在程序销毁时调用 499 | */ 500 | public static void webviewOnDestroy() { 501 | if (getInstance() != null) { 502 | if (getInstance().mWebviews != null && !getInstance().mWebviews.isEmpty()) { 503 | for (int i = 0; i < getInstance().mWebviews.size(); i++) { 504 | final WebView webView = getInstance().mWebviews.get(i); 505 | getInstance().dumpWebView(webView); 506 | webView.destroy(); 507 | } 508 | getInstance().mWebviews.clear(); 509 | } 510 | INSTANCE = null; 511 | } 512 | } 513 | 514 | /** 515 | * 父类已经做了clear和destroy操作,这里不必重复 516 | * 517 | * @param webView 518 | */ 519 | private void dumpWebView(WebView webView) { 520 | webView.stopLoading(); 521 | webView.getSettings().setJavaScriptEnabled(false); 522 | webView.clearCache(false); 523 | webView.clearHistory(); 524 | webView.loadUrl(BLANK_URL); 525 | webView.removeAllViews(); 526 | callParentDropMe(webView); 527 | webView.onPause(); 528 | webView.setWebChromeClient(null); 529 | webView.setWebViewClient(null); 530 | } 531 | 532 | 533 | /** 534 | * 把自己从绑定界面上移除掉 535 | * 536 | * @param webView 537 | * @return 538 | */ 539 | private void callParentDropMe(WebView webView) { 540 | final ReactViewGroup parent = (ReactViewGroup) webView.getParent(); 541 | if (parent != null) { 542 | parent.removeView(webView); 543 | } 544 | } 545 | 546 | @Override 547 | public String getName() { 548 | return REACT_CLASS; 549 | } 550 | 551 | 552 | /** 553 | * 创建 554 | * 555 | * @param reactContext 556 | * @return 557 | */ 558 | @SuppressLint("SetJavaScriptEnabled") 559 | @Override 560 | protected WebView createViewInstance(ThemedReactContext reactContext) { 561 | AdvancedWebView webView = null; 562 | int size = mWebviews.size(); 563 | if (size == 0) {//首次打开 564 | //新建webview 565 | webView = initWebview(reactContext); 566 | //将webview加入队列 567 | mWebviews.add(webView); 568 | } else if (size == 1 && mWebviews.get(0).getParent() == null) { 569 | //曾经打开过,再重新打开 570 | //将webview队列仅存的一个设置为默认 571 | //赋值 572 | webView = mWebviews.get(0); 573 | reConfigWebview(webView); 574 | resumeWebview(webView); 575 | } else if (mWebviews.getLast().getParent() != null) { 576 | //非首次打开,文档中开启文档 577 | //休眠其它的webview 578 | pauseBefores(); 579 | //新建webview 580 | webView = initWebview(reactContext); 581 | //设置它为默认 582 | //将webview加入队列 583 | mWebviews.add(webView); 584 | } 585 | if (webView == null) { 586 | //解决多次创建webview的bug,此处有个隐患,目前RN多次重复创建都会有销毁动作,但是未来 587 | //如果只有创建没有销毁动作,有可能会有多个界面 588 | webView = initWebview(reactContext); 589 | pauseBefores(); 590 | mWebviews.add(webView); 591 | } 592 | return webView; 593 | } 594 | 595 | private void reConfigWebview(AdvancedWebView webView) { 596 | webView.setWebChromeClient(new WebChromeClient() { 597 | @Override 598 | public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { 599 | callback.invoke(origin, true, false); 600 | } 601 | }); 602 | webView.setWebViewClient(new AdvancedWebViewClient()); 603 | } 604 | 605 | /** 606 | * 重置页面,解决第二次加载失败的bug 607 | * 608 | * @param webView 609 | */ 610 | private void resetPage(WebView webView) { 611 | // 禁用 JavaScript 防止有脚本阻止了页面的重置 612 | // 在重置页面之后不需要再次调用 .setJavaScriptEnabled(true) 613 | // 下个 ReactWebView 在初始化的时候会去主动调用父类的 setJavaScriptEnabled 614 | webView.stopLoading(); 615 | webView.getSettings().setJavaScriptEnabled(false); 616 | webView.loadUrl(BLANK_URL); 617 | } 618 | 619 | /** 620 | * 初始化webview实例,刷新document 621 | * 622 | * @param reactContext 623 | * @return 624 | */ 625 | @SuppressLint("SetJavaScriptEnabled") 626 | public AdvancedWebView initWebview(final ThemedReactContext reactContext) { 627 | AdvancedWebView webView = new AdvancedWebView(reactContext); 628 | webView.setWebChromeClient(new WebChromeClient() { 629 | @Override 630 | public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { 631 | callback.invoke(origin, true, false); 632 | } 633 | }); 634 | 635 | webView.getSettings().setBuiltInZoomControls(true); 636 | webView.getSettings().setDisplayZoomControls(false); 637 | webView.getSettings().setJavaScriptEnabled(true); 638 | webView.getSettings().setDomStorageEnabled(true); 639 | mWebViewConfig.configWebView(webView); 640 | reactContext.addLifecycleEventListener(webView); 641 | // Fixes broken full-screen modals/galleries due to body height being 0. 642 | webView.setLayoutParams( 643 | new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 644 | ViewGroup.LayoutParams.MATCH_PARENT)); 645 | 646 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 647 | if (mEnvValue == 1 || DEBUG_MODEL || BuildConfig.DEBUG) { 648 | WebView.setWebContentsDebuggingEnabled(true); 649 | } else { 650 | WebView.setWebContentsDebuggingEnabled(false); 651 | } 652 | } 653 | 654 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 655 | // chromium, enable hardware acceleration 656 | webView.setLayerType(WebView.LAYER_TYPE_HARDWARE, null); 657 | } else { 658 | // older android version, disable hardware acceleration 659 | webView.setLayerType(WebView.LAYER_TYPE_SOFTWARE, null); 660 | } 661 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 662 | webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); 663 | } 664 | return webView; 665 | } 666 | 667 | protected class AdvancedWebViewClient extends ReactWebViewClient { 668 | protected ArrayList mPendingMessages = new ArrayList<>(); 669 | protected volatile boolean mPageFinished = false; 670 | 671 | @Override 672 | public void onPageFinished(WebView webView, String url) { 673 | super.onPageFinished(webView, url); 674 | mPageFinished = true; 675 | AdvancedWebView reactWebView = (AdvancedWebView) webView; 676 | reactWebView.linkBridge(); 677 | 678 | for (String message : mPendingMessages) { 679 | webView.evaluateJavascript(message, null); 680 | } 681 | mPendingMessages.clear(); 682 | } 683 | 684 | @Override 685 | public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload) { 686 | if (isReload) { 687 | super.doUpdateVisitedHistory(webView, url, true); 688 | } 689 | } 690 | 691 | // WebViewCache Code 692 | @Override 693 | public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { 694 | return WebViewCacheInterceptorInst.getInstance().interceptRequest(request); 695 | } 696 | 697 | } 698 | 699 | @Override 700 | public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray args) { 701 | switch (commandId) { 702 | case COMMAND_GO_BACK: 703 | root.goBack(); 704 | break; 705 | case COMMAND_GO_FORWARD: 706 | root.goForward(); 707 | break; 708 | case COMMAND_RELOAD: 709 | root.reload(); 710 | break; 711 | case COMMAND_STOP_LOADING: 712 | root.stopLoading(); 713 | break; 714 | case COMMAND_POST_MESSAGE: 715 | try { 716 | JSONObject eventInitDict = new JSONObject(); 717 | eventInitDict.put("data", args.getString(0)); 718 | String message = "(function () {" + 719 | "var event;" + 720 | "var data = " + eventInitDict.toString() + ";" + 721 | "try {" + 722 | "event = new MessageEvent('message', data);" + 723 | "} catch (e) {" + 724 | "event = document.createEvent('MessageEvent');" + 725 | "event.initMessageEvent('message', true, true, data.data, data.origin, data.lastEventId, data.source);" + 726 | "}" + 727 | "document.dispatchEvent(event);" + 728 | "})();"; 729 | AdvancedWebViewClient webViewClient = (AdvancedWebViewClient)((AdvancedWebView) root).getWebViewClient(); 730 | if (webViewClient.mPageFinished) { 731 | root.evaluateJavascript(message, null); 732 | } else { 733 | webViewClient.mPendingMessages.add(message); 734 | } 735 | } catch (JSONException e) { 736 | throw new RuntimeException(e); 737 | } 738 | break; 739 | case COMMAND_INJECT_JAVASCRIPT: 740 | root.evaluateJavascript(args.getString(0), null); 741 | break; 742 | } 743 | } 744 | 745 | @Override 746 | protected void addEventEmitters(ThemedReactContext reactContext, WebView view) { 747 | // Do not register default touch emitter and let WebView implementation handle touches 748 | view.setWebViewClient(new AdvancedWebViewClient()); 749 | } 750 | 751 | @ReactProp(name = "allowFileAccessFromFileURLs") 752 | public void setAllowFileAccessFromFileURLs(WebView root, boolean allows) { 753 | root.getSettings().setAllowFileAccessFromFileURLs(allows); 754 | } 755 | 756 | @ReactProp(name = "keyboardDisplayRequiresUserAction") 757 | public void setKeyboardDisplayRequiresUserAction(WebView root, boolean keyboardDisplayRequiresUserAction) { 758 | ((AdvancedWebView) root).setKeyboardDisplayRequiresUserAction(keyboardDisplayRequiresUserAction); 759 | } 760 | 761 | /** 762 | * 焦点变化时调用(既没有设置自定义item也没有是设置白名单,即为不使用自定义ActionMode) 763 | *

764 | * 对页面小白条的初始化工作,存储顺序即为展现顺序,id必须唯一且最好用数字字符串,不要从10以内的数字开始 765 | * 766 | * @param idTitles key:每个item所对应的id,value:每个item的title 767 | */ 768 | @ReactMethod 769 | public void setActionModeMenu(ReadableMap idTitles) { 770 | if (mMenuIdTitles != null) { 771 | mMenuIdTitles.clear(); 772 | } 773 | if (idTitles == null) { 774 | return; 775 | } 776 | ReadableMapKeySetIterator it = idTitles.keySetIterator(); 777 | while (it.hasNextKey()) { 778 | String key = it.nextKey(); 779 | String title = idTitles.getString(key); 780 | int id = Integer.valueOf(key); 781 | mMenuIdTitles.put(id, title); 782 | } 783 | } 784 | 785 | /** 786 | * 焦点变化时调用,或者界面的规则一致时只调用一次即可(既没有设置自定义item也没有是设置白名单,即为不使用自定义ActionMode) 787 | *

788 | * 对页面小白条的初始化工作,加入进来的title,如果系统本身自带的title与之相同将不会被删除 789 | * 例如"复制",传入的列表内包含"复制",那么系统的将被保留,如果不包含,系统所带的"复制"将被删除 790 | * 791 | * @param titles 白名单列表 792 | */ 793 | @ReactMethod 794 | public void setActionModeMenuWhitelist(ReadableArray titles) { 795 | if (mWhiteList != null) { 796 | mWhiteList.clear(); 797 | } 798 | if (titles == null) { 799 | return; 800 | } 801 | for (Object title : 802 | titles.toArrayList()) { 803 | mWhiteList.add((String) title); 804 | } 805 | } 806 | } 807 | -------------------------------------------------------------------------------- /android/src/main/java/im/shimo/react/webview/AdvancedWebViewPackage.java: -------------------------------------------------------------------------------- 1 | package im.shimo.react.webview; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.JavaScriptModule; 5 | import com.facebook.react.bridge.NativeModule; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.uimanager.ViewManager; 8 | 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | import ren.yale.android.cachewebviewlib.WebViewCacheInterceptor; 14 | import ren.yale.android.cachewebviewlib.WebViewCacheInterceptorInst; 15 | import ren.yale.android.cachewebviewlib.config.CacheExtensionConfig; 16 | 17 | public class AdvancedWebViewPackage implements ReactPackage { 18 | @Override 19 | public List createNativeModules(ReactApplicationContext reactApplicationContext) { 20 | return Collections.emptyList(); 21 | } 22 | 23 | @Override 24 | public List createViewManagers(ReactApplicationContext reactApplicationContext) { 25 | //Initialize WebViewCacheInterceptor with config 26 | WebViewCacheInterceptorInst.getInstance().init(new WebViewCacheInterceptor.Builder(reactApplicationContext)); 27 | 28 | return Arrays.asList( 29 | new AdvancedWebViewManager() 30 | ); 31 | } 32 | 33 | public List> createJSModules() { 34 | return Collections.emptyList(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /example/.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /example/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | 16 | ; Ignore polyfills 17 | .*/Libraries/polyfills/.* 18 | 19 | ; Ignore metro 20 | .*/node_modules/metro/.* 21 | 22 | [include] 23 | 24 | [libs] 25 | node_modules/react-native/Libraries/react-native/react-native-interface.js 26 | node_modules/react-native/flow/ 27 | node_modules/react-native/flow-github/ 28 | 29 | [options] 30 | emoji=true 31 | 32 | module.system=haste 33 | module.system.haste.use_name_reducers=true 34 | # get basename 35 | module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1' 36 | # strip .js or .js.flow suffix 37 | module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1' 38 | # strip .ios suffix 39 | module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1' 40 | module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1' 41 | module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' 42 | module.system.haste.paths.blacklist=.*/__tests__/.* 43 | module.system.haste.paths.blacklist=.*/__mocks__/.* 44 | module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.* 45 | module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.* 46 | 47 | munge_underscores=true 48 | 49 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 50 | 51 | module.file_ext=.js 52 | module.file_ext=.jsx 53 | module.file_ext=.json 54 | module.file_ext=.native.js 55 | 56 | suppress_type=$FlowIssue 57 | suppress_type=$FlowFixMe 58 | suppress_type=$FlowFixMeProps 59 | suppress_type=$FlowFixMeState 60 | 61 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 62 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 63 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 64 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 65 | 66 | [version] 67 | ^0.75.0 68 | -------------------------------------------------------------------------------- /example/.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | yarn-error.log 38 | 39 | # BUCK 40 | buck-out/ 41 | \.buckd/ 42 | *.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | -------------------------------------------------------------------------------- /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /example/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { AppRegistry, StyleSheet, WebView } from 'react-native'; 3 | 4 | import OfflineCacheWebView from 'react-native-offline-cache-webview'; 5 | 6 | 7 | export default class webview extends Component { 8 | render() { 9 | 10 | return ( 11 | 16 | ); 17 | } 18 | } 19 | 20 | const styles = StyleSheet.create({ 21 | webview: { 22 | flex: 1 23 | } 24 | }); 25 | 26 | AppRegistry.registerComponent('webview', () => webview); 27 | -------------------------------------------------------------------------------- /example/android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | lib_deps = [] 12 | 13 | for jarfile in glob(['libs/*.jar']): 14 | name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')] 15 | lib_deps.append(':' + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | 21 | for aarfile in glob(['libs/*.aar']): 22 | name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')] 23 | lib_deps.append(':' + name) 24 | android_prebuilt_aar( 25 | name = name, 26 | aar = aarfile, 27 | ) 28 | 29 | android_library( 30 | name = "all-libs", 31 | exported_deps = lib_deps, 32 | ) 33 | 34 | android_library( 35 | name = "app-code", 36 | srcs = glob([ 37 | "src/main/java/**/*.java", 38 | ]), 39 | deps = [ 40 | ":all-libs", 41 | ":build_config", 42 | ":res", 43 | ], 44 | ) 45 | 46 | android_build_config( 47 | name = "build_config", 48 | package = "com.example", 49 | ) 50 | 51 | android_resource( 52 | name = "res", 53 | package = "com.example", 54 | res = "src/main/res", 55 | ) 56 | 57 | android_binary( 58 | name = "app", 59 | keystore = "//android/keystores:debug", 60 | manifest = "src/main/AndroidManifest.xml", 61 | package_type = "debug", 62 | deps = [ 63 | ":app-code", 64 | ], 65 | ) 66 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | import com.android.build.OutputFile 4 | 5 | /** 6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets 7 | * and bundleReleaseJsAndAssets). 8 | * These basically call `react-native bundle` with the correct arguments during the Android build 9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the 10 | * bundle directly from the development server. Below you can see all the possible configurations 11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the 12 | * `apply from: "../../node_modules/react-native/react.gradle"` line. 13 | * 14 | * project.ext.react = [ 15 | * // the name of the generated asset file containing your JS bundle 16 | * bundleAssetName: "index.android.bundle", 17 | * 18 | * // the entry file for bundle generation 19 | * entryFile: "index.android.js", 20 | * 21 | * // whether to bundle JS and assets in debug mode 22 | * bundleInDebug: false, 23 | * 24 | * // whether to bundle JS and assets in release mode 25 | * bundleInRelease: true, 26 | * 27 | * // whether to bundle JS and assets in another build variant (if configured). 28 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants 29 | * // The configuration property can be in the following formats 30 | * // 'bundleIn${productFlavor}${buildType}' 31 | * // 'bundleIn${buildType}' 32 | * // bundleInFreeDebug: true, 33 | * // bundleInPaidRelease: true, 34 | * // bundleInBeta: true, 35 | * 36 | * // whether to disable dev mode in custom build variants (by default only disabled in release) 37 | * // for example: to disable dev mode in the staging build type (if configured) 38 | * devDisabledInStaging: true, 39 | * // The configuration property can be in the following formats 40 | * // 'devDisabledIn${productFlavor}${buildType}' 41 | * // 'devDisabledIn${buildType}' 42 | * 43 | * // the root of your project, i.e. where "package.json" lives 44 | * root: "../../", 45 | * 46 | * // where to put the JS bundle asset in debug mode 47 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", 48 | * 49 | * // where to put the JS bundle asset in release mode 50 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release", 51 | * 52 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 53 | * // require('./image.png')), in debug mode 54 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", 55 | * 56 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 57 | * // require('./image.png')), in release mode 58 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", 59 | * 60 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means 61 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to 62 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle 63 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ 64 | * // for example, you might want to remove it from here. 65 | * inputExcludes: ["android/**", "ios/**"], 66 | * 67 | * // override which node gets called and with what additional arguments 68 | * nodeExecutableAndArgs: ["node"], 69 | * 70 | * // supply additional arguments to the packager 71 | * extraPackagerArgs: [] 72 | * ] 73 | */ 74 | 75 | project.ext.react = [ 76 | entryFile: "index.js" 77 | ] 78 | 79 | apply from: "../../node_modules/react-native/react.gradle" 80 | 81 | /** 82 | * Set this to true to create two separate APKs instead of one: 83 | * - An APK that only works on ARM devices 84 | * - An APK that only works on x86 devices 85 | * The advantage is the size of the APK is reduced by about 4MB. 86 | * Upload all the APKs to the Play Store and people will download 87 | * the correct one based on the CPU architecture of their device. 88 | */ 89 | def enableSeparateBuildPerCPUArchitecture = false 90 | 91 | /** 92 | * Run Proguard to shrink the Java bytecode in release builds. 93 | */ 94 | def enableProguardInReleaseBuilds = false 95 | 96 | android { 97 | compileSdkVersion rootProject.ext.compileSdkVersion 98 | buildToolsVersion rootProject.ext.buildToolsVersion 99 | 100 | defaultConfig { 101 | applicationId "com.example" 102 | minSdkVersion rootProject.ext.minSdkVersion 103 | targetSdkVersion rootProject.ext.targetSdkVersion 104 | versionCode 1 105 | versionName "1.0" 106 | ndk { 107 | abiFilters "armeabi-v7a", "x86" 108 | } 109 | } 110 | splits { 111 | abi { 112 | reset() 113 | enable enableSeparateBuildPerCPUArchitecture 114 | universalApk false // If true, also generate a universal APK 115 | include "armeabi-v7a", "x86" 116 | } 117 | } 118 | buildTypes { 119 | release { 120 | minifyEnabled enableProguardInReleaseBuilds 121 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 122 | } 123 | } 124 | // applicationVariants are e.g. debug, release 125 | applicationVariants.all { variant -> 126 | variant.outputs.each { output -> 127 | // For each separate APK per architecture, set a unique version code as described here: 128 | // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits 129 | def versionCodes = ["armeabi-v7a":1, "x86":2] 130 | def abi = output.getFilter(OutputFile.ABI) 131 | if (abi != null) { // null for the universal-debug, universal-release variants 132 | output.versionCodeOverride = 133 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode 134 | } 135 | } 136 | } 137 | } 138 | 139 | dependencies { 140 | compile project(':react-native-offline-cache-webview') 141 | compile fileTree(dir: "libs", include: ["*.jar"]) 142 | compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" 143 | compile "com.facebook.react:react-native:+" // From node_modules 144 | } 145 | 146 | // Run this once to be able to run the application with BUCK 147 | // puts all compile dependencies into folder libs for BUCK to use 148 | task copyDownloadableDepsToLibs(type: Copy) { 149 | from configurations.compile 150 | into 'libs' 151 | } 152 | -------------------------------------------------------------------------------- /example/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. 9 | * This is used to schedule rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "example"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.react.ReactApplication; 6 | import im.shimo.react.webview.AdvancedWebViewPackage; 7 | import im.shimo.react.webview.AdvancedWebViewPackage; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.react.shell.MainReactPackage; 11 | import com.facebook.soloader.SoLoader; 12 | 13 | import java.util.Arrays; 14 | import java.util.List; 15 | 16 | public class MainApplication extends Application implements ReactApplication { 17 | 18 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 19 | @Override 20 | public boolean getUseDeveloperSupport() { 21 | return BuildConfig.DEBUG; 22 | } 23 | 24 | @Override 25 | protected List getPackages() { 26 | return Arrays.asList( 27 | new MainReactPackage(), 28 | new AdvancedWebViewPackage(), 29 | new AdvancedWebViewPackage() 30 | ); 31 | } 32 | 33 | @Override 34 | protected String getJSMainModuleName() { 35 | return "index"; 36 | } 37 | }; 38 | 39 | @Override 40 | public ReactNativeHost getReactNativeHost() { 41 | return mReactNativeHost; 42 | } 43 | 44 | @Override 45 | public void onCreate() { 46 | super.onCreate(); 47 | SoLoader.init(this, /* native exopackage */ false); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangtaii/react-native-offline-cache-webview/f754fb6304eef7c4004f40ec47093b894785f33e/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangtaii/react-native-offline-cache-webview/f754fb6304eef7c4004f40ec47093b894785f33e/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangtaii/react-native-offline-cache-webview/f754fb6304eef7c4004f40ec47093b894785f33e/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangtaii/react-native-offline-cache-webview/f754fb6304eef7c4004f40ec47093b894785f33e/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangtaii/react-native-offline-cache-webview/f754fb6304eef7c4004f40ec47093b894785f33e/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangtaii/react-native-offline-cache-webview/f754fb6304eef7c4004f40ec47093b894785f33e/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangtaii/react-native-offline-cache-webview/f754fb6304eef7c4004f40ec47093b894785f33e/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangtaii/react-native-offline-cache-webview/f754fb6304eef7c4004f40ec47093b894785f33e/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangtaii/react-native-offline-cache-webview/f754fb6304eef7c4004f40ec47093b894785f33e/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangtaii/react-native-offline-cache-webview/f754fb6304eef7c4004f40ec47093b894785f33e/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | example 3 | 4 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | maven { 7 | url 'https://maven.google.com/' 8 | name 'Google' 9 | } 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:2.3.3' 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | mavenLocal() 22 | jcenter() 23 | maven { 24 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 25 | url "$rootDir/../node_modules/react-native/android" 26 | } 27 | maven { 28 | url 'https://maven.google.com/' 29 | name 'Google' 30 | } 31 | } 32 | } 33 | 34 | ext { 35 | buildToolsVersion = "26.0.3" 36 | minSdkVersion = 16 37 | compileSdkVersion = 26 38 | targetSdkVersion = 26 39 | supportLibVersion = "26.1.0" 40 | } 41 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useDeprecatedNdk=true 21 | -------------------------------------------------------------------------------- /example/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /example/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /example/android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = "debug", 3 | properties = "debug.keystore.properties", 4 | store = "debug.keystore", 5 | visibility = [ 6 | "PUBLIC", 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /example/android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'example' 2 | include ':react-native-offline-cache-webview' 3 | project(':react-native-offline-cache-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-offline-cache-webview/android') 4 | include ':app' 5 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "displayName": "example" 4 | } -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import {AppRegistry} from 'react-native'; 4 | import App from './App'; 5 | import {name as appName} from './app.json'; 6 | 7 | AppRegistry.registerComponent(appName, () => App); 8 | -------------------------------------------------------------------------------- /example/ios/example-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UIViewControllerBasedStatusBarAppearance 38 | 39 | NSLocationWhenInUseUsageDescription 40 | 41 | NSAppTransportSecurity 42 | 43 | 44 | NSExceptionDomains 45 | 46 | localhost 47 | 48 | NSExceptionAllowsInsecureHTTPLoads 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /example/ios/example-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /example/ios/example.xcodeproj/xcshareddata/xcschemes/example-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /example/ios/example.xcodeproj/xcshareddata/xcschemes/example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /example/ios/example/AppDelegate.h: -------------------------------------------------------------------------------- 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 | #import 9 | 10 | @interface AppDelegate : UIResponder 11 | 12 | @property (nonatomic, strong) UIWindow *window; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /example/ios/example/AppDelegate.m: -------------------------------------------------------------------------------- 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 | #import "AppDelegate.h" 9 | 10 | #import 11 | #import 12 | 13 | @implementation AppDelegate 14 | 15 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 16 | { 17 | NSURL *jsCodeLocation; 18 | 19 | jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 20 | 21 | RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation 22 | moduleName:@"example" 23 | initialProperties:nil 24 | launchOptions:launchOptions]; 25 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 26 | 27 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 28 | UIViewController *rootViewController = [UIViewController new]; 29 | rootViewController.view = rootView; 30 | self.window.rootViewController = rootViewController; 31 | [self.window makeKeyAndVisible]; 32 | return YES; 33 | } 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /example/ios/example/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /example/ios/example/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /example/ios/example/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UIViewControllerBasedStatusBarAppearance 40 | 41 | NSLocationWhenInUseUsageDescription 42 | 43 | NSAppTransportSecurity 44 | 45 | 46 | NSExceptionDomains 47 | 48 | localhost 49 | 50 | NSExceptionAllowsInsecureHTTPLoads 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /example/ios/example/main.m: -------------------------------------------------------------------------------- 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 | #import 9 | 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/ios/exampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /example/ios/exampleTests/exampleTests.m: -------------------------------------------------------------------------------- 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 | #import 9 | #import 10 | 11 | #import 12 | #import 13 | 14 | #define TIMEOUT_SECONDS 600 15 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!" 16 | 17 | @interface exampleTests : XCTestCase 18 | 19 | @end 20 | 21 | @implementation exampleTests 22 | 23 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 24 | { 25 | if (test(view)) { 26 | return YES; 27 | } 28 | for (UIView *subview in [view subviews]) { 29 | if ([self findSubviewInView:subview matching:test]) { 30 | return YES; 31 | } 32 | } 33 | return NO; 34 | } 35 | 36 | - (void)testRendersWelcomeScreen 37 | { 38 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 39 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 40 | BOOL foundElement = NO; 41 | 42 | __block NSString *redboxError = nil; 43 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 44 | if (level >= RCTLogLevelError) { 45 | redboxError = message; 46 | } 47 | }); 48 | 49 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 50 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 51 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 52 | 53 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 54 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 55 | return YES; 56 | } 57 | return NO; 58 | }]; 59 | } 60 | 61 | RCTSetLogFunction(RCTDefaultLogFunction); 62 | 63 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 64 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 65 | } 66 | 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node node_modules/react-native/local-cli/cli.js start", 7 | "test": "jest" 8 | }, 9 | "dependencies": { 10 | "react": "16.4.1", 11 | "react-native": "0.56.0", 12 | "react-native-offline-cache-webview": "../" 13 | }, 14 | "devDependencies": { 15 | "babel-jest": "23.4.2", 16 | "babel-preset-react-native": "^5", 17 | "jest": "23.4.2", 18 | "react-test-renderer": "16.4.1" 19 | }, 20 | "jest": { 21 | "preset": "react-native" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/RNAdvancedWebView.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @class RNAdvancedWebView; 5 | 6 | /** 7 | * Special scheme used to pass messages to the injectedJavaScript 8 | * code without triggering a page load. Usage: 9 | * 10 | * window.location.href = RCTJSNavigationScheme + '://hello' 11 | */ 12 | extern NSString *const RCTJSNavigationScheme; 13 | 14 | @protocol RNAdvancedWebViewDelegate 15 | 16 | - (BOOL)webView:(RNAdvancedWebView *)webView 17 | shouldStartLoadForRequest:(NSMutableDictionary *)request 18 | withCallback:(RCTDirectEventBlock)callback; 19 | 20 | @end 21 | 22 | @interface RNAdvancedWebView : RCTView 23 | 24 | - (instancetype)initWithProcessPool:(WKProcessPool *)processPool; 25 | 26 | @property (nonatomic, weak) id delegate; 27 | 28 | @property (nonatomic, copy) NSDictionary *source; 29 | @property (nonatomic, assign) UIEdgeInsets contentInset; 30 | @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; 31 | @property (nonatomic, assign) BOOL openNewWindowInWebView; 32 | @property (nonatomic, assign) NSInteger contentInsetAdjustmentBehavior; 33 | @property (nonatomic, copy) NSString *injectedJavaScript; 34 | 35 | /** 36 | supported schemes, others will use openURL。 default is @[@"http", @"https", @"file", @"ftp", @"ws"] 37 | */ 38 | @property (nonatomic, strong) NSArray *validSchemes; 39 | 40 | /** 41 | Whether support postMessage 42 | */ 43 | @property (nonatomic, assign) BOOL messagingEnabled; 44 | 45 | @property (nonatomic, assign) BOOL hideAccessory; 46 | 47 | @property (nonatomic, assign) BOOL keyboardDisplayRequiresUserAction; 48 | 49 | @property (nonatomic, assign) BOOL disableKeyboardAdjust; 50 | 51 | - (void)goForward; 52 | - (void)goBack; 53 | - (void)reload; 54 | - (void)stopLoading; 55 | 56 | - (void)postMessage:(NSString *)message; 57 | - (void)injectJavaScript:(NSString *)script; 58 | 59 | - (BOOL)canGoBack; 60 | - (BOOL)canGoForward; 61 | - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *error))completionHandler; 62 | 63 | @end 64 | -------------------------------------------------------------------------------- /ios/RNAdvancedWebView.m: -------------------------------------------------------------------------------- 1 | #import "RNAdvancedWebView.h" 2 | #import 3 | #import "RNAdvancedWebView.h" 4 | #import 5 | 6 | #import 7 | 8 | #import 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | 16 | #import 17 | 18 | #import 19 | #import 20 | 21 | NSString *const RNAdvancedWebJSNavigationScheme = @"react-js-navigation"; 22 | NSString *const RNAdvancedWebViewJSPostMessageHost = @"postMessage"; 23 | NSString *const RNAdvancedWebViewJSDataTransferSetHost = @"dataTransferSet"; 24 | NSString *const RNAdvancedWebViewHtmlType = @"Apple Web Archive pasteboard type"; 25 | // runtime trick to remove WKWebView keyboard default toolbar 26 | // see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279 27 | @interface _SwizzleHelperWK : NSObject 28 | @end 29 | 30 | @implementation _SwizzleHelperWK 31 | 32 | -(id)inputAccessoryView 33 | { 34 | return nil; 35 | } 36 | 37 | @end 38 | 39 | 40 | @interface RNAdvancedWebView () 41 | 42 | @property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; 43 | @property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; 44 | @property (nonatomic, copy) RCTDirectEventBlock onLoadingError; 45 | @property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest; 46 | @property (nonatomic, copy) RCTDirectEventBlock onProgress; 47 | @property (nonatomic, copy) RCTDirectEventBlock onMessage; 48 | @property (assign) BOOL sendCookies; 49 | 50 | @end 51 | 52 | @implementation RNAdvancedWebView 53 | { 54 | WKWebView *_webView; 55 | NSString *_injectedJavaScript; 56 | CGPoint _originOffset; 57 | BOOL _navigationFinished; 58 | NSMutableArray * _pendingMessages; 59 | } 60 | 61 | - (instancetype)initWithFrame:(CGRect)frame 62 | { 63 | self = [super initWithFrame:frame]; 64 | if (self) { 65 | _hideAccessory = NO; 66 | _keyboardDisplayRequiresUserAction = NO; 67 | _contentInsetAdjustmentBehavior = 0; 68 | _validSchemes = @[@"http", @"https", @"file", @"ftp", @"ws"]; 69 | 70 | _pendingMessages = [[NSMutableArray alloc] init]; 71 | 72 | [[NSNotificationCenter defaultCenter] addObserver:self 73 | selector:@selector(keyboardWillChange:) 74 | name:UIKeyboardWillChangeFrameNotification object:nil]; 75 | } 76 | return self; 77 | } 78 | 79 | RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) 80 | 81 | - (instancetype)initWithProcessPool:(WKProcessPool *)processPool 82 | { 83 | if(self = [self initWithFrame:CGRectZero]) 84 | { 85 | super.backgroundColor = [UIColor clearColor]; 86 | 87 | _automaticallyAdjustContentInsets = YES; 88 | _contentInset = UIEdgeInsetsZero; 89 | 90 | WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init]; 91 | config.processPool = processPool; 92 | @try { 93 | [config setValue:[NSNumber numberWithBool:YES] forKey:@"allowUniversalAccessFromFileURLs"]; 94 | } @catch (NSException *exception) { 95 | NSLog(@"%@", exception); 96 | } 97 | 98 | WKPreferences *preferences = [[WKPreferences alloc] init]; 99 | @try { 100 | [preferences setValue:[NSNumber numberWithBool:YES] forKey:@"allowFileAccessFromFileURLs"]; 101 | } @catch (NSException *exception) { 102 | NSLog(@"%@", exception); 103 | } 104 | 105 | config.preferences = preferences; 106 | 107 | _webView = [[WKWebView alloc] initWithFrame:self.bounds configuration:config]; 108 | _webView.UIDelegate = self; 109 | _webView.navigationDelegate = self; 110 | 111 | [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil]; 112 | [_webView.scrollView addObserver:self forKeyPath:@"contentInset" options:NSKeyValueObservingOptionNew context:nil]; 113 | 114 | [[NSNotificationCenter defaultCenter] addObserver:self 115 | selector:@selector(pasteboardChangedNotification:) 116 | name:UIPasteboardChangedNotification 117 | object:nil]; 118 | [[NSNotificationCenter defaultCenter] addObserver:self 119 | selector:@selector(pasteboardChangedNotification:) 120 | name:UIApplicationDidBecomeActiveNotification 121 | object:nil]; 122 | [self injectDataTransferGetData]; 123 | [self addSubview:_webView]; 124 | } 125 | return self; 126 | } 127 | 128 | - (void)loadRequest:(NSURLRequest *)request 129 | { 130 | _navigationFinished = NO; 131 | if (request.URL && _sendCookies) { 132 | NSDictionary *cookies = [NSHTTPCookie requestHeaderFieldsWithCookies:[[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL]]; 133 | if ([cookies objectForKey:@"Cookie"]) { 134 | NSMutableURLRequest *mutableRequest = request.mutableCopy; 135 | [mutableRequest addValue:cookies[@"Cookie"] forHTTPHeaderField:@"Cookie"]; 136 | request = mutableRequest; 137 | } 138 | } 139 | [_webView loadRequest:request]; 140 | } 141 | 142 | - (void)goForward 143 | { 144 | _navigationFinished = NO; 145 | [_webView goForward]; 146 | } 147 | 148 | - (void)evaluateJavaScript:(NSString *)javaScriptString 149 | completionHandler:(void (^)(id, NSError *error))completionHandler 150 | { 151 | [_webView evaluateJavaScript:javaScriptString completionHandler:completionHandler]; 152 | } 153 | 154 | - (void)goBack 155 | { 156 | _navigationFinished = NO; 157 | [_webView goBack]; 158 | } 159 | 160 | - (BOOL)canGoBack 161 | { 162 | return [_webView canGoBack]; 163 | } 164 | 165 | - (BOOL)canGoForward 166 | { 167 | return [_webView canGoForward]; 168 | } 169 | 170 | - (void)reload 171 | { 172 | _navigationFinished = NO; 173 | NSURLRequest *request = [RCTConvert NSURLRequest:self.source]; 174 | if (request.URL && !_webView.URL.absoluteString.length) { 175 | [self loadRequest:request]; 176 | } else { 177 | [_webView reload]; 178 | } 179 | } 180 | 181 | - (void)stopLoading 182 | { 183 | _navigationFinished = NO; 184 | [_webView stopLoading]; 185 | } 186 | 187 | - (void)postMessage:(NSString *)message 188 | { 189 | if (_navigationFinished) { 190 | NSDictionary *eventInitDict = @{ 191 | @"data": message, 192 | }; 193 | NSString *source = [NSString 194 | stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));", 195 | RCTJSONStringify(eventInitDict, NULL) 196 | ]; 197 | [_webView evaluateJavaScript:source completionHandler:nil]; 198 | } else { 199 | [_pendingMessages addObject:message]; 200 | } 201 | 202 | } 203 | 204 | - (void)injectJavaScript:(NSString *)script 205 | { 206 | [_webView evaluateJavaScript:script completionHandler:nil]; 207 | } 208 | 209 | - (void)setSource:(NSDictionary *)source 210 | { 211 | // Decode query string and hash in local file path 212 | NSString *URLString = source[@"uri"] ?: source[@"url"]; 213 | if ([URLString hasPrefix:@"/"] || [URLString hasPrefix:@"file:///"]) { 214 | source = @{ 215 | @"uri": [URLString stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] 216 | }; 217 | 218 | } 219 | if (![_source isEqualToDictionary:source]) { 220 | _source = [source copy]; 221 | _sendCookies = [source[@"sendCookies"] boolValue]; 222 | if ([source[@"customUserAgent"] length] != 0 && [_webView respondsToSelector:@selector(setCustomUserAgent:)]) { 223 | [_webView setCustomUserAgent:source[@"customUserAgent"]]; 224 | } 225 | 226 | // Allow loading local files: 227 | // 228 | // Only works for iOS 9+. So iOS 8 will simply ignore those two values 229 | NSString *file = [RCTConvert NSString:source[@"file"]]; 230 | NSString *allowingReadAccessToURL = [RCTConvert NSString:source[@"allowingReadAccessToURL"]]; 231 | 232 | if (file && [_webView respondsToSelector:@selector(loadFileURL:allowingReadAccessToURL:)]) { 233 | NSURL *fileURL = [RCTConvert NSURL:file]; 234 | NSURL *baseURL = [RCTConvert NSURL:allowingReadAccessToURL]; 235 | [_webView loadFileURL:fileURL allowingReadAccessToURL:baseURL]; 236 | return; 237 | } 238 | 239 | // Check for a static html source first 240 | NSString *html = [RCTConvert NSString:source[@"html"]]; 241 | if (html) { 242 | NSURL *baseURL = [RCTConvert NSURL:source[@"baseUrl"]]; 243 | if (!baseURL) { 244 | baseURL = [NSURL URLWithString:@"about:blank"]; 245 | } 246 | [_webView loadHTMLString:html baseURL:baseURL]; 247 | return; 248 | } 249 | 250 | NSURLRequest *request = [RCTConvert NSURLRequest:source]; 251 | 252 | // Because of the way React works, as pages redirect, we actually end up 253 | // passing the redirect urls back here, so we ignore them if trying to load 254 | // the same url. We'll expose a call to 'reload' to allow a user to load 255 | // the existing page. 256 | if ([request.URL isEqual:_webView.URL]) { 257 | return; 258 | } 259 | if (!request.URL) { 260 | // Clear the webview 261 | [_webView loadHTMLString:@"" baseURL:nil]; 262 | return; 263 | } 264 | [self loadRequest:request]; 265 | } 266 | } 267 | 268 | - (void)layoutSubviews 269 | { 270 | [super layoutSubviews]; 271 | _webView.frame = self.bounds; 272 | } 273 | 274 | - (void)setContentInset:(UIEdgeInsets)contentInset 275 | { 276 | _contentInset = contentInset; 277 | [RCTView autoAdjustInsetsForView:self 278 | withScrollView:_webView.scrollView 279 | updateOffset:NO]; 280 | } 281 | 282 | - (void)setContentInsetAdjustmentBehavior:(NSInteger)contentInsetAdjustmentBehavior 283 | { 284 | if (_contentInsetAdjustmentBehavior == contentInsetAdjustmentBehavior) { 285 | return; 286 | } 287 | _contentInsetAdjustmentBehavior = contentInsetAdjustmentBehavior; 288 | if (@available(iOS 11.0, *)) { 289 | _webView.scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)_contentInsetAdjustmentBehavior; 290 | } 291 | } 292 | 293 | - (void)setBackgroundColor:(UIColor *)backgroundColor 294 | { 295 | CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor); 296 | self.opaque = _webView.opaque = _webView.scrollView.opaque = (alpha == 1.0); 297 | _webView.backgroundColor = _webView.scrollView.backgroundColor = backgroundColor; 298 | } 299 | 300 | - (UIColor *)backgroundColor 301 | { 302 | return _webView.backgroundColor; 303 | } 304 | 305 | - (NSMutableDictionary *)baseEvent 306 | { 307 | NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary:@{ 308 | @"url": _webView.URL.absoluteString ?: @"", 309 | @"loading" : @(_webView.loading), 310 | @"title": _webView.title, 311 | @"canGoBack": @(_webView.canGoBack), 312 | @"canGoForward" : @(_webView.canGoForward), 313 | }]; 314 | return event; 315 | } 316 | 317 | - (void)refreshContentInset 318 | { 319 | [RCTView autoAdjustInsetsForView:self 320 | withScrollView:_webView.scrollView 321 | updateOffset:YES]; 322 | } 323 | 324 | - (void)dealloc 325 | { 326 | _webView.scrollView.delegate = nil; 327 | _webView.UIDelegate = nil; 328 | _webView.navigationDelegate = nil; 329 | [_webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"about:blank"]]]; 330 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 331 | @try { 332 | [_webView removeObserver:self forKeyPath:@"estimatedProgress"]; 333 | [_webView.scrollView removeObserver:self forKeyPath:@"contentInset"]; 334 | } 335 | @catch (NSException * __unused exception) {} 336 | _webView = nil; 337 | } 338 | 339 | #pragma mark - NSKeyValueObserving 340 | 341 | - (void)observeValueForKeyPath:(NSString *)keyPath 342 | ofObject:(id)object 343 | change:(NSDictionary *)change 344 | context:(void *)context 345 | { 346 | if ([keyPath isEqualToString:@"estimatedProgress"]) { 347 | if (_onProgress) { 348 | _onProgress(@{@"progress": [change objectForKey:NSKeyValueChangeNewKey]}); 349 | } 350 | } else if ([keyPath isEqualToString:@"contentInset"]) { 351 | if (!_automaticallyAdjustContentInsets) { 352 | NSValue *value = [change objectForKey:NSKeyValueChangeNewKey]; 353 | UIEdgeInsets contentInset = value.UIEdgeInsetsValue; 354 | if (!UIEdgeInsetsEqualToEdgeInsets(_contentInset, contentInset)) { 355 | [RCTView autoAdjustInsetsForView:self 356 | withScrollView:_webView.scrollView 357 | updateOffset:NO]; 358 | } 359 | } 360 | } 361 | } 362 | 363 | #pragma mark - UIPastboard inject 364 | 365 | - (void)pasteboardChangedNotification:(NSNotification*)notification { 366 | dispatch_async(dispatch_get_main_queue(), ^{ 367 | [self injectDataTransferGetData]; 368 | }); 369 | } 370 | 371 | // Set data from JavaScript to Pastboard 372 | - (void)setDataToPasteboard:(NSString *)dataString 373 | { 374 | NSRange range = [dataString rangeOfString:@","]; 375 | NSString *type = [dataString substringToIndex:range.location]; 376 | NSString *data = [dataString substringFromIndex:range.location + range.length]; 377 | UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; 378 | 379 | if ([type isEqualToString:@"text/html"]) { 380 | NSMutableDictionary *resourceDictionary = [NSMutableDictionary dictionary]; 381 | 382 | [resourceDictionary setObject:[data dataUsingEncoding:NSUTF8StringEncoding] forKey:@"WebResourceData"]; 383 | [resourceDictionary setObject:@"" forKey:@"WebResourceFrameName"]; 384 | [resourceDictionary setObject:@"text/html" forKey:@"WebResourceMIMEType"]; 385 | [resourceDictionary setObject:@"UTF-8" forKey:@"WebResourceTextEncodingName"]; 386 | [resourceDictionary setObject:[_webView.URL absoluteString] forKey:@"WebResourceURL"]; 387 | 388 | NSDictionary *containerDictionary = [NSDictionary dictionaryWithObjectsAndKeys:resourceDictionary, @"WebMainResource", nil]; 389 | NSDictionary *htmlItems = [NSDictionary dictionaryWithObjectsAndKeys:containerDictionary, RNAdvancedWebViewHtmlType, nil]; 390 | [clipboard addItems: [NSArray arrayWithObjects: htmlItems, nil]]; 391 | } else if ([type isEqualToString:@"text/plain"]) { 392 | clipboard.string = data; 393 | } 394 | } 395 | 396 | - (void)injectDataTransferGetData { 397 | UIPasteboard *pasteBoard = [UIPasteboard generalPasteboard]; 398 | NSArray *> *items = [pasteBoard items]; 399 | NSString *htmlString = @""; 400 | for (NSDictionary *item in items) { 401 | NSData *archiveData = [item objectForKey:RNAdvancedWebViewHtmlType]; 402 | if (archiveData) { 403 | NSError* error = nil; 404 | id webArchive = [NSPropertyListSerialization propertyListWithData:(NSData *)archiveData 405 | options:NSPropertyListImmutable 406 | format:NULL error:&error]; 407 | NSData *webResourceData = [[webArchive objectForKey:@"WebMainResource"] objectForKey:@"WebResourceData"]; 408 | htmlString = [[NSString alloc] initWithData:webResourceData encoding:NSUTF8StringEncoding]; 409 | htmlString = [htmlString stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; 410 | } 411 | } 412 | 413 | // Override DataTransfer.prototype.getData 414 | NSString *dataTransferInjection = [NSString stringWithFormat: 415 | @"(function () {" 416 | "if (!window.originDataTransferGetData) {" 417 | " window.originDataTransferGetData = DataTransfer.prototype.getData;" 418 | "}" 419 | "DataTransfer.prototype.getData = function (type, data) {" 420 | " if (type === 'text/html') {" 421 | " return '%@';" 422 | " } else {" 423 | " return window.originDataTransferGetData.call(this, type, data);" 424 | " }" 425 | "};" 426 | "})();", htmlString]; 427 | [_webView evaluateJavaScript:dataTransferInjection completionHandler:nil]; 428 | } 429 | 430 | #pragma mark - WKNavigationDelegate 431 | 432 | - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation { 433 | if (_hideAccessory) { 434 | [self doHideAccessory]; 435 | } 436 | if (!_keyboardDisplayRequiresUserAction) { 437 | [self doKeyboardDisplayAutomatically]; 438 | } 439 | } 440 | 441 | - (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 442 | { 443 | NSURLRequest *request = navigationAction.request; 444 | NSURL* url = request.URL; 445 | NSString* scheme = url.scheme; 446 | 447 | BOOL isJSNavigation = [scheme isEqualToString:RNAdvancedWebJSNavigationScheme]; 448 | 449 | if (isJSNavigation) { 450 | NSURL *url = request.URL; 451 | NSString *data = url.query; 452 | data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "]; 453 | data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 454 | if ([url.host isEqualToString:RNAdvancedWebViewJSPostMessageHost]) { 455 | 456 | NSMutableDictionary *event = [self baseEvent]; 457 | [event addEntriesFromDictionary: @{ 458 | @"data": data, 459 | }]; 460 | _onMessage(event); 461 | } else if ([url.host isEqualToString:RNAdvancedWebViewJSDataTransferSetHost]) { 462 | [self setDataToPasteboard:data]; 463 | } 464 | 465 | 466 | decisionHandler(WKNavigationActionPolicyCancel); 467 | return; 468 | } else { 469 | if (_onShouldStartLoadWithRequest) { 470 | NSMutableDictionary *event = [self baseEvent]; 471 | [event addEntriesFromDictionary: @{ 472 | @"url": (request.URL).absoluteString, 473 | @"navigationType": @(navigationAction.navigationType) 474 | }]; 475 | if (![self.delegate webView:self 476 | shouldStartLoadForRequest:event 477 | withCallback:_onShouldStartLoadWithRequest]) { 478 | decisionHandler(WKNavigationActionPolicyCancel); 479 | return; 480 | } 481 | } 482 | 483 | if ([self externalAppRequiredToOpenURL:url]) { 484 | if ([[UIApplication sharedApplication] canOpenURL:url]) { 485 | [[UIApplication sharedApplication] openURL:url]; 486 | decisionHandler(WKNavigationActionPolicyCancel); 487 | return; 488 | } 489 | } else { 490 | if (!navigationAction.targetFrame) { 491 | [webView loadRequest:navigationAction.request]; 492 | decisionHandler(WKNavigationActionPolicyCancel); 493 | return; 494 | } 495 | } 496 | 497 | if (_onLoadingStart) { 498 | // We have this check to filter out iframe requests and whatnot 499 | BOOL isTopFrame = [url isEqual:request.mainDocumentURL]; 500 | if (isTopFrame) { 501 | NSMutableDictionary *event = [self baseEvent]; 502 | [event addEntriesFromDictionary: @{ 503 | @"url": url.absoluteString, 504 | @"navigationType": @(navigationAction.navigationType) 505 | }]; 506 | _onLoadingStart(event); 507 | } 508 | } 509 | } 510 | decisionHandler(WKNavigationActionPolicyAllow); 511 | } 512 | 513 | - (void)webView:(__unused WKWebView *)webView didFailProvisionalNavigation:(__unused WKNavigation *)navigation withError:(NSError *)error 514 | { 515 | if (_onLoadingError) { 516 | if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { 517 | // NSURLErrorCancelled is reported when a page has a redirect OR if you load 518 | // a new URL in the WebView before the previous one came back. We can just 519 | // ignore these since they aren't real errors. 520 | // http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os 521 | return; 522 | } 523 | 524 | NSMutableDictionary *event = [self baseEvent]; 525 | [event addEntriesFromDictionary:@{ 526 | @"domain": error.domain, 527 | @"code": @(error.code), 528 | @"description": error.localizedDescription, 529 | }]; 530 | _onLoadingError(event); 531 | } 532 | } 533 | 534 | - (void)webView:(WKWebView *)webView didFinishNavigation:(__unused WKNavigation *)navigation 535 | { 536 | _navigationFinished = YES; 537 | [_pendingMessages enumerateObjectsUsingBlock:^(NSString * message, NSUInteger idx, BOOL * _Nonnull stop) { 538 | [self postMessage:message]; 539 | }]; 540 | _pendingMessages = [[NSMutableArray alloc] init]; 541 | 542 | if (self.messagingEnabled) { 543 | NSString *source = [NSString stringWithFormat: 544 | @"(function() {" 545 | "var isNative = window.postMessage.length !== 1;" 546 | "if (!isNative) {return}" 547 | "var messageStack = [];" 548 | "var executing = false;" 549 | "function executeStack() {" 550 | " var message = messageStack.shift();" 551 | " if (message) {" 552 | " executing = true;" 553 | " window.location = message;" 554 | " setTimeout(executeStack);" 555 | " } else {" 556 | " executing = false;" 557 | " }" 558 | "};" 559 | "window.originalPostMessage = window.postMessage;" 560 | "window.postMessage = function(data) {" 561 | " messageStack.push('%@://%@?' + encodeURIComponent(String(data)));" 562 | " if (!executing) executeStack();" 563 | "};" 564 | "document.dispatchEvent(new CustomEvent('ReactNativeContextReady'));" 565 | "})();", RNAdvancedWebJSNavigationScheme, RNAdvancedWebViewJSPostMessageHost 566 | ]; 567 | [webView evaluateJavaScript:source completionHandler:nil]; 568 | } 569 | 570 | // Polyfill for Clipboard API 571 | NSString *dataTransferInjection = [NSString stringWithFormat: 572 | @"(function () {;" 573 | "var messageStack = [];" 574 | "var executing = false;" 575 | "function executeStack() {" 576 | " var message = messageStack.shift();" 577 | " if (message) {" 578 | " executing = true;" 579 | " window.location = message;" 580 | " setTimeout(executeStack);" 581 | " } else {" 582 | " executing = false;" 583 | " }" 584 | "};" 585 | "DataTransfer.prototype.setData = function (type, data) {" 586 | " messageStack.push('%@://%@?' + encodeURIComponent(type + ',' + data));" 587 | " if (!executing) executeStack();" 588 | "};" 589 | "})();", RNAdvancedWebJSNavigationScheme, RNAdvancedWebViewJSDataTransferSetHost]; 590 | [webView evaluateJavaScript:dataTransferInjection completionHandler:nil]; 591 | if (_injectedJavaScript) { 592 | if (_onLoadingFinish) { 593 | [webView evaluateJavaScript:_injectedJavaScript completionHandler:^(id result, NSError *error) { 594 | NSMutableDictionary *event = [self baseEvent]; 595 | event[@"jsEvaluationValue"] = [NSString stringWithFormat:@"%@", result]; 596 | _onLoadingFinish(event); 597 | }]; 598 | } 599 | } 600 | // we only need the final 'finishLoad' call so only fire the event when we're actually done loading. 601 | else if (_onLoadingFinish && !webView.loading && ![webView.URL.absoluteString isEqualToString:@"about:blank"]) { 602 | _onLoadingFinish([self baseEvent]); 603 | } 604 | } 605 | 606 | #pragma mark - WKUIDelegate 607 | 608 | - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { 609 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert]; 610 | 611 | [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { 612 | completionHandler(); 613 | }]]; 614 | UIViewController *presentingController = RCTPresentedViewController(); 615 | [presentingController presentViewController:alertController animated:YES completion:nil]; 616 | } 617 | 618 | - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler { 619 | 620 | // TODO We have to think message to confirm "YES" 621 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert]; 622 | [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 623 | completionHandler(YES); 624 | }]]; 625 | [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { 626 | completionHandler(NO); 627 | }]]; 628 | UIViewController *presentingController = RCTPresentedViewController(); 629 | [presentingController presentViewController:alertController animated:YES completion:nil]; 630 | } 631 | 632 | - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler { 633 | 634 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:nil preferredStyle:UIAlertControllerStyleAlert]; 635 | [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { 636 | textField.text = defaultText; 637 | }]; 638 | 639 | [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 640 | NSString *input = ((UITextField *)alertController.textFields.firstObject).text; 641 | completionHandler(input); 642 | }]]; 643 | 644 | [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { 645 | completionHandler(nil); 646 | }]]; 647 | UIViewController *presentingController = RCTPresentedViewController(); 648 | [presentingController presentViewController:alertController animated:YES completion:nil]; 649 | } 650 | 651 | - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures 652 | { 653 | NSString *scheme = navigationAction.request.URL.scheme; 654 | if ((navigationAction.targetFrame.isMainFrame || _openNewWindowInWebView) && ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"])) { 655 | [webView loadRequest:navigationAction.request]; 656 | } else { 657 | UIApplication *app = [UIApplication sharedApplication]; 658 | NSURL *url = navigationAction.request.URL; 659 | if ([app canOpenURL:url]) { 660 | [app openURL:url]; 661 | } 662 | } 663 | return nil; 664 | } 665 | 666 | #pragma mark - Notification 667 | 668 | - (void)keyboardWillChange:(NSNotification*)aNotification 669 | { 670 | if (_disableKeyboardAdjust) { 671 | // Disable Keyboard push up WebView. 672 | _originOffset = _webView.scrollView.contentOffset; 673 | _webView.scrollView.delegate = self; 674 | } 675 | } 676 | 677 | #pragma mark - UIScrollViewDelegate 678 | 679 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView { 680 | if (scrollView == _webView.scrollView) { 681 | float height = scrollView.frame.size.height; 682 | if (_originOffset.y + height <= scrollView.contentSize.height) { 683 | // Reset WebView's scrollView 684 | scrollView.contentOffset = _originOffset; 685 | } 686 | scrollView.delegate = nil; 687 | } 688 | } 689 | 690 | #pragma mark - Private 691 | 692 | /** 693 | hide inputAccessoryView 694 | */ 695 | -(void)doHideAccessory 696 | { 697 | UIView* subview; 698 | for (UIView* view in _webView.scrollView.subviews) { 699 | if([[view.class description] hasPrefix:@"WKContent"]) 700 | subview = view; 701 | } 702 | 703 | if(subview == nil) return; 704 | 705 | NSString* name = [NSString stringWithFormat:@"%@_SwizzleHelperWK", subview.class.superclass]; 706 | Class newClass = NSClassFromString(name); 707 | 708 | if(newClass == nil) 709 | { 710 | newClass = objc_allocateClassPair(subview.class, [name cStringUsingEncoding:NSASCIIStringEncoding], 0); 711 | if(!newClass) return; 712 | Method method = class_getInstanceMethod([_SwizzleHelperWK class], @selector(inputAccessoryView)); 713 | class_addMethod(newClass, @selector(inputAccessoryView), method_getImplementation(method), method_getTypeEncoding(method)); 714 | objc_registerClassPair(newClass); 715 | } 716 | object_setClass(subview, newClass); 717 | } 718 | 719 | - (void)doKeyboardDisplayAutomatically { 720 | Class class = NSClassFromString(@"WKContentView"); 721 | NSOperatingSystemVersion iOS_11_3_0 = (NSOperatingSystemVersion){11, 3, 0}; 722 | 723 | if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_11_3_0]) { 724 | SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:"); 725 | Method method = class_getInstanceMethod(class, selector); 726 | IMP original = method_getImplementation(method); 727 | IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) { 728 | ((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4); 729 | }); 730 | method_setImplementation(method, override); 731 | } else { 732 | SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:"); 733 | Method method = class_getInstanceMethod(class, selector); 734 | IMP original = method_getImplementation(method); 735 | IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, id arg3) { 736 | ((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3); 737 | }); 738 | method_setImplementation(method, override); 739 | } 740 | } 741 | 742 | /** 743 | Whether need external app to open url 744 | @param URL URL description 745 | @return return value description 746 | */ 747 | - (BOOL)externalAppRequiredToOpenURL:(NSURL *)URL { 748 | NSString *scheme = URL.scheme; 749 | if (scheme.length) { 750 | return ![_validSchemes containsObject:URL.scheme]; 751 | } else { 752 | return NO; 753 | } 754 | } 755 | 756 | @end 757 | -------------------------------------------------------------------------------- /ios/RNAdvancedWebView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7F5EB4A9209AF8AE0029C840 /* UIScrollView+adjustedContentInset.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F5EB4A7209AF8AE0029C840 /* UIScrollView+adjustedContentInset.m */; }; 11 | 7F6E55141E1CA7E000C11C87 /* RNAdvancedWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F6E55131E1CA7E000C11C87 /* RNAdvancedWebViewManager.m */; }; 12 | 7F6E55161E1CA8C400C11C87 /* RNAdvancedWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F6E55151E1CA8C400C11C87 /* RNAdvancedWebView.m */; }; 13 | 8E1F5D81210F243E00CFED9B /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 8E1F5D71210F243E00CFED9B /* Reachability.m */; }; 14 | 8E1F5D82210F243E00CFED9B /* NSString+Sha1.m in Sources */ = {isa = PBXBuildFile; fileRef = 8E1F5D72210F243E00CFED9B /* NSString+Sha1.m */; }; 15 | 8E1F5D83210F243E00CFED9B /* RNCachingURLProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 8E1F5D77210F243E00CFED9B /* RNCachingURLProtocol.m */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 1017892E1B283EE1006C41B8 /* CopyFiles */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = "include/$(PRODUCT_NAME)"; 23 | dstSubfolderSpec = 16; 24 | files = ( 25 | ); 26 | runOnlyForDeploymentPostprocessing = 0; 27 | }; 28 | /* End PBXCopyFilesBuildPhase section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 101789301B283EE1006C41B8 /* libRNAdvancedWebView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNAdvancedWebView.a; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | 7F5EB4A7209AF8AE0029C840 /* UIScrollView+adjustedContentInset.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+adjustedContentInset.m"; sourceTree = ""; }; 33 | 7F5EB4A8209AF8AE0029C840 /* UIScrollView+adjustedContentInset.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+adjustedContentInset.h"; sourceTree = ""; }; 34 | 7F6E55121E1CA7E000C11C87 /* RNAdvancedWebViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNAdvancedWebViewManager.h; sourceTree = ""; }; 35 | 7F6E55131E1CA7E000C11C87 /* RNAdvancedWebViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNAdvancedWebViewManager.m; sourceTree = ""; }; 36 | 7F6E55151E1CA8C400C11C87 /* RNAdvancedWebView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNAdvancedWebView.m; sourceTree = ""; }; 37 | 7F6E55171E1CA8D000C11C87 /* RNAdvancedWebView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNAdvancedWebView.h; sourceTree = ""; }; 38 | 8E1F5D71210F243E00CFED9B /* Reachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Reachability.m; sourceTree = ""; }; 39 | 8E1F5D72210F243E00CFED9B /* NSString+Sha1.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+Sha1.m"; sourceTree = ""; }; 40 | 8E1F5D73210F243E00CFED9B /* RNCachingURLProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNCachingURLProtocol.h; sourceTree = ""; }; 41 | 8E1F5D74210F243E00CFED9B /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 42 | 8E1F5D75210F243E00CFED9B /* Reachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Reachability.h; sourceTree = ""; }; 43 | 8E1F5D76210F243E00CFED9B /* NSString+Sha1.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Sha1.h"; sourceTree = ""; }; 44 | 8E1F5D77210F243E00CFED9B /* RNCachingURLProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCachingURLProtocol.m; sourceTree = ""; }; 45 | 8E1F5D79210F243E00CFED9B /* RNCachingURLProtocol.iml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = RNCachingURLProtocol.iml; sourceTree = ""; }; 46 | 8E1F5D7A210F243E00CFED9B /* xcode.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = xcode.xml; sourceTree = ""; }; 47 | 8E1F5D7B210F243E00CFED9B /* encodings.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = encodings.xml; sourceTree = ""; }; 48 | 8E1F5D7C210F243E00CFED9B /* vcs.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = vcs.xml; sourceTree = ""; }; 49 | 8E1F5D7D210F243E00CFED9B /* modules.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = modules.xml; sourceTree = ""; }; 50 | 8E1F5D7E210F243E00CFED9B /* misc.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = misc.xml; sourceTree = ""; }; 51 | 8E1F5D80210F243E00CFED9B /* scope_settings.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = scope_settings.xml; sourceTree = ""; }; 52 | /* End PBXFileReference section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | 1017892D1B283EE1006C41B8 /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | /* End PBXFrameworksBuildPhase section */ 63 | 64 | /* Begin PBXGroup section */ 65 | 101789271B283EE1006C41B8 = { 66 | isa = PBXGroup; 67 | children = ( 68 | 101789311B283EE1006C41B8 /* Products */, 69 | 7F5EB4A8209AF8AE0029C840 /* UIScrollView+adjustedContentInset.h */, 70 | 7F5EB4A7209AF8AE0029C840 /* UIScrollView+adjustedContentInset.m */, 71 | 7F6E55171E1CA8D000C11C87 /* RNAdvancedWebView.h */, 72 | 7F6E55151E1CA8C400C11C87 /* RNAdvancedWebView.m */, 73 | 7F6E55121E1CA7E000C11C87 /* RNAdvancedWebViewManager.h */, 74 | 7F6E55131E1CA7E000C11C87 /* RNAdvancedWebViewManager.m */, 75 | 8E1F5D70210F243E00CFED9B /* RNCachingURLProtocol */, 76 | ); 77 | sourceTree = ""; 78 | }; 79 | 101789311B283EE1006C41B8 /* Products */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 101789301B283EE1006C41B8 /* libRNAdvancedWebView.a */, 83 | ); 84 | name = Products; 85 | sourceTree = ""; 86 | }; 87 | 8E1F5D70210F243E00CFED9B /* RNCachingURLProtocol */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 8E1F5D71210F243E00CFED9B /* Reachability.m */, 91 | 8E1F5D72210F243E00CFED9B /* NSString+Sha1.m */, 92 | 8E1F5D73210F243E00CFED9B /* RNCachingURLProtocol.h */, 93 | 8E1F5D74210F243E00CFED9B /* README.md */, 94 | 8E1F5D75210F243E00CFED9B /* Reachability.h */, 95 | 8E1F5D76210F243E00CFED9B /* NSString+Sha1.h */, 96 | 8E1F5D77210F243E00CFED9B /* RNCachingURLProtocol.m */, 97 | 8E1F5D78210F243E00CFED9B /* .idea */, 98 | ); 99 | path = RNCachingURLProtocol; 100 | sourceTree = ""; 101 | }; 102 | 8E1F5D78210F243E00CFED9B /* .idea */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 8E1F5D79210F243E00CFED9B /* RNCachingURLProtocol.iml */, 106 | 8E1F5D7A210F243E00CFED9B /* xcode.xml */, 107 | 8E1F5D7B210F243E00CFED9B /* encodings.xml */, 108 | 8E1F5D7C210F243E00CFED9B /* vcs.xml */, 109 | 8E1F5D7D210F243E00CFED9B /* modules.xml */, 110 | 8E1F5D7E210F243E00CFED9B /* misc.xml */, 111 | 8E1F5D7F210F243E00CFED9B /* scopes */, 112 | ); 113 | path = .idea; 114 | sourceTree = ""; 115 | }; 116 | 8E1F5D7F210F243E00CFED9B /* scopes */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 8E1F5D80210F243E00CFED9B /* scope_settings.xml */, 120 | ); 121 | path = scopes; 122 | sourceTree = ""; 123 | }; 124 | /* End PBXGroup section */ 125 | 126 | /* Begin PBXNativeTarget section */ 127 | 1017892F1B283EE1006C41B8 /* RNAdvancedWebView */ = { 128 | isa = PBXNativeTarget; 129 | buildConfigurationList = 101789441B283EE1006C41B8 /* Build configuration list for PBXNativeTarget "RNAdvancedWebView" */; 130 | buildPhases = ( 131 | 1017892C1B283EE1006C41B8 /* Sources */, 132 | 1017892D1B283EE1006C41B8 /* Frameworks */, 133 | 1017892E1B283EE1006C41B8 /* CopyFiles */, 134 | ); 135 | buildRules = ( 136 | ); 137 | dependencies = ( 138 | ); 139 | name = RNAdvancedWebView; 140 | productName = RNAdvancedWebView; 141 | productReference = 101789301B283EE1006C41B8 /* libRNAdvancedWebView.a */; 142 | productType = "com.apple.product-type.library.static"; 143 | }; 144 | /* End PBXNativeTarget section */ 145 | 146 | /* Begin PBXProject section */ 147 | 101789281B283EE1006C41B8 /* Project object */ = { 148 | isa = PBXProject; 149 | attributes = { 150 | LastUpgradeCheck = 0620; 151 | ORGANIZATIONNAME = shimo; 152 | TargetAttributes = { 153 | 1017892F1B283EE1006C41B8 = { 154 | CreatedOnToolsVersion = 6.2; 155 | }; 156 | }; 157 | }; 158 | buildConfigurationList = 1017892B1B283EE1006C41B8 /* Build configuration list for PBXProject "RNAdvancedWebView" */; 159 | compatibilityVersion = "Xcode 3.2"; 160 | developmentRegion = English; 161 | hasScannedForEncodings = 0; 162 | knownRegions = ( 163 | en, 164 | ); 165 | mainGroup = 101789271B283EE1006C41B8; 166 | productRefGroup = 101789311B283EE1006C41B8 /* Products */; 167 | projectDirPath = ""; 168 | projectRoot = ""; 169 | targets = ( 170 | 1017892F1B283EE1006C41B8 /* RNAdvancedWebView */, 171 | ); 172 | }; 173 | /* End PBXProject section */ 174 | 175 | /* Begin PBXSourcesBuildPhase section */ 176 | 1017892C1B283EE1006C41B8 /* Sources */ = { 177 | isa = PBXSourcesBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | 7F6E55141E1CA7E000C11C87 /* RNAdvancedWebViewManager.m in Sources */, 181 | 7F6E55161E1CA8C400C11C87 /* RNAdvancedWebView.m in Sources */, 182 | 8E1F5D81210F243E00CFED9B /* Reachability.m in Sources */, 183 | 8E1F5D82210F243E00CFED9B /* NSString+Sha1.m in Sources */, 184 | 7F5EB4A9209AF8AE0029C840 /* UIScrollView+adjustedContentInset.m in Sources */, 185 | 8E1F5D83210F243E00CFED9B /* RNCachingURLProtocol.m in Sources */, 186 | ); 187 | runOnlyForDeploymentPostprocessing = 0; 188 | }; 189 | /* End PBXSourcesBuildPhase section */ 190 | 191 | /* Begin XCBuildConfiguration section */ 192 | 101789421B283EE1006C41B8 /* Debug */ = { 193 | isa = XCBuildConfiguration; 194 | buildSettings = { 195 | ALWAYS_SEARCH_USER_PATHS = NO; 196 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 197 | CLANG_CXX_LIBRARY = "libc++"; 198 | CLANG_ENABLE_MODULES = YES; 199 | CLANG_ENABLE_OBJC_ARC = YES; 200 | CLANG_WARN_BOOL_CONVERSION = YES; 201 | CLANG_WARN_CONSTANT_CONVERSION = YES; 202 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 203 | CLANG_WARN_EMPTY_BODY = YES; 204 | CLANG_WARN_ENUM_CONVERSION = YES; 205 | CLANG_WARN_INT_CONVERSION = YES; 206 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 207 | CLANG_WARN_UNREACHABLE_CODE = YES; 208 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 209 | COPY_PHASE_STRIP = NO; 210 | ENABLE_STRICT_OBJC_MSGSEND = YES; 211 | GCC_C_LANGUAGE_STANDARD = gnu99; 212 | GCC_DYNAMIC_NO_PIC = NO; 213 | GCC_OPTIMIZATION_LEVEL = 0; 214 | GCC_PREPROCESSOR_DEFINITIONS = ( 215 | "DEBUG=1", 216 | "$(inherited)", 217 | ); 218 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 219 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 220 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 221 | GCC_WARN_UNDECLARED_SELECTOR = YES; 222 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 223 | GCC_WARN_UNUSED_FUNCTION = YES; 224 | GCC_WARN_UNUSED_VARIABLE = YES; 225 | HEADER_SEARCH_PATHS = ( 226 | "$(SRCROOT)/../node_modules/react-native/React/**", 227 | "$(inherited)", 228 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 229 | ); 230 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 231 | MTL_ENABLE_DEBUG_INFO = YES; 232 | ONLY_ACTIVE_ARCH = YES; 233 | SDKROOT = iphoneos; 234 | }; 235 | name = Debug; 236 | }; 237 | 101789431B283EE1006C41B8 /* Release */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 242 | CLANG_CXX_LIBRARY = "libc++"; 243 | CLANG_ENABLE_MODULES = YES; 244 | CLANG_ENABLE_OBJC_ARC = YES; 245 | CLANG_WARN_BOOL_CONVERSION = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 248 | CLANG_WARN_EMPTY_BODY = YES; 249 | CLANG_WARN_ENUM_CONVERSION = YES; 250 | CLANG_WARN_INT_CONVERSION = YES; 251 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 252 | CLANG_WARN_UNREACHABLE_CODE = YES; 253 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 254 | COPY_PHASE_STRIP = NO; 255 | ENABLE_NS_ASSERTIONS = NO; 256 | ENABLE_STRICT_OBJC_MSGSEND = YES; 257 | GCC_C_LANGUAGE_STANDARD = gnu99; 258 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 259 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 260 | GCC_WARN_UNDECLARED_SELECTOR = YES; 261 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 262 | GCC_WARN_UNUSED_FUNCTION = YES; 263 | GCC_WARN_UNUSED_VARIABLE = YES; 264 | HEADER_SEARCH_PATHS = ( 265 | "$(SRCROOT)/../node_modules/react-native/React/**", 266 | "$(inherited)", 267 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 268 | ); 269 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 270 | MTL_ENABLE_DEBUG_INFO = NO; 271 | SDKROOT = iphoneos; 272 | VALIDATE_PRODUCT = YES; 273 | }; 274 | name = Release; 275 | }; 276 | 101789451B283EE1006C41B8 /* Debug */ = { 277 | isa = XCBuildConfiguration; 278 | buildSettings = { 279 | HEADER_SEARCH_PATHS = ( 280 | "$(SRCROOT)/../../node_modules/react-native/React/**", 281 | "$(inherited)", 282 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 283 | "$(SRCROOT)/../../react-native/React/**", 284 | ); 285 | OTHER_LDFLAGS = "-ObjC"; 286 | PRODUCT_NAME = "$(TARGET_NAME)"; 287 | SKIP_INSTALL = YES; 288 | }; 289 | name = Debug; 290 | }; 291 | 101789461B283EE1006C41B8 /* Release */ = { 292 | isa = XCBuildConfiguration; 293 | buildSettings = { 294 | HEADER_SEARCH_PATHS = ( 295 | "$(SRCROOT)/../../node_modules/react-native/React/**", 296 | "$(inherited)", 297 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 298 | "$(SRCROOT)/../../react-native/React/**", 299 | ); 300 | OTHER_LDFLAGS = "-ObjC"; 301 | PRODUCT_NAME = "$(TARGET_NAME)"; 302 | SKIP_INSTALL = YES; 303 | }; 304 | name = Release; 305 | }; 306 | /* End XCBuildConfiguration section */ 307 | 308 | /* Begin XCConfigurationList section */ 309 | 1017892B1B283EE1006C41B8 /* Build configuration list for PBXProject "RNAdvancedWebView" */ = { 310 | isa = XCConfigurationList; 311 | buildConfigurations = ( 312 | 101789421B283EE1006C41B8 /* Debug */, 313 | 101789431B283EE1006C41B8 /* Release */, 314 | ); 315 | defaultConfigurationIsVisible = 0; 316 | defaultConfigurationName = Release; 317 | }; 318 | 101789441B283EE1006C41B8 /* Build configuration list for PBXNativeTarget "RNAdvancedWebView" */ = { 319 | isa = XCConfigurationList; 320 | buildConfigurations = ( 321 | 101789451B283EE1006C41B8 /* Debug */, 322 | 101789461B283EE1006C41B8 /* Release */, 323 | ); 324 | defaultConfigurationIsVisible = 0; 325 | defaultConfigurationName = Release; 326 | }; 327 | /* End XCConfigurationList section */ 328 | }; 329 | rootObject = 101789281B283EE1006C41B8 /* Project object */; 330 | } 331 | -------------------------------------------------------------------------------- /ios/RNAdvancedWebViewManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface RNAdvancedWebViewManager : RCTViewManager 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /ios/RNAdvancedWebViewManager.m: -------------------------------------------------------------------------------- 1 | #import "RNAdvancedWebViewManager.h" 2 | 3 | #import "RNAdvancedWebView.h" 4 | #import 5 | #import 6 | #import 7 | #import 8 | #import 9 | 10 | #import 11 | 12 | #import "RNCachingURLProtocol.h" 13 | 14 | 15 | @interface RNAdvancedWebViewManager () 16 | 17 | @end 18 | 19 | @implementation RNAdvancedWebViewManager 20 | { 21 | NSConditionLock *_shouldStartLoadLock; 22 | BOOL _shouldStartLoad; 23 | } 24 | 25 | RCT_EXPORT_MODULE() 26 | 27 | - (UIView *)view 28 | { 29 | [NSURLProtocol registerClass:[RNCachingURLProtocol class]]; 30 | 31 | RNAdvancedWebView *webView = [[RNAdvancedWebView alloc] initWithProcessPool:[[WKProcessPool alloc] init]]; 32 | webView.delegate = self; 33 | return webView; 34 | } 35 | 36 | RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary) 37 | RCT_REMAP_VIEW_PROPERTY(bounces, _webView.scrollView.bounces, BOOL) 38 | RCT_REMAP_VIEW_PROPERTY(pagingEnabled, _webView.scrollView.pagingEnabled, BOOL) 39 | RCT_REMAP_VIEW_PROPERTY(scrollEnabled, _webView.scrollView.scrollEnabled, BOOL) 40 | RCT_REMAP_VIEW_PROPERTY(allowsBackForwardNavigationGestures, _webView.allowsBackForwardNavigationGestures, BOOL) 41 | RCT_REMAP_VIEW_PROPERTY(userAgent, _webView.customUserAgent, NSString) 42 | RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString) 43 | RCT_EXPORT_VIEW_PROPERTY(openNewWindowInWebView, BOOL) 44 | RCT_EXPORT_VIEW_PROPERTY(contentInsetAdjustmentBehavior, NSInteger) 45 | RCT_EXPORT_VIEW_PROPERTY(disableKeyboardAdjust, BOOL) 46 | RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets) 47 | RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL) 48 | RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock) 49 | RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock) 50 | RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock) 51 | RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock) 52 | RCT_EXPORT_VIEW_PROPERTY(onProgress, RCTDirectEventBlock) 53 | RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock) 54 | 55 | #pragma mark - added 56 | 57 | RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL) 58 | RCT_EXPORT_VIEW_PROPERTY(hideAccessory, BOOL) 59 | RCT_EXPORT_VIEW_PROPERTY(keyboardDisplayRequiresUserAction, BOOL) 60 | RCT_EXPORT_VIEW_PROPERTY(validSchemes, NSArray) 61 | 62 | RCT_EXPORT_METHOD(goBack:(nonnull NSNumber *)reactTag) 63 | { 64 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 65 | RNAdvancedWebView *view = viewRegistry[reactTag]; 66 | if (![view isKindOfClass:[RNAdvancedWebView class]]) { 67 | RCTLogError(@"Invalid view returned from registry, expecting RNAdvancedWebView, got: %@", view); 68 | } else { 69 | [view goBack]; 70 | } 71 | }]; 72 | } 73 | 74 | RCT_EXPORT_METHOD(goForward:(nonnull NSNumber *)reactTag) 75 | { 76 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 77 | RNAdvancedWebView *view = viewRegistry[reactTag]; 78 | if (![view isKindOfClass:[RNAdvancedWebView class]]) { 79 | RCTLogError(@"Invalid view returned from registry, expecting RNAdvancedWebView, got: %@", view); 80 | } else { 81 | [view goForward]; 82 | } 83 | }]; 84 | } 85 | 86 | RCT_EXPORT_METHOD(canGoBack:(nonnull NSNumber *)reactTag 87 | resolver:(RCTPromiseResolveBlock)resolve 88 | rejecter:(RCTPromiseRejectBlock)reject) 89 | { 90 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 91 | RNAdvancedWebView *view = viewRegistry[reactTag]; 92 | resolve([NSNumber numberWithBool:[view canGoBack]]); 93 | }]; 94 | } 95 | 96 | RCT_EXPORT_METHOD(canGoForward:(nonnull NSNumber *)reactTag 97 | resolver:(RCTPromiseResolveBlock)resolve 98 | rejecter:(RCTPromiseRejectBlock)reject) 99 | { 100 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 101 | RNAdvancedWebView *view = viewRegistry[reactTag]; 102 | resolve([NSNumber numberWithBool:[view canGoForward]]); 103 | }]; 104 | } 105 | 106 | RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag) 107 | { 108 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 109 | RNAdvancedWebView *view = viewRegistry[reactTag]; 110 | if (![view isKindOfClass:[RNAdvancedWebView class]]) { 111 | RCTLogError(@"Invalid view returned from registry, expecting RNAdvancedWebView, got: %@", view); 112 | } else { 113 | [view reload]; 114 | } 115 | }]; 116 | } 117 | 118 | RCT_EXPORT_METHOD(stopLoading:(nonnull NSNumber *)reactTag) 119 | { 120 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 121 | RNAdvancedWebView *view = viewRegistry[reactTag]; 122 | if (![view isKindOfClass:[RNAdvancedWebView class]]) { 123 | RCTLogError(@"Invalid view returned from registry, expecting RNAdvancedWebView, got: %@", view); 124 | } else { 125 | [view stopLoading]; 126 | } 127 | }]; 128 | } 129 | 130 | RCT_EXPORT_METHOD(evaluateJavaScript:(nonnull NSNumber *)reactTag 131 | js:(NSString *)js 132 | resolver:(RCTPromiseResolveBlock)resolve 133 | rejecter:(RCTPromiseRejectBlock)reject) 134 | { 135 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 136 | RNAdvancedWebView *view = viewRegistry[reactTag]; 137 | if (![view isKindOfClass:[RNAdvancedWebView class]]) { 138 | RCTLogError(@"Invalid view returned from registry, expecting RNAdvancedWebView, got: %@", view); 139 | } else { 140 | [view evaluateJavaScript:js completionHandler:^(id result, NSError *error) { 141 | if (error) { 142 | reject(@"js_error", @"Error occurred while evaluating Javascript", error); 143 | } else { 144 | resolve(result); 145 | } 146 | }]; 147 | } 148 | }]; 149 | } 150 | 151 | #pragma mark - missing methods 152 | 153 | RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message) 154 | { 155 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 156 | RNAdvancedWebView *view = viewRegistry[reactTag]; 157 | if (![view isKindOfClass:[RNAdvancedWebView class]]) { 158 | RCTLogError(@"Invalid view returned from registry, expecting RNAdvancedWebView, got: %@", view); 159 | } else { 160 | [view postMessage:message]; 161 | } 162 | }]; 163 | } 164 | 165 | RCT_EXPORT_METHOD(injectJavaScript:(nonnull NSNumber *)reactTag script:(NSString *)script) 166 | { 167 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 168 | RNAdvancedWebView *view = viewRegistry[reactTag]; 169 | if (![view isKindOfClass:[RNAdvancedWebView class]]) { 170 | RCTLogError(@"Invalid view returned from registry, expecting RNAdvancedWebView, got: %@", view); 171 | } else { 172 | [view injectJavaScript:script]; 173 | } 174 | }]; 175 | } 176 | 177 | #pragma mark - Exported synchronous methods 178 | 179 | - (BOOL)webView:(__unused RNAdvancedWebView *)webView 180 | shouldStartLoadForRequest:(NSMutableDictionary *)request 181 | withCallback:(RCTDirectEventBlock)callback 182 | { 183 | _shouldStartLoadLock = [[NSConditionLock alloc] initWithCondition:arc4random()]; 184 | _shouldStartLoad = YES; 185 | request[@"lockIdentifier"] = @(_shouldStartLoadLock.condition); 186 | callback(request); 187 | 188 | // Block the main thread for a maximum of 250ms until the JS thread returns 189 | if ([_shouldStartLoadLock lockWhenCondition:0 beforeDate:[NSDate dateWithTimeIntervalSinceNow:.25]]) { 190 | BOOL returnValue = _shouldStartLoad; 191 | [_shouldStartLoadLock unlock]; 192 | _shouldStartLoadLock = nil; 193 | return returnValue; 194 | } else { 195 | RCTLogWarn(@"Did not receive response to shouldStartLoad in time, defaulting to YES"); 196 | return YES; 197 | } 198 | } 199 | 200 | RCT_EXPORT_METHOD(startLoadWithResult:(BOOL)result lockIdentifier:(NSInteger)lockIdentifier) 201 | { 202 | if ([_shouldStartLoadLock tryLockWhenCondition:lockIdentifier]) { 203 | _shouldStartLoad = result; 204 | [_shouldStartLoadLock unlockWithCondition:0]; 205 | } else { 206 | RCTLogWarn(@"startLoadWithResult invoked with invalid lockIdentifier: " 207 | "got %zd, expected %zd", lockIdentifier, _shouldStartLoadLock.condition); 208 | } 209 | } 210 | 211 | @end 212 | 213 | -------------------------------------------------------------------------------- /ios/RNCachingURLProtocol/NSString+Sha1.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | #import 4 | 5 | /** 6 | * This extension contains several a helper 7 | * for creating a sha1 hash from instances of NSString 8 | */ 9 | @interface NSString (Sha1) 10 | 11 | /** 12 | * Creates a SHA1 (hash) representation of NSString. 13 | * 14 | * @return NSString 15 | */ 16 | - (NSString *)sha1; 17 | 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /ios/RNCachingURLProtocol/NSString+Sha1.m: -------------------------------------------------------------------------------- 1 | 2 | #import "NSString+Sha1.h" 3 | 4 | @implementation NSString (Sha1) 5 | 6 | - (NSString *)sha1 7 | { 8 | // see http://www.makebetterthings.com/iphone/how-to-get-md5-and-sha1-in-objective-c-ios-sdk/ 9 | NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding]; 10 | uint8_t digest[CC_SHA1_DIGEST_LENGTH]; 11 | 12 | CC_SHA1(data.bytes, (CC_LONG)data.length, digest); 13 | 14 | NSMutableString *output = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2]; 15 | 16 | for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) { 17 | [output appendFormat:@"%02x", digest[i]]; 18 | } 19 | 20 | return output; 21 | } 22 | 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /ios/RNCachingURLProtocol/README.md: -------------------------------------------------------------------------------- 1 | # BACKGROUND 2 | 3 | RNCachingURLProtocol is a simple shim for the HTTP protocol (that’s not 4 | nearly as scary as it sounds). Anytime a URL is downloaded, the response is 5 | cached to disk. Anytime a URL is requested, if we’re online then things 6 | proceed normally. If we’re offline, then we retrieve the cached version. 7 | 8 | The point of RNCachingURLProtocol is mostly to demonstrate how this is done. 9 | The current implementation is extremely simple. In particular, it doesn’t 10 | worry about cleaning up the cache. The assumption is that you’re caching just 11 | a few simple things, like your “Latest News” page (which was the problem I 12 | was solving). It caches all HTTP traffic, so without some modifications, it’s 13 | not appropriate for an app that has a lot of HTTP connections (see 14 | MKNetworkKit for that). But if you need to cache some URLs and not others, 15 | that is easy to implement. 16 | 17 | You should also look at [AFCache](https://github.com/artifacts/AFCache) for a 18 | more powerful caching engine that is currently integrating the ideas of 19 | RNCachingURLProtocol. 20 | 21 | # USAGE 22 | 23 | 1. To build, you will need the Reachability code from Apple (included). That requires that you link with 24 | `SystemConfiguration.framework`. 25 | 26 | 2. At some point early in the program (usually `application:didFinishLaunchingWithOptions:`), 27 | call the following: 28 | 29 | `[NSURLProtocol registerClass:[RNCachingURLProtocol class]];` 30 | 31 | 3. There is no step 3. 32 | 33 | For more details see 34 | [Drop-in offline caching for UIWebView (and NSURLProtocol)](http://robnapier.net/blog/offline-uiwebview-nsurlprotocol-588). 35 | 36 | # EXAMPLE 37 | 38 | See the CachedWebView project for example usage. 39 | 40 | # LICENSE 41 | 42 | This code is licensed under the MIT License: 43 | 44 | Permission is hereby granted, free of charge, to any person obtaining a 45 | copy of this software and associated documentation files (the "Software"), 46 | to deal in the Software without restriction, including without limitation 47 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 48 | and/or sell copies of the Software, and to permit persons to whom the 49 | Software is furnished to do so, subject to the following conditions: 50 | 51 | The above copyright notice and this permission notice shall be included in 52 | all copies or substantial portions of the Software. 53 | 54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 55 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 56 | FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 57 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 58 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 59 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 60 | DEALINGS IN THE SOFTWARE. 61 | -------------------------------------------------------------------------------- /ios/RNCachingURLProtocol/RNCachingURLProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNCachingURLProtocol.h 3 | // 4 | // Created by Robert Napier on 1/10/12. 5 | // Copyright (c) 2012 Rob Napier. All rights reserved. 6 | // 7 | // This code is licensed under the MIT License: 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a 10 | // copy of this software and associated documentation files (the "Software"), 11 | // to deal in the Software without restriction, including without limitation 12 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 13 | // and/or sell copies of the Software, and to permit persons to whom the 14 | // Software is furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | // DEALINGS IN THE SOFTWARE. 26 | // 27 | 28 | // RNCachingURLProtocol is a simple shim for the HTTP protocol (that’s not 29 | // nearly as scary as it sounds). Anytime a URL is download, the response is 30 | // cached to disk. Anytime a URL is requested, if we’re online then things 31 | // proceed normally. If we’re offline, then we retrieve the cached version. 32 | // 33 | // The point of RNCachingURLProtocol is mostly to demonstrate how this is done. 34 | // The current implementation is extremely simple. In particular, it doesn’t 35 | // worry about cleaning up the cache. The assumption is that you’re caching just 36 | // a few simple things, like your “Latest News” page (which was the problem I 37 | // was solving). It caches all HTTP traffic, so without some modifications, it’s 38 | // not appropriate for an app that has a lot of HTTP connections (see 39 | // MKNetworkKit for that). But if you need to cache some URLs and not others, 40 | // that is easy to implement. 41 | // 42 | // You should also look at [AFCache](https://github.com/artifacts/AFCache) for a 43 | // more powerful caching engine that is currently integrating the ideas of 44 | // RNCachingURLProtocol. 45 | // 46 | // A quick rundown of how to use it: 47 | // 48 | // 1. To build, you will need the Reachability code from Apple (included). That requires that you link with 49 | // `SystemConfiguration.framework`. 50 | // 51 | // 2. At some point early in the program (application:didFinishLaunchingWithOptions:), 52 | // call the following: 53 | // 54 | // `[NSURLProtocol registerClass:[RNCachingURLProtocol class]];` 55 | // 56 | // 3. There is no step 3. 57 | // 58 | // For more details see 59 | // [Drop-in offline caching for UIWebView (and NSURLProtocol)](http://robnapier.net/blog/offline-uiwebview-nsurlprotocol-588). 60 | 61 | #import 62 | 63 | @interface RNCachingURLProtocol : NSURLProtocol 64 | 65 | + (NSSet *)supportedSchemes; 66 | + (void)setSupportedSchemes:(NSSet *)supportedSchemes; 67 | 68 | - (NSString *)cachePathForRequest:(NSURLRequest *)aRequest; 69 | - (BOOL) useCache; 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /ios/RNCachingURLProtocol/RNCachingURLProtocol.m: -------------------------------------------------------------------------------- 1 | // 2 | // RNCachingURLProtocol.m 3 | // 4 | // Created by Robert Napier on 1/10/12. 5 | // Copyright (c) 2012 Rob Napier. 6 | // 7 | // This code is licensed under the MIT License: 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a 10 | // copy of this software and associated documentation files (the "Software"), 11 | // to deal in the Software without restriction, including without limitation 12 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 13 | // and/or sell copies of the Software, and to permit persons to whom the 14 | // Software is furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | // DEALINGS IN THE SOFTWARE. 26 | // 27 | 28 | #import "RNCachingURLProtocol.h" 29 | #import "Reachability.h" 30 | #import "NSString+Sha1.h" 31 | 32 | #define WORKAROUND_MUTABLE_COPY_LEAK 1 33 | 34 | #if WORKAROUND_MUTABLE_COPY_LEAK 35 | // required to workaround http://openradar.appspot.com/11596316 36 | @interface NSURLRequest(MutableCopyWorkaround) 37 | 38 | - (id) mutableCopyWorkaround; 39 | 40 | @end 41 | #endif 42 | 43 | @interface RNCachedData : NSObject 44 | @property (nonatomic, readwrite, strong) NSData *data; 45 | @property (nonatomic, readwrite, strong) NSURLResponse *response; 46 | @property (nonatomic, readwrite, strong) NSURLRequest *redirectRequest; 47 | @end 48 | 49 | static NSString *RNCachingURLHeader = @"X-RNCache"; 50 | 51 | @interface RNCachingURLProtocol () // iOS5-only 52 | @property (nonatomic, readwrite, strong) NSURLConnection *connection; 53 | @property (nonatomic, readwrite, strong) NSMutableData *data; 54 | @property (nonatomic, readwrite, strong) NSURLResponse *response; 55 | - (void)appendData:(NSData *)newData; 56 | @end 57 | 58 | static NSObject *RNCachingSupportedSchemesMonitor; 59 | static NSSet *RNCachingSupportedSchemes; 60 | 61 | @implementation RNCachingURLProtocol 62 | @synthesize connection = connection_; 63 | @synthesize data = data_; 64 | @synthesize response = response_; 65 | 66 | + (void)initialize 67 | { 68 | if (self == [RNCachingURLProtocol class]) 69 | { 70 | static dispatch_once_t onceToken; 71 | dispatch_once(&onceToken, ^{ 72 | RNCachingSupportedSchemesMonitor = [NSObject new]; 73 | }); 74 | 75 | [self setSupportedSchemes:[NSSet setWithObject:@"http"]]; 76 | } 77 | } 78 | 79 | + (BOOL)canInitWithRequest:(NSURLRequest *)request 80 | { 81 | // only handle http requests we haven't marked with our header. 82 | if ([[self supportedSchemes] containsObject:[[request URL] scheme]] && 83 | ([request valueForHTTPHeaderField:RNCachingURLHeader] == nil)) 84 | { 85 | return YES; 86 | } 87 | return NO; 88 | } 89 | 90 | + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request 91 | { 92 | return request; 93 | } 94 | 95 | - (NSString *)cachePathForRequest:(NSURLRequest *)aRequest 96 | { 97 | // This stores in the Caches directory, which can be deleted when space is low, but we only use it for offline access 98 | NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; 99 | NSString *fileName = [[[aRequest URL] absoluteString] sha1]; 100 | 101 | return [cachesPath stringByAppendingPathComponent:fileName]; 102 | } 103 | 104 | - (void)startLoading 105 | { 106 | if (![self useCache]) { 107 | NSMutableURLRequest *connectionRequest = 108 | #if WORKAROUND_MUTABLE_COPY_LEAK 109 | [[self request] mutableCopyWorkaround]; 110 | #else 111 | [[self request] mutableCopy]; 112 | #endif 113 | // we need to mark this request with our header so we know not to handle it in +[NSURLProtocol canInitWithRequest:]. 114 | [connectionRequest setValue:@"" forHTTPHeaderField:RNCachingURLHeader]; 115 | NSURLConnection *connection = [NSURLConnection connectionWithRequest:connectionRequest 116 | delegate:self]; 117 | [self setConnection:connection]; 118 | } 119 | else { 120 | RNCachedData *cache = [NSKeyedUnarchiver unarchiveObjectWithFile:[self cachePathForRequest:[self request]]]; 121 | if (cache) { 122 | NSData *data = [cache data]; 123 | NSURLResponse *response = [cache response]; 124 | NSURLRequest *redirectRequest = [cache redirectRequest]; 125 | if (redirectRequest) { 126 | [[self client] URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:response]; 127 | } else { 128 | 129 | [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; // we handle caching ourselves. 130 | [[self client] URLProtocol:self didLoadData:data]; 131 | [[self client] URLProtocolDidFinishLoading:self]; 132 | } 133 | } 134 | else { 135 | [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotConnectToHost userInfo:nil]]; 136 | } 137 | } 138 | } 139 | 140 | - (void)stopLoading 141 | { 142 | [[self connection] cancel]; 143 | } 144 | 145 | // NSURLConnection delegates (generally we pass these on to our client) 146 | 147 | - (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response 148 | { 149 | // Thanks to Nick Dowell https://gist.github.com/1885821 150 | if (response != nil) { 151 | NSMutableURLRequest *redirectableRequest = 152 | #if WORKAROUND_MUTABLE_COPY_LEAK 153 | [request mutableCopyWorkaround]; 154 | #else 155 | [request mutableCopy]; 156 | #endif 157 | // We need to remove our header so we know to handle this request and cache it. 158 | // There are 3 requests in flight: the outside request, which we handled, the internal request, 159 | // which we marked with our header, and the redirectableRequest, which we're modifying here. 160 | // The redirectable request will cause a new outside request from the NSURLProtocolClient, which 161 | // must not be marked with our header. 162 | [redirectableRequest setValue:nil forHTTPHeaderField:RNCachingURLHeader]; 163 | 164 | NSString *cachePath = [self cachePathForRequest:[self request]]; 165 | RNCachedData *cache = [RNCachedData new]; 166 | [cache setResponse:response]; 167 | [cache setData:[self data]]; 168 | [cache setRedirectRequest:redirectableRequest]; 169 | [NSKeyedArchiver archiveRootObject:cache toFile:cachePath]; 170 | [[self client] URLProtocol:self wasRedirectedToRequest:redirectableRequest redirectResponse:response]; 171 | return redirectableRequest; 172 | } else { 173 | return request; 174 | } 175 | } 176 | 177 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 178 | { 179 | [[self client] URLProtocol:self didLoadData:data]; 180 | [self appendData:data]; 181 | } 182 | 183 | - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 184 | { 185 | [[self client] URLProtocol:self didFailWithError:error]; 186 | [self setConnection:nil]; 187 | [self setData:nil]; 188 | [self setResponse:nil]; 189 | } 190 | 191 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 192 | { 193 | [self setResponse:response]; 194 | [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; // We cache ourselves. 195 | } 196 | 197 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection 198 | { 199 | [[self client] URLProtocolDidFinishLoading:self]; 200 | 201 | NSString *cachePath = [self cachePathForRequest:[self request]]; 202 | RNCachedData *cache = [RNCachedData new]; 203 | [cache setResponse:[self response]]; 204 | [cache setData:[self data]]; 205 | [NSKeyedArchiver archiveRootObject:cache toFile:cachePath]; 206 | 207 | [self setConnection:nil]; 208 | [self setData:nil]; 209 | [self setResponse:nil]; 210 | } 211 | 212 | - (BOOL) useCache 213 | { 214 | BOOL reachable = (BOOL) [[Reachability reachabilityWithHostName:[[[self request] URL] host]] currentReachabilityStatus] != NotReachable; 215 | return !reachable; 216 | } 217 | 218 | - (void)appendData:(NSData *)newData 219 | { 220 | if ([self data] == nil) { 221 | [self setData:[newData mutableCopy]]; 222 | } 223 | else { 224 | [[self data] appendData:newData]; 225 | } 226 | } 227 | 228 | + (NSSet *)supportedSchemes { 229 | NSSet *supportedSchemes; 230 | @synchronized(RNCachingSupportedSchemesMonitor) 231 | { 232 | supportedSchemes = RNCachingSupportedSchemes; 233 | } 234 | return supportedSchemes; 235 | } 236 | 237 | + (void)setSupportedSchemes:(NSSet *)supportedSchemes 238 | { 239 | @synchronized(RNCachingSupportedSchemesMonitor) 240 | { 241 | RNCachingSupportedSchemes = supportedSchemes; 242 | } 243 | } 244 | 245 | @end 246 | 247 | static NSString *const kDataKey = @"data"; 248 | static NSString *const kResponseKey = @"response"; 249 | static NSString *const kRedirectRequestKey = @"redirectRequest"; 250 | 251 | @implementation RNCachedData 252 | @synthesize data = data_; 253 | @synthesize response = response_; 254 | @synthesize redirectRequest = redirectRequest_; 255 | 256 | - (void)encodeWithCoder:(NSCoder *)aCoder 257 | { 258 | [aCoder encodeObject:[self data] forKey:kDataKey]; 259 | [aCoder encodeObject:[self response] forKey:kResponseKey]; 260 | [aCoder encodeObject:[self redirectRequest] forKey:kRedirectRequestKey]; 261 | } 262 | 263 | - (id)initWithCoder:(NSCoder *)aDecoder 264 | { 265 | self = [super init]; 266 | if (self != nil) { 267 | [self setData:[aDecoder decodeObjectForKey:kDataKey]]; 268 | [self setResponse:[aDecoder decodeObjectForKey:kResponseKey]]; 269 | [self setRedirectRequest:[aDecoder decodeObjectForKey:kRedirectRequestKey]]; 270 | } 271 | 272 | return self; 273 | } 274 | 275 | @end 276 | 277 | #if WORKAROUND_MUTABLE_COPY_LEAK 278 | @implementation NSURLRequest(MutableCopyWorkaround) 279 | 280 | - (id) mutableCopyWorkaround { 281 | NSMutableURLRequest *mutableURLRequest = [[NSMutableURLRequest alloc] initWithURL:[self URL] 282 | cachePolicy:[self cachePolicy] 283 | timeoutInterval:[self timeoutInterval]]; 284 | [mutableURLRequest setAllHTTPHeaderFields:[self allHTTPHeaderFields]]; 285 | if ([self HTTPBodyStream]) { 286 | [mutableURLRequest setHTTPBodyStream:[self HTTPBodyStream]]; 287 | } else { 288 | [mutableURLRequest setHTTPBody:[self HTTPBody]]; 289 | } 290 | [mutableURLRequest setHTTPMethod:[self HTTPMethod]]; 291 | 292 | return mutableURLRequest; 293 | } 294 | 295 | @end 296 | #endif 297 | -------------------------------------------------------------------------------- /ios/RNCachingURLProtocol/Reachability.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | Basic demonstration of how to use the SystemConfiguration Reachablity APIs. 7 | */ 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | 14 | typedef enum : NSInteger { 15 | NotReachable = 0, 16 | ReachableViaWiFi, 17 | ReachableViaWWAN 18 | } NetworkStatus; 19 | 20 | #pragma mark IPv6 Support 21 | //Reachability fully support IPv6. For full details, see ReadMe.md. 22 | 23 | 24 | extern NSString *kReachabilityChangedNotification; 25 | 26 | 27 | @interface Reachability : NSObject 28 | 29 | /*! 30 | * Use to check the reachability of a given host name. 31 | */ 32 | + (instancetype)reachabilityWithHostName:(NSString *)hostName; 33 | 34 | /*! 35 | * Use to check the reachability of a given IP address. 36 | */ 37 | + (instancetype)reachabilityWithAddress:(const struct sockaddr *)hostAddress; 38 | 39 | /*! 40 | * Checks whether the default route is available. Should be used by applications that do not connect to a particular host. 41 | */ 42 | + (instancetype)reachabilityForInternetConnection; 43 | 44 | 45 | #pragma mark reachabilityForLocalWiFi 46 | //reachabilityForLocalWiFi has been removed from the sample. See ReadMe.md for more information. 47 | //+ (instancetype)reachabilityForLocalWiFi; 48 | 49 | /*! 50 | * Start listening for reachability notifications on the current run loop. 51 | */ 52 | - (BOOL)startNotifier; 53 | - (void)stopNotifier; 54 | 55 | - (NetworkStatus)currentReachabilityStatus; 56 | 57 | /*! 58 | * WWAN may be available, but not active until a connection has been established. WiFi may require a connection for VPN on Demand. 59 | */ 60 | - (BOOL)connectionRequired; 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /ios/RNCachingURLProtocol/Reachability.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | Basic demonstration of how to use the SystemConfiguration Reachablity APIs. 7 | */ 8 | 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | 15 | #import 16 | 17 | #import "Reachability.h" 18 | 19 | #pragma mark IPv6 Support 20 | //Reachability fully support IPv6. For full details, see ReadMe.md. 21 | 22 | 23 | NSString *kReachabilityChangedNotification = @"kNetworkReachabilityChangedNotification"; 24 | 25 | 26 | #pragma mark - Supporting functions 27 | 28 | #define kShouldPrintReachabilityFlags 1 29 | 30 | static void PrintReachabilityFlags(SCNetworkReachabilityFlags flags, const char* comment) 31 | { 32 | #if kShouldPrintReachabilityFlags 33 | 34 | NSLog(@"Reachability Flag Status: %c%c %c%c%c%c%c%c%c %s\n", 35 | (flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-', 36 | (flags & kSCNetworkReachabilityFlagsReachable) ? 'R' : '-', 37 | 38 | (flags & kSCNetworkReachabilityFlagsTransientConnection) ? 't' : '-', 39 | (flags & kSCNetworkReachabilityFlagsConnectionRequired) ? 'c' : '-', 40 | (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-', 41 | (flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-', 42 | (flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-', 43 | (flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-', 44 | (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-', 45 | comment 46 | ); 47 | #endif 48 | } 49 | 50 | 51 | static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) 52 | { 53 | #pragma unused (target, flags) 54 | NSCAssert(info != NULL, @"info was NULL in ReachabilityCallback"); 55 | NSCAssert([(__bridge NSObject*) info isKindOfClass: [Reachability class]], @"info was wrong class in ReachabilityCallback"); 56 | 57 | Reachability* noteObject = (__bridge Reachability *)info; 58 | // Post a notification to notify the client that the network reachability changed. 59 | [[NSNotificationCenter defaultCenter] postNotificationName: kReachabilityChangedNotification object: noteObject]; 60 | } 61 | 62 | 63 | #pragma mark - Reachability implementation 64 | 65 | @implementation Reachability 66 | { 67 | SCNetworkReachabilityRef _reachabilityRef; 68 | } 69 | 70 | + (instancetype)reachabilityWithHostName:(NSString *)hostName 71 | { 72 | Reachability* returnValue = NULL; 73 | SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithName(NULL, [hostName UTF8String]); 74 | if (reachability != NULL) 75 | { 76 | returnValue= [[self alloc] init]; 77 | if (returnValue != NULL) 78 | { 79 | returnValue->_reachabilityRef = reachability; 80 | } 81 | else { 82 | CFRelease(reachability); 83 | } 84 | } 85 | return returnValue; 86 | } 87 | 88 | 89 | + (instancetype)reachabilityWithAddress:(const struct sockaddr *)hostAddress 90 | { 91 | SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, hostAddress); 92 | 93 | Reachability* returnValue = NULL; 94 | 95 | if (reachability != NULL) 96 | { 97 | returnValue = [[self alloc] init]; 98 | if (returnValue != NULL) 99 | { 100 | returnValue->_reachabilityRef = reachability; 101 | } 102 | else { 103 | CFRelease(reachability); 104 | } 105 | } 106 | return returnValue; 107 | } 108 | 109 | 110 | + (instancetype)reachabilityForInternetConnection 111 | { 112 | struct sockaddr_in zeroAddress; 113 | bzero(&zeroAddress, sizeof(zeroAddress)); 114 | zeroAddress.sin_len = sizeof(zeroAddress); 115 | zeroAddress.sin_family = AF_INET; 116 | 117 | return [self reachabilityWithAddress: (const struct sockaddr *) &zeroAddress]; 118 | } 119 | 120 | #pragma mark reachabilityForLocalWiFi 121 | //reachabilityForLocalWiFi has been removed from the sample. See ReadMe.md for more information. 122 | //+ (instancetype)reachabilityForLocalWiFi 123 | 124 | 125 | 126 | #pragma mark - Start and stop notifier 127 | 128 | - (BOOL)startNotifier 129 | { 130 | BOOL returnValue = NO; 131 | SCNetworkReachabilityContext context = {0, (__bridge void *)(self), NULL, NULL, NULL}; 132 | 133 | if (SCNetworkReachabilitySetCallback(_reachabilityRef, ReachabilityCallback, &context)) 134 | { 135 | if (SCNetworkReachabilityScheduleWithRunLoop(_reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode)) 136 | { 137 | returnValue = YES; 138 | } 139 | } 140 | 141 | return returnValue; 142 | } 143 | 144 | 145 | - (void)stopNotifier 146 | { 147 | if (_reachabilityRef != NULL) 148 | { 149 | SCNetworkReachabilityUnscheduleFromRunLoop(_reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); 150 | } 151 | } 152 | 153 | 154 | - (void)dealloc 155 | { 156 | [self stopNotifier]; 157 | if (_reachabilityRef != NULL) 158 | { 159 | CFRelease(_reachabilityRef); 160 | } 161 | } 162 | 163 | 164 | #pragma mark - Network Flag Handling 165 | 166 | - (NetworkStatus)networkStatusForFlags:(SCNetworkReachabilityFlags)flags 167 | { 168 | PrintReachabilityFlags(flags, "networkStatusForFlags"); 169 | if ((flags & kSCNetworkReachabilityFlagsReachable) == 0) 170 | { 171 | // The target host is not reachable. 172 | return NotReachable; 173 | } 174 | 175 | NetworkStatus returnValue = NotReachable; 176 | 177 | if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0) 178 | { 179 | /* 180 | If the target host is reachable and no connection is required then we'll assume (for now) that you're on Wi-Fi... 181 | */ 182 | returnValue = ReachableViaWiFi; 183 | } 184 | 185 | if ((((flags & kSCNetworkReachabilityFlagsConnectionOnDemand ) != 0) || 186 | (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0)) 187 | { 188 | /* 189 | ... and the connection is on-demand (or on-traffic) if the calling application is using the CFSocketStream or higher APIs... 190 | */ 191 | 192 | if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0) 193 | { 194 | /* 195 | ... and no [user] intervention is needed... 196 | */ 197 | returnValue = ReachableViaWiFi; 198 | } 199 | } 200 | 201 | if ((flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN) 202 | { 203 | /* 204 | ... but WWAN connections are OK if the calling application is using the CFNetwork APIs. 205 | */ 206 | returnValue = ReachableViaWWAN; 207 | } 208 | 209 | return returnValue; 210 | } 211 | 212 | 213 | - (BOOL)connectionRequired 214 | { 215 | NSAssert(_reachabilityRef != NULL, @"connectionRequired called with NULL reachabilityRef"); 216 | SCNetworkReachabilityFlags flags; 217 | 218 | if (SCNetworkReachabilityGetFlags(_reachabilityRef, &flags)) 219 | { 220 | return (flags & kSCNetworkReachabilityFlagsConnectionRequired); 221 | } 222 | 223 | return NO; 224 | } 225 | 226 | 227 | - (NetworkStatus)currentReachabilityStatus 228 | { 229 | NSAssert(_reachabilityRef != NULL, @"currentNetworkStatus called with NULL SCNetworkReachabilityRef"); 230 | NetworkStatus returnValue = NotReachable; 231 | SCNetworkReachabilityFlags flags; 232 | 233 | if (SCNetworkReachabilityGetFlags(_reachabilityRef, &flags)) 234 | { 235 | returnValue = [self networkStatusForFlags:flags]; 236 | } 237 | 238 | return returnValue; 239 | } 240 | 241 | 242 | @end 243 | -------------------------------------------------------------------------------- /ios/UIScrollView+adjustedContentInset.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+adjustedContentInset.h 3 | // RNAdvancedWebView 4 | // 5 | // Created by Bell Zhong on 2018/5/3. 6 | // Copyright © 2018年 shimo. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIScrollView (adjustedContentInset) 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/UIScrollView+adjustedContentInset.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+adjustedContentInset.m 3 | // RNAdvancedWebView 4 | // 5 | // Created by Bell Zhong on 2018/5/3. 6 | // Copyright © 2018年 shimo. All rights reserved. 7 | // 8 | 9 | #import "UIScrollView+adjustedContentInset.h" 10 | #import 11 | 12 | /** 13 | fix: `_webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;` do not work 14 | */ 15 | @implementation UIScrollView (adjustedContentInset) 16 | 17 | + (void)load { 18 | static dispatch_once_t onceToken; 19 | dispatch_once(&onceToken, ^{ 20 | if (@available(iOS 11.0, *)) { 21 | Method originMethod = class_getInstanceMethod(self, @selector(adjustedContentInset)); 22 | Method presentMethod = class_getInstanceMethod(self, @selector(shm_adjustedContentInset)); 23 | method_exchangeImplementations(originMethod, presentMethod); 24 | } 25 | }); 26 | } 27 | 28 | - (UIEdgeInsets)shm_adjustedContentInset { 29 | if (@available(iOS 11.0, *)) { 30 | if (self.contentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentNever && [self isKindOfClass:NSClassFromString(@"WKScrollView")]) { 31 | return UIEdgeInsetsZero; 32 | } 33 | } 34 | return [self shm_adjustedContentInset]; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-offline-cache-webview", 3 | "version": "0.1.1", 4 | "description": "Offline cache enabled WebView", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/zhangtaii/react-native-offline-cache-webview.git" 8 | }, 9 | "license": "MIT", 10 | "main": "./WebView", 11 | "keywords": [ 12 | "react-component", 13 | "react-native", 14 | "webview", 15 | "cookie", 16 | "jsbridge", 17 | "ios", 18 | "android", 19 | "offline", 20 | "cache", 21 | "website" 22 | ], 23 | "scripts": { 24 | "lint": "eslint ./" 25 | }, 26 | "devDependencies": { 27 | "babel-eslint": "^6.1.2", 28 | "eslint": "^2.13.1", 29 | "eslint-plugin-react": "^4.3.0" 30 | }, 31 | "peerDependencies": { 32 | "react-native": ">=0.46.0", 33 | "react": "16.0.0-alpha.12", 34 | "prop-types": "^15.5.8" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/zhangtaii/react-native-offline-cache-webview/issues" 38 | }, 39 | "homepage": "https://github.com/zhangtaii/react-native-offline-cache-webview#readme", 40 | "directories": { 41 | "example": "example" 42 | }, 43 | "author": "zhangtaii" 44 | } 45 | --------------------------------------------------------------------------------