├── src ├── index.js └── WebViewJSContext.js ├── android ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── shaynesweeney │ │ └── react_native_webview_js_context │ │ ├── RNWebViewJSContextPackage.java │ │ └── RNWebViewJSContextModule.java └── build.gradle ├── ios ├── RNWebViewJSContext │ ├── RNWebViewJSContext.h │ └── RNWebViewJSContext.m └── RNWebViewJSContext.xcodeproj │ └── project.pbxproj ├── .gitignore ├── react-native-webview-js-context.podspec ├── package.json ├── LICENSE ├── .flowconfig └── README.md /src/index.js: -------------------------------------------------------------------------------- 1 | import WebViewJSContext from './WebViewJSContext'; 2 | 3 | module.exports = WebViewJSContext; 4 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ios/RNWebViewJSContext/RNWebViewJSContext.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNWebViewJSContext.h 3 | // RNWebViewJSContext 4 | // 5 | // Created by Shayne Sweeney on 10/21/15. 6 | // Copyright © 2015 Shayne Sweeney. All rights reserved. 7 | // 8 | 9 | #import "RCTBridgeModule.h" 10 | 11 | @interface RNWebViewJSContext : NSObject 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | .DS_Store 3 | 4 | # node.js 5 | node_modules/ 6 | npm-debug.log 7 | 8 | # Xcode 9 | build/ 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | *.xcworkspace 19 | !default.xcworkspace 20 | xcuserdata 21 | profile 22 | *.moved-aside 23 | DerivedData 24 | .idea/ 25 | 26 | # Pods 27 | Pods 28 | -------------------------------------------------------------------------------- /react-native-webview-js-context.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "react-native-webview-js-context" 3 | s.version = "0.2.0" 4 | s.summary = "Utilize the JavaScript VM running inside an iOS UIWebView to exploit libraries targeting the DOM (e.g. Google Charts)" 5 | 6 | s.homepage = "https://github.com/shayne/react-native-webview-js-context" 7 | 8 | s.license = "MIT" 9 | s.platform = :ios, "8.0" 10 | 11 | s.source = { :git => "https://github.com/shayne/react-native-webview-js-context" } 12 | 13 | s.source_files = "ios/*.{h,m}" 14 | 15 | s.dependency 'React' 16 | end 17 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.1" 6 | 7 | defaultConfig { 8 | minSdkVersion 16 9 | targetSdkVersion 22 10 | versionCode 1 11 | versionName "0.2.0" 12 | } 13 | 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | 21 | // needed for https://github.com/square/okio/issues/58 22 | lintOptions { 23 | warning 'InvalidPackage' 24 | } 25 | } 26 | 27 | dependencies { 28 | compile fileTree(dir: 'libs', include: ['*.jar']) 29 | compile 'com.facebook.react:react-native:0.11.+' 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-webview-js-context", 3 | "version": "0.2.3", 4 | "description": "Utilize the JavaScript VM running inside an iOS UIWebView to exploit libraries targeting the DOM (e.g. Google Charts).", 5 | "main": "src/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/shayne/react-native-webview-js-context" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/shayne/react-native-webview-js-context/issues" 12 | }, 13 | "homepage": { 14 | "url": "https://github.com/shayne/react-native-webview-js-context#readme" 15 | }, 16 | "keywords": [ 17 | "dom", 18 | "web", 19 | "jsc", 20 | "ios", 21 | "android", 22 | "react", 23 | "react-native", 24 | "react-component" 25 | ], 26 | "author": "Shayne Sweeney ", 27 | "license": "MIT", 28 | "peerDependencies": { 29 | "react-native": "*" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Shayne Sweeney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/WebViewJSContext.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @flow 3 | */ 4 | 5 | var React = require('react-native'); 6 | 7 | var { Platform, NativeModules: { RNWebViewJSContext } } = React; 8 | 9 | export default class WebViewJSContext { 10 | ctx: any; 11 | 12 | static async createWithHTML(html: string): Promise { 13 | if (Platform.OS === 'android') { 14 | return new Promise((resolve, reject) => { 15 | RNWebViewJSContext.loadHTML(html, ctx => { 16 | resolve(new WebViewJSContext(ctx)); 17 | }, reject); 18 | }); 19 | } 20 | const ctx = await RNWebViewJSContext.loadHTML(html); 21 | return new WebViewJSContext(ctx); 22 | } 23 | 24 | constructor(ctx: any) { 25 | this.ctx = ctx; 26 | } 27 | 28 | evaluateScript(script: string): Promise { 29 | console.log('HERE'); 30 | if (Platform.OS === 'android') { 31 | return new Promise((resolve, reject) => { 32 | RNWebViewJSContext.evaluateScript(this.ctx, script, resolve, reject); 33 | }); 34 | } 35 | return RNWebViewJSContext.evaluateScript(this.ctx, script); 36 | } 37 | 38 | destroy() { 39 | RNWebViewJSContext.destroy(this.ctx); 40 | this.ctx = null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /android/src/main/java/com/shaynesweeney/react_native_webview_js_context/RNWebViewJSContextPackage.java: -------------------------------------------------------------------------------- 1 | package com.shaynesweeney.react_native_webview_js_context; 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.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | 14 | public final class RNWebViewJSContextPackage implements ReactPackage { 15 | 16 | @Override 17 | public List createNativeModules( 18 | ReactApplicationContext reactContext) { 19 | List modules = new ArrayList<>(); 20 | 21 | modules.add(new RNWebViewJSContextModule(reactContext)); 22 | 23 | return modules; 24 | } 25 | 26 | @Override 27 | public List> createJSModules() { 28 | return Collections.emptyList(); 29 | } 30 | 31 | @Override 32 | public List createViewManagers( 33 | ReactApplicationContext reactContext) { 34 | return Collections.emptyList(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | # We fork some components by platform. 4 | .*/*.web.js 5 | .*/*.android.js 6 | 7 | # Some modules have their own node_modules with overlap 8 | .*/node_modules/node-haste/.* 9 | 10 | # Ignore react-tools where there are overlaps, but don't ignore anything that 11 | # react-native relies on 12 | .*/node_modules/react-tools/src/React.js 13 | .*/node_modules/react-tools/src/renderers/shared/event/EventPropagators.js 14 | .*/node_modules/react-tools/src/renderers/shared/event/eventPlugins/ResponderEventPlugin.js 15 | .*/node_modules/react-tools/src/shared/vendor/core/ExecutionEnvironment.js 16 | 17 | # Ignore commoner tests 18 | .*/node_modules/commoner/test/.* 19 | 20 | # See https://github.com/facebook/flow/issues/442 21 | .*/react-tools/node_modules/commoner/lib/reader.js 22 | 23 | # Ignore jest 24 | .*/node_modules/jest-cli/.* 25 | 26 | # Ignore Website 27 | .*/website/.* 28 | 29 | [include] 30 | 31 | [libs] 32 | node_modules/react-native/Libraries/react-native/react-native-interface.js 33 | 34 | [options] 35 | module.system=haste 36 | 37 | munge_underscores=true 38 | 39 | module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' 40 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.png$' -> 'RelativeImageStub' 41 | 42 | suppress_type=$FlowIssue 43 | suppress_type=$FlowFixMe 44 | suppress_type=$FixMe 45 | 46 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-6]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 47 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-6]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+ 48 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 49 | 50 | [version] 51 | 0.16.0 52 | -------------------------------------------------------------------------------- /ios/RNWebViewJSContext/RNWebViewJSContext.m: -------------------------------------------------------------------------------- 1 | // 2 | // RNWebViewJSContext.m 3 | // RNWebViewJSContext 4 | // 5 | // Created by Shayne Sweeney on 10/21/15. 6 | // Copyright © 2015 Shayne Sweeney. All rights reserved. 7 | // 8 | 9 | @import UIKit; 10 | @import JavaScriptCore; 11 | 12 | #import "RNWebViewJSContext.h" 13 | 14 | #pragma mark - UIWebView (JSContext) 15 | 16 | @interface UIWebView (JSContext) 17 | 18 | - (JSContext *)JSContext; 19 | 20 | @end 21 | 22 | 23 | @implementation UIWebView (JSContext) 24 | 25 | - (JSContext *)JSContext { 26 | return [self valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; 27 | } 28 | 29 | @end 30 | 31 | #pragma mark - 32 | 33 | 34 | @interface RNWebViewJSContext () 35 | 36 | @property (nonatomic, strong) NSMutableDictionary *webViews; 37 | 38 | @end 39 | 40 | @implementation RNWebViewJSContext 41 | 42 | RCT_EXPORT_MODULE() 43 | 44 | - (instancetype)init { 45 | if (self = [super init]) { 46 | self.webViews = [NSMutableDictionary dictionary]; 47 | } 48 | return self; 49 | } 50 | 51 | 52 | RCT_EXPORT_METHOD(destroy:(nonnull NSNumber *)contextID) 53 | { 54 | [self.webViews removeObjectForKey:contextID]; 55 | } 56 | 57 | 58 | RCT_EXPORT_METHOD(loadHTML:(NSString *)htmlString resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 59 | { 60 | 61 | NSUInteger hash = [htmlString hash]; 62 | 63 | __weak RNWebViewJSContext *weakSelf = self; 64 | dispatch_async(dispatch_get_main_queue(), ^{ 65 | __strong RNWebViewJSContext *strongSelf = weakSelf; 66 | UIWebView *webView = self.webViews[@(hash)]; 67 | if (!webView) { 68 | webView = [[UIWebView alloc] initWithFrame:CGRectZero]; 69 | strongSelf.webViews[@(hash)] = webView; 70 | } 71 | 72 | JSContext *context = [webView JSContext]; 73 | 74 | RCTPromiseResolveBlock resolveWrapper = ^(id obj) { 75 | resolve(@(hash)); 76 | }; 77 | 78 | [self generateCallbacksInContext:context resolver:resolveWrapper rejecter:reject UUIDSuffix:false]; 79 | 80 | __weak UIWebView *weakWebView = webView; 81 | dispatch_async(dispatch_get_main_queue(), ^{ 82 | __strong UIWebView *strongWebView = weakWebView; 83 | [strongWebView loadHTMLString:htmlString baseURL:nil]; 84 | }); 85 | }); 86 | } 87 | 88 | RCT_REMAP_METHOD(loadModule, loadModuleInContext:(nonnull NSNumber *)contextID script:(NSString *)script resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 89 | { 90 | 91 | } 92 | 93 | 94 | RCT_REMAP_METHOD(evaluateScript, evaluateScriptInContext:(nonnull NSNumber *)contextID script:(NSString *)script resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 95 | { 96 | UIWebView *webView = self.webViews[contextID]; 97 | NSAssert(webView, @"WebView was nil!"); 98 | JSContext *context = [webView JSContext]; 99 | 100 | NSDictionary *callbacks = [self generateCallbacksInContext:context resolver:resolve rejecter:reject UUIDSuffix:true]; 101 | 102 | NSString *jsWrapper = [NSString stringWithFormat:@"\ 103 | setTimeout(function(){ \ 104 | var resolve = %@, reject = %@; \ 105 | try { \ 106 | %@ \ 107 | } catch (e) { \ 108 | reject(e); \ 109 | } \ 110 | }, 0)", callbacks[@"resolveName"], callbacks[@"rejectName"], script]; 111 | 112 | dispatch_async(dispatch_get_main_queue(), ^{ 113 | [context evaluateScript:jsWrapper]; 114 | }); 115 | 116 | } 117 | 118 | 119 | - (NSDictionary *)generateCallbacksInContext:(JSContext *)context resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject UUIDSuffix:(BOOL)withSuffix { 120 | NSString *resolveName = @"resolve"; 121 | NSString *rejectName = @"reject"; 122 | 123 | if (withSuffix) { 124 | NSString *uniquePrefix = [[NSUUID UUID].UUIDString substringToIndex:7]; 125 | resolveName = [NSString stringWithFormat:@"resolve_%@", uniquePrefix]; 126 | rejectName = [NSString stringWithFormat:@"reject_%@", uniquePrefix]; 127 | } 128 | 129 | context[resolveName] = ^(JSValue *val) { 130 | resolve([val toString]); 131 | }; 132 | 133 | context[rejectName] = ^(JSValue *val) { 134 | NSString *description = [val toString]; 135 | NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: description }; 136 | NSError *error = [NSError errorWithDomain:@"JSWebViewManagerErrorDomain" 137 | code:-57 138 | userInfo:userInfo]; 139 | 140 | reject(@"-57", description, error); 141 | }; 142 | 143 | return @{@"resolveName": resolveName, @"rejectName": rejectName}; 144 | } 145 | 146 | @end 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-webview-js-context [![npm version](https://badge.fury.io/js/react-native-webview-js-context.svg)](http://badge.fury.io/js/react-native-webview-js-context) 2 | 3 | Interactive JavaScript between a UIWebView and React Native. 4 | 5 | **Example:** Google Charts used to render a chart (base64 encoded image) in a `` component 6 | 7 | 8 | 9 | ```javascript 10 | const GC_HTML = ` 11 | 12 | 13 | 14 | 18 | 19 |
20 | `; 21 | 22 | const CHART_JS = ` 23 | var data = new google.visualization.DataTable(); 24 | data.addColumn('date', 'Day'); 25 | data.addColumn('number', 'Weight'); 26 | data.addColumn({ type: 'string', role: 'annotation' }); 27 | data.addRows([ 28 | [new Date(2015, 2, 1), 150, '150'], 29 | [new Date(2015, 2, 2), 152, null], 30 | [new Date(2015, 2, 3), 146, '146'], 31 | [new Date(2015, 2, 4), 150, null], 32 | [new Date(2015, 2, 5), 157, '157'], 33 | [new Date(2015, 2, 06), 147, null], 34 | [new Date(2015, 2, 07), 147.5, '147'], 35 | ]); 36 | 37 | var options = { enableInteractivity: false, 38 | legend: {position: 'none'}, 39 | lineWidth: 3, width:750, height:420, 40 | pointShape: 'circle', pointSize: 8, 41 | chartArea: { left: 30, width: 690 }, areaOpacity: 0.07, 42 | colors: ['#e14c4d'], backgroundColor: { 'fill': '#34343f' }, 43 | annotations: { 44 | textStyle: { fontSize: 26, bold: true, color: '#bbbbbd', auroColor: '#3f3f3f' }, 45 | }, 46 | hAxis: { 47 | format: 'MMM d', 48 | textStyle: {color: '#bbbbbd', fontSize: 16,}, gridlines: { color: 'transparent' }, 49 | }, 50 | vAxis: { gridlines: { count: 3, color: '#3f414f' } }, 51 | }; 52 | 53 | var chart = new google.visualization.AreaChart(document.getElementById('chart_div')); 54 | chart.draw(data, options); 55 | 56 | resolve(chart.getImageURI()); /* <--- resolve() is called by RNWebViewJSContext */`; 57 | 58 | import WebViewJSContext from 'react-native-webview-js-context'; 59 | 60 | class RNCharts { 61 | state: { imageUri: null }; 62 | 63 | componentWillMount() { 64 | WebViewJSContext.createWithHTML(GC_HTML) 65 | .then(context => { 66 | this.ctx = context; 67 | this.loadChart(); 68 | }); 69 | } 70 | 71 | componentWillUnmount() { 72 | this.ctx && this.ctx.destroy(); 73 | }, 74 | 75 | render() { 76 | return this.state.imageUri ? 77 | 78 | : ; 79 | } 80 | 81 | async loadChart() { 82 | var imageUri = await this.ctx.evaluateScript(CHART_JS); 83 | this.setState({ imageUri }); 84 | } 85 | } 86 | ``` 87 | 88 | ## Usage 89 | 90 | First you need to install react-native-webview-js-context: 91 | 92 | ```javascript 93 | npm install react-native-webview-js-context --save 94 | ``` 95 | 96 | 97 | ## `iOS` 98 | 99 | 1. In XCode, in the project navigator, right click `Libraries` ➜ `Add Files to [your project's name]` 100 | 2. Go to `node_modules` ➜ `react-native-webview-js-context` ➜ `ios` and add `RNWebViewJSContext.xcodeproj` 101 | 3. In XCode, in the project navigator, select your project. Add `libRNWebViewJSContext.a` to your project's `Build Phases` ➜ `Link Binary With Libraries` 102 | 4. Run your project (`Cmd+R`) 103 | 104 | ### `Android` 105 | 106 | * `android/settings.gradle` 107 | 108 | ```gradle 109 | ... 110 | include ':react-native-webview-js-context' 111 | project(':react-native-webview-js-context').projectDir = new File(settingsDir, '../node_modules/react-native-webview-js-context/android') 112 | ``` 113 | * `android/app/build.gradle` 114 | 115 | ```gradle 116 | dependencies { 117 | ... 118 | compile project(':react-native-webview-js-context') 119 | } 120 | ``` 121 | 122 | * register module (in MainActivity.java) 123 | 124 | ```java 125 | ... 126 | 127 | import com.shaynesweeney.react_native_webview_js_context.RNWebViewJSContextPackage; // <--- IMPORT 128 | 129 | public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler { 130 | ... 131 | 132 | @Override 133 | protected void onCreate(Bundle savedInstanceState) { 134 | super.onCreate(savedInstanceState); 135 | mReactRootView = new ReactRootView(this); 136 | 137 | mReactInstanceManager = ReactInstanceManager.builder() 138 | .setApplication(getApplication()) 139 | .setBundleAssetName("index.android.bundle") 140 | .setJSMainModuleName("index.android") 141 | .addPackage(new MainReactPackage()) 142 | .addPackage(new RNWebViewJSContextPackage()) // <- ADD HERE 143 | .setUseDeveloperSupport(BuildConfig.DEBUG) 144 | .setInitialLifecycleState(LifecycleState.RESUMED) 145 | .build(); 146 | 147 | mReactRootView.startReactApplication(mReactInstanceManager, "YourProject", null); 148 | 149 | setContentView(mReactRootView); 150 | } 151 | } 152 | ``` 153 | 154 | -------------------------------------------------------------------------------- /android/src/main/java/com/shaynesweeney/react_native_webview_js_context/RNWebViewJSContextModule.java: -------------------------------------------------------------------------------- 1 | package com.shaynesweeney.react_native_webview_js_context; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Handler; 5 | import android.util.SparseArray; 6 | import android.webkit.JavascriptInterface; 7 | import android.webkit.WebView; 8 | 9 | import com.facebook.react.bridge.Callback; 10 | import com.facebook.react.bridge.ReactApplicationContext; 11 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 12 | import com.facebook.react.bridge.ReactMethod; 13 | 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.UUID; 17 | 18 | interface JSCallback { 19 | void invoke(String response); 20 | } 21 | 22 | interface JSCallbacks { 23 | @JavascriptInterface 24 | void global_resolver(); 25 | 26 | @JavascriptInterface 27 | void global_resolver(String response); 28 | 29 | @JavascriptInterface 30 | void global_rejecter(String response); 31 | 32 | @JavascriptInterface 33 | void global_rejecter(); 34 | 35 | @JavascriptInterface 36 | void callback_resolver(String uuid, String response); 37 | 38 | @JavascriptInterface 39 | void callback_resolver(String uuid); 40 | 41 | @JavascriptInterface 42 | void callback_rejecter(String uuid, String response); 43 | 44 | @JavascriptInterface 45 | void callback_rejecter(String uuid); 46 | } 47 | 48 | public class RNWebViewJSContextModule extends ReactContextBaseJavaModule { 49 | @SuppressWarnings("unused") 50 | private static final String TAG = "RNWebViewJSContext"; 51 | public static final String GLOBAL_REJECT_KEY = "global-reject"; 52 | private final SparseArray mWebViews; 53 | private final Map mCallbacks; 54 | public static final String GLOBAL_RESOLVE_KEY = "global-resolve"; 55 | 56 | public RNWebViewJSContextModule(ReactApplicationContext reactContext) { 57 | super(reactContext); 58 | 59 | mWebViews = new SparseArray<>(); 60 | mCallbacks = new HashMap<>(); 61 | } 62 | 63 | @Override 64 | public String getName() { 65 | return "RNWebViewJSContext"; 66 | } 67 | 68 | @ReactMethod 69 | public void loadHTML(final String html, final Callback resolveCallback, final Callback rejectCallback) { 70 | final Integer contextID = html.hashCode(); 71 | 72 | final ReactApplicationContext context = getReactApplicationContext(); 73 | Handler mainHandler = new Handler(context.getMainLooper()); 74 | Runnable myRunnable = new Runnable() { 75 | @Override 76 | public void run() { 77 | 78 | WebView webView = mWebViews.get(contextID); 79 | if (webView == null) { 80 | webView = createWebView(contextID); 81 | } 82 | 83 | mCallbacks.put("global-resolve", new JSCallback() { 84 | @Override 85 | public void invoke(String response) { 86 | resolveCallback.invoke(contextID); 87 | } 88 | }); 89 | 90 | mCallbacks.put("global-reject", new JSCallback() { 91 | @Override 92 | public void invoke(String response) { 93 | rejectCallback.invoke(); 94 | } 95 | }); 96 | 97 | webView.loadData(html, "text/html", "UTF-8"); 98 | // webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null); 99 | } 100 | }; 101 | mainHandler.post(myRunnable); 102 | } 103 | 104 | @ReactMethod 105 | public void evaluateScript(final Integer contextID, String script, final Callback resolveCallback, final Callback rejectCallback) { 106 | UUID uuid = UUID.randomUUID(); 107 | String uuidString = uuid.toString().substring(0, 7); 108 | 109 | final String resolverName = "resolve_" + uuidString; 110 | final String rejecterName = "reject_" + uuidString; 111 | 112 | final String jsWrapper = String.format( 113 | "setTimeout(function(){" 114 | + "var resolve = function(response) {" 115 | + " $RNWebViewJSContext.callback_resolver(\"%s\", response);" 116 | + "}," 117 | + "reject = function(uuid, response) {" 118 | + " $RNWebViewJSContext.callback_rejecter(\"%s\", response);" 119 | + "};" 120 | + "try {" 121 | + " %s" 122 | + "} catch (e) {" 123 | + " reject(e);" 124 | + "}" 125 | + "}, 0)", 126 | resolverName, rejecterName, script 127 | ); 128 | 129 | final ReactApplicationContext context = getReactApplicationContext(); 130 | Handler mainHandler = new Handler(context.getMainLooper()); 131 | Runnable myRunnable = new Runnable() { 132 | @Override 133 | public void run() { 134 | WebView webView = mWebViews.get(contextID); 135 | assert webView != null; 136 | 137 | mCallbacks.put(resolverName, new JSCallback() { 138 | @Override 139 | public void invoke(String response) { 140 | resolveCallback.invoke(response); 141 | } 142 | }); 143 | 144 | mCallbacks.put(rejecterName, new JSCallback() { 145 | @Override 146 | public void invoke(String response) { 147 | rejectCallback.invoke(); 148 | } 149 | }); 150 | 151 | webView.evaluateJavascript(jsWrapper, null); 152 | } 153 | }; 154 | mainHandler.post(myRunnable); 155 | } 156 | 157 | @ReactMethod 158 | public void destroy(Integer contextID) { 159 | mWebViews.delete(contextID); 160 | } 161 | 162 | 163 | @SuppressLint("SetJavaScriptEnabled") 164 | private WebView createWebView(Integer contextID) { 165 | 166 | WebView webView = new WebView(getReactApplicationContext()); 167 | webView.getSettings().setJavaScriptEnabled(true); 168 | mWebViews.put(contextID, webView); 169 | 170 | webView.addJavascriptInterface(new JSCallbacks() { 171 | 172 | @JavascriptInterface 173 | public void global_resolver() { 174 | JSCallback resolver = mCallbacks.get(GLOBAL_RESOLVE_KEY); 175 | resolver.invoke(null); 176 | } 177 | 178 | @JavascriptInterface 179 | public void global_resolver(String response) { 180 | JSCallback resolver = mCallbacks.get(GLOBAL_RESOLVE_KEY); 181 | resolver.invoke(response); 182 | } 183 | 184 | @JavascriptInterface 185 | public void global_rejecter() { 186 | JSCallback rejecter = mCallbacks.get(GLOBAL_REJECT_KEY); 187 | rejecter.invoke(null); 188 | } 189 | 190 | @JavascriptInterface 191 | public void global_rejecter(String response) { 192 | JSCallback rejecter = mCallbacks.get(GLOBAL_REJECT_KEY); 193 | rejecter.invoke(response); 194 | } 195 | 196 | @JavascriptInterface 197 | public void callback_resolver(String callbackKey) { 198 | JSCallback resolver = mCallbacks.get(callbackKey); 199 | resolver.invoke(null); 200 | } 201 | 202 | @JavascriptInterface 203 | public void callback_resolver(String callbackKey, String response) { 204 | JSCallback resolver = mCallbacks.get(callbackKey); 205 | resolver.invoke(response); 206 | } 207 | 208 | @JavascriptInterface 209 | public void callback_rejecter(String callbackKey) { 210 | JSCallback rejecter = mCallbacks.get(callbackKey); 211 | rejecter.invoke(null); 212 | } 213 | 214 | @JavascriptInterface 215 | public void callback_rejecter(String callbackKey, String response) { 216 | JSCallback rejecter = mCallbacks.get(callbackKey); 217 | rejecter.invoke(response); 218 | } 219 | }, "$RNWebViewJSContext"); 220 | 221 | 222 | webView.evaluateJavascript( 223 | "(function($w){" + 224 | "$w.resolve = $RNWebViewJSContext.global_resolver.bind($RNWebViewJSContext);" + 225 | "$w.reject = $RNWebViewJSContext.global_rejecter.bind($RNWebViewJSContext);" + 226 | "})(window);", null); 227 | 228 | 229 | return webView; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /ios/RNWebViewJSContext.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1C5F19C91BD87D96000069D7 /* RNWebViewJSContext.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1C5F19C81BD87D96000069D7 /* RNWebViewJSContext.h */; }; 11 | 1C5F19CB1BD87D96000069D7 /* RNWebViewJSContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C5F19CA1BD87D96000069D7 /* RNWebViewJSContext.m */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXCopyFilesBuildPhase section */ 15 | 1C5F19C31BD87D96000069D7 /* CopyFiles */ = { 16 | isa = PBXCopyFilesBuildPhase; 17 | buildActionMask = 2147483647; 18 | dstPath = "include/$(PRODUCT_NAME)"; 19 | dstSubfolderSpec = 16; 20 | files = ( 21 | 1C5F19C91BD87D96000069D7 /* RNWebViewJSContext.h in CopyFiles */, 22 | ); 23 | runOnlyForDeploymentPostprocessing = 0; 24 | }; 25 | /* End PBXCopyFilesBuildPhase section */ 26 | 27 | /* Begin PBXFileReference section */ 28 | 1C5F19C51BD87D96000069D7 /* libRNWebViewJSContext.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNWebViewJSContext.a; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | 1C5F19C81BD87D96000069D7 /* RNWebViewJSContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNWebViewJSContext.h; sourceTree = ""; }; 30 | 1C5F19CA1BD87D96000069D7 /* RNWebViewJSContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNWebViewJSContext.m; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | 1C5F19C21BD87D96000069D7 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 1C5F19BC1BD87D96000069D7 = { 45 | isa = PBXGroup; 46 | children = ( 47 | 1C5F19C71BD87D96000069D7 /* RNWebViewJSContext */, 48 | 1C5F19C61BD87D96000069D7 /* Products */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | 1C5F19C61BD87D96000069D7 /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 1C5F19C51BD87D96000069D7 /* libRNWebViewJSContext.a */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | 1C5F19C71BD87D96000069D7 /* RNWebViewJSContext */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 1C5F19C81BD87D96000069D7 /* RNWebViewJSContext.h */, 64 | 1C5F19CA1BD87D96000069D7 /* RNWebViewJSContext.m */, 65 | ); 66 | path = RNWebViewJSContext; 67 | sourceTree = ""; 68 | }; 69 | /* End PBXGroup section */ 70 | 71 | /* Begin PBXNativeTarget section */ 72 | 1C5F19C41BD87D96000069D7 /* RNWebViewJSContext */ = { 73 | isa = PBXNativeTarget; 74 | buildConfigurationList = 1C5F19CE1BD87D96000069D7 /* Build configuration list for PBXNativeTarget "RNWebViewJSContext" */; 75 | buildPhases = ( 76 | 1C5F19C11BD87D96000069D7 /* Sources */, 77 | 1C5F19C21BD87D96000069D7 /* Frameworks */, 78 | 1C5F19C31BD87D96000069D7 /* CopyFiles */, 79 | ); 80 | buildRules = ( 81 | ); 82 | dependencies = ( 83 | ); 84 | name = RNWebViewJSContext; 85 | productName = RNWebViewJSContext; 86 | productReference = 1C5F19C51BD87D96000069D7 /* libRNWebViewJSContext.a */; 87 | productType = "com.apple.product-type.library.static"; 88 | }; 89 | /* End PBXNativeTarget section */ 90 | 91 | /* Begin PBXProject section */ 92 | 1C5F19BD1BD87D96000069D7 /* Project object */ = { 93 | isa = PBXProject; 94 | attributes = { 95 | LastUpgradeCheck = 0700; 96 | ORGANIZATIONNAME = "Shayne Sweeney"; 97 | TargetAttributes = { 98 | 1C5F19C41BD87D96000069D7 = { 99 | CreatedOnToolsVersion = 7.0; 100 | }; 101 | }; 102 | }; 103 | buildConfigurationList = 1C5F19C01BD87D96000069D7 /* Build configuration list for PBXProject "RNWebViewJSContext" */; 104 | compatibilityVersion = "Xcode 3.2"; 105 | developmentRegion = English; 106 | hasScannedForEncodings = 0; 107 | knownRegions = ( 108 | en, 109 | ); 110 | mainGroup = 1C5F19BC1BD87D96000069D7; 111 | productRefGroup = 1C5F19C61BD87D96000069D7 /* Products */; 112 | projectDirPath = ""; 113 | projectRoot = ""; 114 | targets = ( 115 | 1C5F19C41BD87D96000069D7 /* RNWebViewJSContext */, 116 | ); 117 | }; 118 | /* End PBXProject section */ 119 | 120 | /* Begin PBXSourcesBuildPhase section */ 121 | 1C5F19C11BD87D96000069D7 /* Sources */ = { 122 | isa = PBXSourcesBuildPhase; 123 | buildActionMask = 2147483647; 124 | files = ( 125 | 1C5F19CB1BD87D96000069D7 /* RNWebViewJSContext.m in Sources */, 126 | ); 127 | runOnlyForDeploymentPostprocessing = 0; 128 | }; 129 | /* End PBXSourcesBuildPhase section */ 130 | 131 | /* Begin XCBuildConfiguration section */ 132 | 1C5F19CC1BD87D96000069D7 /* Debug */ = { 133 | isa = XCBuildConfiguration; 134 | buildSettings = { 135 | ALWAYS_SEARCH_USER_PATHS = NO; 136 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 137 | CLANG_CXX_LIBRARY = "libc++"; 138 | CLANG_ENABLE_MODULES = YES; 139 | CLANG_ENABLE_OBJC_ARC = YES; 140 | CLANG_WARN_BOOL_CONVERSION = YES; 141 | CLANG_WARN_CONSTANT_CONVERSION = YES; 142 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 143 | CLANG_WARN_EMPTY_BODY = YES; 144 | CLANG_WARN_ENUM_CONVERSION = YES; 145 | CLANG_WARN_INT_CONVERSION = YES; 146 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 147 | CLANG_WARN_UNREACHABLE_CODE = YES; 148 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 149 | COPY_PHASE_STRIP = NO; 150 | DEBUG_INFORMATION_FORMAT = dwarf; 151 | ENABLE_STRICT_OBJC_MSGSEND = YES; 152 | ENABLE_TESTABILITY = YES; 153 | GCC_C_LANGUAGE_STANDARD = gnu99; 154 | GCC_DYNAMIC_NO_PIC = NO; 155 | GCC_NO_COMMON_BLOCKS = YES; 156 | GCC_OPTIMIZATION_LEVEL = 0; 157 | GCC_PREPROCESSOR_DEFINITIONS = ( 158 | "DEBUG=1", 159 | "$(inherited)", 160 | ); 161 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 162 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 163 | GCC_WARN_UNDECLARED_SELECTOR = YES; 164 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 165 | GCC_WARN_UNUSED_FUNCTION = YES; 166 | GCC_WARN_UNUSED_VARIABLE = YES; 167 | HEADER_SEARCH_PATHS = ( 168 | "$(SRCROOT)/../../react-native/React/**", 169 | "$(SRCROOT)/node_modules/react-native/React/**", 170 | ); 171 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 172 | MTL_ENABLE_DEBUG_INFO = YES; 173 | ONLY_ACTIVE_ARCH = YES; 174 | SDKROOT = iphoneos; 175 | }; 176 | name = Debug; 177 | }; 178 | 1C5F19CD1BD87D96000069D7 /* Release */ = { 179 | isa = XCBuildConfiguration; 180 | buildSettings = { 181 | ALWAYS_SEARCH_USER_PATHS = NO; 182 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 183 | CLANG_CXX_LIBRARY = "libc++"; 184 | CLANG_ENABLE_MODULES = YES; 185 | CLANG_ENABLE_OBJC_ARC = YES; 186 | CLANG_WARN_BOOL_CONVERSION = YES; 187 | CLANG_WARN_CONSTANT_CONVERSION = YES; 188 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 189 | CLANG_WARN_EMPTY_BODY = YES; 190 | CLANG_WARN_ENUM_CONVERSION = YES; 191 | CLANG_WARN_INT_CONVERSION = YES; 192 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 193 | CLANG_WARN_UNREACHABLE_CODE = YES; 194 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 195 | COPY_PHASE_STRIP = NO; 196 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 197 | ENABLE_NS_ASSERTIONS = NO; 198 | ENABLE_STRICT_OBJC_MSGSEND = YES; 199 | GCC_C_LANGUAGE_STANDARD = gnu99; 200 | GCC_NO_COMMON_BLOCKS = YES; 201 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 202 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 203 | GCC_WARN_UNDECLARED_SELECTOR = YES; 204 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 205 | GCC_WARN_UNUSED_FUNCTION = YES; 206 | GCC_WARN_UNUSED_VARIABLE = YES; 207 | HEADER_SEARCH_PATHS = ( 208 | "$(SRCROOT)/../../react-native/React/**", 209 | "$(SRCROOT)/node_modules/react-native/React/**", 210 | ); 211 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 212 | MTL_ENABLE_DEBUG_INFO = NO; 213 | SDKROOT = iphoneos; 214 | VALIDATE_PRODUCT = YES; 215 | }; 216 | name = Release; 217 | }; 218 | 1C5F19CF1BD87D96000069D7 /* Debug */ = { 219 | isa = XCBuildConfiguration; 220 | buildSettings = { 221 | OTHER_LDFLAGS = "-ObjC"; 222 | PRODUCT_NAME = "$(TARGET_NAME)"; 223 | SKIP_INSTALL = YES; 224 | }; 225 | name = Debug; 226 | }; 227 | 1C5F19D01BD87D96000069D7 /* Release */ = { 228 | isa = XCBuildConfiguration; 229 | buildSettings = { 230 | OTHER_LDFLAGS = "-ObjC"; 231 | PRODUCT_NAME = "$(TARGET_NAME)"; 232 | SKIP_INSTALL = YES; 233 | }; 234 | name = Release; 235 | }; 236 | /* End XCBuildConfiguration section */ 237 | 238 | /* Begin XCConfigurationList section */ 239 | 1C5F19C01BD87D96000069D7 /* Build configuration list for PBXProject "RNWebViewJSContext" */ = { 240 | isa = XCConfigurationList; 241 | buildConfigurations = ( 242 | 1C5F19CC1BD87D96000069D7 /* Debug */, 243 | 1C5F19CD1BD87D96000069D7 /* Release */, 244 | ); 245 | defaultConfigurationIsVisible = 0; 246 | defaultConfigurationName = Release; 247 | }; 248 | 1C5F19CE1BD87D96000069D7 /* Build configuration list for PBXNativeTarget "RNWebViewJSContext" */ = { 249 | isa = XCConfigurationList; 250 | buildConfigurations = ( 251 | 1C5F19CF1BD87D96000069D7 /* Debug */, 252 | 1C5F19D01BD87D96000069D7 /* Release */, 253 | ); 254 | defaultConfigurationIsVisible = 0; 255 | defaultConfigurationName = Release; 256 | }; 257 | /* End XCConfigurationList section */ 258 | }; 259 | rootObject = 1C5F19BD1BD87D96000069D7 /* Project object */; 260 | } 261 | --------------------------------------------------------------------------------