react-native-browser-with-polyfill 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # react-native-browser-with-polyfill
2
+
3
+ A polyfill-injecting WebView browser component for React Native and Expo, built with a dependency injection pattern so it works across any SDK version.
4
+
5
+ ## Why Dependency Injection?
6
+
7
+ Expo SDK versions change frequently, and native module import paths shift between versions. Hardcoding imports of `react-native`, `react`, or `react-native-webview` causes build failures when your SDK version doesn't match.
8
+
9
+ This library sidesteps the problem entirely: instead of importing modules internally, `createBrowser()` accepts `React`, `ReactNative`, and `Webview` as parameters. The same package works with Expo SDK 45, SDK 50, or any version in between.
10
+
11
+ ## Install
12
+
13
+ Choose one of these two methods:
14
+
15
+ ```bash
16
+ # Install from npm
17
+ npm install react-native-browser-with-polyfill
18
+ ```
19
+
20
+ ```bash
21
+ # Install directly from GitHub (unpublished / bleeding-edge)
22
+ npm install github:lequanghuylc/react-native-browser-with-polyfill
23
+ ```
24
+
25
+ ### Using in Expo Snack
26
+
27
+ In Expo Snack, you don't run `npm install`. Instead, edit `package.json` directly and add the dependency:
28
+
29
+ ```json
30
+ {
31
+ "dependencies": {
32
+ "react-native-webview": "^13.0.0",
33
+ "react-native-browser-with-polyfill": "github:lequanghuylc/react-native-browser-with-polyfill"
34
+ }
35
+ }
36
+ ```
37
+
38
+ Expo Snack will automatically install the package from GitHub when you save.
39
+
40
+ ## Quick Start
41
+
42
+ ### Basic Usage (without polyfills)
43
+
44
+ Copy-paste ready for [Expo Snack](https://snack.expo.dev/):
45
+
46
+ ```jsx
47
+ import createBrowser from 'react-native-browser-with-polyfill';
48
+ import Webview from 'react-native-webview';
49
+ import * as React from 'react';
50
+ import * as ReactNative from 'react-native';
51
+
52
+ const { Browser } = createBrowser({ Webview, React, ReactNative });
53
+
54
+ export default function App() {
55
+ return <Browser initialUrl="https://browserleaks.com/js" />;
56
+ }
57
+ ```
58
+
59
+ ### With Polyfills (iPadOS 15 + Dev Keyboard Bar)
60
+
61
+ ```jsx
62
+ import createBrowser from 'react-native-browser-with-polyfill';
63
+ import Webview from 'react-native-webview';
64
+ import * as React from 'react';
65
+ import * as ReactNative from 'react-native';
66
+ import polyfillScript from 'react-native-browser-with-polyfill/src/polyfills/ipados15-polyfill';
67
+ import keyboardScript from 'react-native-browser-with-polyfill/src/polyfills/dev-keyboard-bar';
68
+
69
+ const { Browser } = createBrowser({ Webview, React, ReactNative });
70
+
71
+ export default function App() {
72
+ return (
73
+ <Browser
74
+ initialUrl="https://browserleaks.com/js"
75
+ polyfillScript={polyfillScript}
76
+ keyboardScript={keyboardScript}
77
+ />
78
+ );
79
+ }
80
+ ```
81
+
82
+ ## API
83
+
84
+ ### `createBrowser({ Webview, React, ReactNative })`
85
+
86
+ Creates a browser instance with dependency-injected modules.
87
+
88
+ **Parameters:**
89
+
90
+ | Param | Type | Description |
91
+ | ------------ | ------------ | ----------------------------------- |
92
+ | `Webview` | Component | `react-native-webview` WebView |
93
+ | `React` | Module | `react` (use `import * as React`) |
94
+ | `ReactNative`| Module | `react-native` (use `import * as ReactNative`) |
95
+
96
+ **Returns:**
97
+
98
+ ```ts
99
+ {
100
+ Browser, // Main screen component
101
+ WebViewScreen, // Standalone WebView screen
102
+ DevBar, // Developer console bar
103
+ useWebViewConsole // Hook for the console log viewer
104
+ }
105
+ ```
106
+
107
+ ### `Browser` Props
108
+
109
+ | Prop | Type | Default | Description |
110
+ | ---------------- | -------- | ------- | ----------------------------------------- |
111
+ | `initialUrl` | string | `"about:blank"` | URL to load on first render |
112
+ | `polyfillScript` | string | `null` | JavaScript to inject into every page (e.g. ipados15 polyfill) |
113
+ | `keyboardScript` | string | `null` | JavaScript to inject for the floating dev keyboard bar |
114
+
115
+ ### `useWebViewConsole()` Hook
116
+
117
+ Returns an object you can use to read and display the WebView's console output:
118
+
119
+ ```ts
120
+ {
121
+ logs: Array<{ time: string; type: string; content: string }>,
122
+ addLog: (log: { type: string; content: string }) => void,
123
+ clearLogs: () => void,
124
+ handleWebViewMessage: (event: any) => void
125
+ }
126
+ ```
127
+
128
+ ## Included Polyfills
129
+
130
+ ### `src/polyfills/ipados15-polyfill.js`
131
+
132
+ Safari 15 / iPadOS 15 compatibility polyfills:
133
+ - `Promise.withResolvers` polyfill
134
+ - `DOMParser` polyfill
135
+ - `AbortController` / `AbortSignal` polyfill
136
+ - `URLPattern` polyfill
137
+
138
+ ### `src/polyfills/dev-keyboard-bar.js`
139
+
140
+ Injects a floating toolbar at the bottom of the WebView that toggles a full-screen developer console overlay. Capture logs from the WebView and display them for debugging.
141
+
142
+ ## Architecture
143
+
144
+ ```
145
+ react-native-browser-with-polyfill/
146
+ ├── index.js # Single entry point — exports createBrowser
147
+ ├── package.json
148
+ ├── README.md
149
+ └── src/
150
+ ├── createBrowser.jsx # DI factory — accepts React, ReactNative, Webview
151
+ └── polyfills/
152
+ ├── ipados15-polyfill.js # Safari 15 compatibility shims
153
+ └── dev-keyboard-bar.js # Floating dev toolbar
154
+ ```
155
+
156
+ The library is intentionally minimal — a single entry point, no bundler required. It works with Expo, bare React Native, or any React Native CLI project.
157
+
158
+ ## License
159
+
160
+ MIT
package/index.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * expo-browser — Polyfill-injecting WebView browser for React Native / Expo
3
+ *
4
+ * Usage:
5
+ * import createBrowser from "expo-browser";
6
+ * import Webview from "react-native-webview";
7
+ * import * as React from "react";
8
+ * import * as ReactNative from "react-native";
9
+ *
10
+ * // Optional: import polyfill scripts as strings
11
+ * import polyfillScript from "expo-browser/src/polyfills/ipados15-polyfill";
12
+ * import keyboardScript from "expo-browser/src/polyfills/dev-keyboard-bar";
13
+ *
14
+ * const { Browser } = createBrowser({ Webview, React, ReactNative });
15
+ *
16
+ * export default function App() {
17
+ * return (
18
+ * <Browser
19
+ * initialUrl="https://browserleaks.com/js"
20
+ * polyfillScript={polyfillScript}
21
+ * keyboardScript={keyboardScript}
22
+ * />
23
+ * );
24
+ * }
25
+ */
26
+ const createBrowser = require("./src/createBrowser");
27
+ module.exports = createBrowser;
28
+ module.exports.default = createBrowser;
29
+ module.exports.createBrowser = createBrowser;
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "react-native-browser-with-polyfill",
3
+ "version": "1.0.0",
4
+ "description": "Polyfill-injecting WebView browser for React Native / Expo. Uses dependency injection to avoid SDK version conflicts.",
5
+ "main": "index.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/lequanghuylc/react-native-browser-with-polyfill.git"
9
+ },
10
+ "author": "Huy Le",
11
+ "keywords": ["react-native", "expo", "webview", "polyfill", "browser", "ipados"],
12
+ "license": "MIT",
13
+ "peerDependencies": {
14
+ "react": ">=16.8.0",
15
+ "react-native": ">=0.60.0",
16
+ "react-native-webview": ">=11.0.0"
17
+ },
18
+ "files": [
19
+ "index.js",
20
+ "src/"
21
+ ]
22
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * createBrowser — factory that accepts peer dependencies via DI
3
+ *
4
+ * Usage:
5
+ * import createBrowser from "expo-browser";
6
+ * import Webview from "react-native-webview";
7
+ * import * as React from "react";
8
+ * import * as ReactNative from "react-native";
9
+ *
10
+ * const { Browser, useWebViewConsole } = createBrowser({
11
+ * Webview, React, ReactNative,
12
+ * });
13
+ */
14
+
15
+ function createBrowser({ Webview, React, ReactNative }) {
16
+ const { useState, useRef, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } = React;
17
+ const { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, SafeAreaView, StatusBar, Platform } = ReactNative;
18
+
19
+ // ── Console Hook ───────────────────────────────────────────────
20
+ function useWebViewConsole() {
21
+ const [logs, setLogs] = useState([]);
22
+
23
+ const addLog = useCallback((type, message) => {
24
+ const timestamp = new Date().toLocaleTimeString();
25
+ setLogs((prev) => [...prev, { type, message, timestamp, id: Date.now() + Math.random() }]);
26
+ }, []);
27
+
28
+ const clearLogs = useCallback(() => { setLogs([]); }, []);
29
+
30
+ const handleWebViewMessage = useCallback((event) => {
31
+ try {
32
+ const data = JSON.parse(event.nativeEvent.data);
33
+ if (data && data.__console) {
34
+ addLog(data.type || "log", data.message || "");
35
+ }
36
+ } catch (_) {}
37
+ }, [addLog]);
38
+
39
+ return { logs, addLog, clearLogs, handleWebViewMessage };
40
+ }
41
+
42
+ // ── Console Intercept Script ───────────────────────────────────
43
+ const CONSOLE_INTERCEPT = `(function(){
44
+ var oL=console.log,oW=console.warn,oE=console.error;
45
+ function s(t,a){try{var m=Array.prototype.slice.call(a).map(function(x){
46
+ if(typeof x==="object"){try{return JSON.stringify(x)}catch(e){return String(x)}}
47
+ return String(x)}).join(" ");
48
+ if(window.ReactNativeWebView)window.ReactNativeWebView.postMessage(
49
+ JSON.stringify({__console:true,type:t,message:m}))}catch(e){}}
50
+ console.log=function(){s("log",arguments);oL.apply(console,arguments)};
51
+ console.warn=function(){s("warn",arguments);oW.apply(console,arguments)};
52
+ console.error=function(){s("error",arguments);oE.apply(console,arguments)}})();`;
53
+
54
+ // ── DevBar Component ───────────────────────────────────────────
55
+ function DevBar({ webViewRef, logs, clearLogs, currentUrl }) {
56
+ const [expanded, setExpanded] = useState(false);
57
+ const [showConsole, setShowConsole] = useState(false);
58
+ const [urlInput, setUrlInput] = useState(currentUrl || "");
59
+ const consoleScrollRef = useRef(null);
60
+
61
+ useEffect(() => { setUrlInput(currentUrl || ""); }, [currentUrl]);
62
+
63
+ const handleGo = () => {
64
+ let url = urlInput.trim();
65
+ if (url && !url.startsWith("http")) url = "https://" + url;
66
+ if (url && webViewRef.current) webViewRef.current.loadUrl(url);
67
+ };
68
+
69
+ if (!expanded) {
70
+ return (
71
+ <TouchableOpacity style={styles.toggleBtn} onPress={() => setExpanded(true)} activeOpacity={0.7}>
72
+ <Text style={styles.toggleText}>🛠</Text>
73
+ </TouchableOpacity>
74
+ );
75
+ }
76
+
77
+ return (
78
+ <View style={styles.devbarContainer}>
79
+ <View style={styles.devbarHeader}>
80
+ <Text style={styles.devbarTitle}>🔧 Dev Toolbar</Text>
81
+ <TouchableOpacity onPress={() => setExpanded(false)}>
82
+ <Text style={styles.devbarClose}>✕</Text>
83
+ </TouchableOpacity>
84
+ </View>
85
+
86
+ <View style={styles.urlRow}>
87
+ <TextInput
88
+ style={styles.urlInput}
89
+ value={urlInput}
90
+ onChangeText={setUrlInput}
91
+ onSubmitEditing={handleGo}
92
+ placeholder="Enter URL..."
93
+ placeholderTextColor="#888"
94
+ autoCapitalize="none"
95
+ autoCorrect={false}
96
+ keyboardType="url"
97
+ returnKeyType="go"
98
+ />
99
+ <TouchableOpacity style={styles.goBtn} onPress={handleGo}>
100
+ <Text style={styles.goBtnText}>Go</Text>
101
+ </TouchableOpacity>
102
+ </View>
103
+
104
+ <Text style={styles.currentUrl} numberOfLines={1}>{currentUrl}</Text>
105
+
106
+ <View style={styles.btnRow}>
107
+ {[
108
+ ["← Back", () => webViewRef.current?.goBack()],
109
+ ["→ Fwd", () => webViewRef.current?.goForward()],
110
+ ["↻ Reload", () => webViewRef.current?.reload()],
111
+ ["🗑 Clear", () => webViewRef.current?.clearCache?.()],
112
+ ["📋 Console", () => setShowConsole(v => !v)],
113
+ ].map(([label, onPress]) => (
114
+ <TouchableOpacity key={label} style={[styles.navBtn, label.includes("Console") && showConsole && styles.navBtnActive]} onPress={onPress}>
115
+ <Text style={styles.navBtnText}>{label}</Text>
116
+ </TouchableOpacity>
117
+ ))}
118
+ </View>
119
+
120
+ {showConsole && (
121
+ <View style={styles.consolePanel}>
122
+ <View style={styles.consoleHeader}>
123
+ <Text style={styles.consoleTitle}>Console ({logs.length})</Text>
124
+ <TouchableOpacity onPress={clearLogs}>
125
+ <Text style={styles.consoleClear}>Clear</Text>
126
+ </TouchableOpacity>
127
+ </View>
128
+ <ScrollView ref={consoleScrollRef} style={styles.consoleScroll}
129
+ onContentSizeChange={() => consoleScrollRef.current?.scrollToEnd({ animated: true })}>
130
+ {logs.length === 0 ? (
131
+ <Text style={styles.consoleEmpty}>No messages yet.</Text>
132
+ ) : (
133
+ logs.map((log, i) => (
134
+ <View key={i} style={styles.logEntry}>
135
+ <Text style={[styles.logType, { color: log.type === "error" ? "#F44336" : log.type === "warn" ? "#FF9800" : "#4CAF50" }]}>
136
+ [{log.type}]
137
+ </Text>
138
+ <Text style={styles.logTime}>{log.timestamp}</Text>
139
+ <Text style={styles.logMsg} selectable>{log.message}</Text>
140
+ </View>
141
+ ))
142
+ )}
143
+ </ScrollView>
144
+ </View>
145
+ )}
146
+ </View>
147
+ );
148
+ }
149
+
150
+ // ── WebViewScreen Component ────────────────────────────────────
151
+ const WebViewScreen = forwardRef(function WebViewScreen(props, ref) {
152
+ const { initialUrl, onUrlChange, onMessage, polyfillScript, keyboardScript } = props;
153
+ const webViewRef = useRef(null);
154
+ const [currentUrl, setCurrentUrl] = useState(initialUrl || "https://browserleaks.com/js");
155
+
156
+ const injectedJS = useMemo(() => {
157
+ const parts = [CONSOLE_INTERCEPT];
158
+ if (polyfillScript) parts.push(polyfillScript);
159
+ if (keyboardScript) parts.push(keyboardScript);
160
+ return parts.join("\n");
161
+ }, [polyfillScript, keyboardScript]);
162
+
163
+ useImperativeHandle(ref, () => ({
164
+ goBack: () => webViewRef.current?.goBack(),
165
+ goForward: () => webViewRef.current?.goForward(),
166
+ reload: () => webViewRef.current?.reload(),
167
+ loadUrl: (url) => {
168
+ const newUrl = url.startsWith("http") ? url : "https://" + url;
169
+ setCurrentUrl(newUrl);
170
+ onUrlChange?.(newUrl);
171
+ webViewRef.current?.injectJavaScript("window.location.href=" + JSON.stringify(newUrl) + ";true;");
172
+ },
173
+ clearCache: () => {
174
+ webViewRef.current?.injectJavaScript("if(window.caches){caches.keys().then(function(n){for(var i=0;i<n.length;i++)caches.delete(n[i])})}true;");
175
+ },
176
+ }));
177
+
178
+ return (
179
+ <Webview
180
+ ref={webViewRef}
181
+ source={{ uri: currentUrl }}
182
+ injectedJavaScriptBeforeContentLoaded={injectedJS}
183
+ onMessage={onMessage}
184
+ onNavigationStateChange={(nav) => {
185
+ setCurrentUrl(nav.url);
186
+ onUrlChange?.(nav.url);
187
+ }}
188
+ javaScriptEnabled
189
+ domStorageEnabled
190
+ startInLoadingState
191
+ allowsInlineMediaPlayback
192
+ applicationNameForUserAgent="ExpoBrowser/1.0"
193
+ style={{ flex: 1 }}
194
+ />
195
+ );
196
+ });
197
+
198
+ // ── Browser (main component) ───────────────────────────────────
199
+ function Browser({ initialUrl, polyfillScript, keyboardScript }) {
200
+ const webViewRef = useRef(null);
201
+ const { logs, clearLogs, handleWebViewMessage } = useWebViewConsole();
202
+ const [currentUrl, setCurrentUrl] = useState(initialUrl || "https://browserleaks.com/js");
203
+
204
+ return (
205
+ <View style={{ flex: 1, backgroundColor: "#000" }}>
206
+ <StatusBar barStyle="light-content" backgroundColor="#000" />
207
+ <WebViewScreen
208
+ ref={webViewRef}
209
+ initialUrl={initialUrl}
210
+ onUrlChange={setCurrentUrl}
211
+ onMessage={handleWebViewMessage}
212
+ polyfillScript={polyfillScript}
213
+ keyboardScript={keyboardScript}
214
+ />
215
+ <DevBar
216
+ webViewRef={webViewRef}
217
+ logs={logs}
218
+ clearLogs={clearLogs}
219
+ currentUrl={currentUrl}
220
+ />
221
+ </View>
222
+ );
223
+ }
224
+
225
+ // ── Styles ─────────────────────────────────────────────────────
226
+ const styles = StyleSheet.create({
227
+ toggleBtn: { position: "absolute", bottom: 20, right: 20, width: 48, height: 48, borderRadius: 24, backgroundColor: "rgba(30,30,30,0.9)", justifyContent: "center", alignItems: "center", borderWidth: 2, borderColor: "#4CAF50", elevation: 8 },
228
+ toggleText: { fontSize: 22 },
229
+ devbarContainer: { position: "absolute", bottom: 0, left: 0, right: 0, backgroundColor: "rgba(30,30,30,0.95)", borderTopLeftRadius: 12, borderTopRightRadius: 12, paddingHorizontal: 12, paddingTop: 8, paddingBottom: 4, borderWidth: 1, borderColor: "#444", borderBottomWidth: 0 },
230
+ devbarHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 6 },
231
+ devbarTitle: { color: "#fff", fontSize: 14, fontWeight: "bold" },
232
+ devbarClose: { color: "#aaa", fontSize: 18, fontWeight: "bold", padding: 4 },
233
+ urlRow: { flexDirection: "row", alignItems: "center", marginBottom: 4 },
234
+ urlInput: { flex: 1, backgroundColor: "#333", color: "#fff", fontSize: 12, paddingHorizontal: 8, paddingVertical: 6, borderRadius: 6, borderWidth: 1, borderColor: "#555" },
235
+ goBtn: { marginLeft: 6, backgroundColor: "#4CAF50", paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6 },
236
+ goBtnText: { color: "#fff", fontSize: 12, fontWeight: "bold" },
237
+ currentUrl: { color: "#888", fontSize: 10, marginBottom: 6 },
238
+ btnRow: { flexDirection: "row", flexWrap: "wrap", marginBottom: 6 },
239
+ navBtn: { backgroundColor: "#444", paddingHorizontal: 8, paddingVertical: 6, borderRadius: 6, marginRight: 4, marginBottom: 4 },
240
+ navBtnActive: { backgroundColor: "#1976D2" },
241
+ navBtnText: { color: "#fff", fontSize: 11 },
242
+ consolePanel: { maxHeight: 200, marginTop: 4, borderTopWidth: 1, borderTopColor: "#444", paddingTop: 4 },
243
+ consoleHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 4 },
244
+ consoleTitle: { color: "#aaa", fontSize: 12, fontWeight: "bold" },
245
+ consoleClear: { color: "#F44336", fontSize: 12, fontWeight: "bold" },
246
+ consoleScroll: { maxHeight: 160, backgroundColor: "#1a1a1a", borderRadius: 4, padding: 4 },
247
+ consoleEmpty: { color: "#666", fontSize: 11, fontStyle: "italic", padding: 8 },
248
+ logEntry: { flexDirection: "row", flexWrap: "wrap", alignItems: "flex-start", marginBottom: 2, paddingVertical: 2, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: "#333" },
249
+ logType: { fontSize: 10, fontWeight: "bold", marginRight: 4 },
250
+ logTime: { color: "#666", fontSize: 9, marginRight: 6, marginTop: 1 },
251
+ logMsg: { color: "#ddd", fontSize: 10, flex: 1 },
252
+ });
253
+
254
+ return { Browser, WebViewScreen, DevBar, useWebViewConsole };
255
+ }
256
+
257
+ module.exports = createBrowser;
258
+ module.exports.default = createBrowser;