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 +160 -0
- package/index.js +29 -0
- package/package.json +22 -0
- package/src/createBrowser.jsx +258 -0
- package/src/polyfills/dev-keyboard-bar.js +670 -0
- package/src/polyfills/ipados15-polyfill.js +1859 -0
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;
|