webview.io 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Fabrice K.M. Ekpetse
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.
package/README.md ADDED
@@ -0,0 +1,496 @@
1
+ # webview.io
2
+
3
+ Easy and friendly API to connect and interact between React Native applications and WebView content with enhanced security, reliability, and modern async/await support.
4
+
5
+ ## Features
6
+
7
+ - **Bidirectional Communication**: Seamless messaging between React Native and WebView
8
+ - **Enhanced Security**: Origin validation, message sanitization, and payload size limits
9
+ - **Auto-Reconnection**: Automatic reconnection with exponential backoff strategy
10
+ - **Heartbeat Monitoring**: Connection health monitoring with configurable intervals
11
+ - **Message Queuing**: Queue messages when disconnected and replay on reconnection
12
+ - **Rate Limiting**: Configurable message rate limiting to prevent spam
13
+ - **Promise Support**: Modern async/await APIs with timeout handling
14
+ - **Connection Statistics**: Real-time connection and performance metrics
15
+ - **Comprehensive Error Handling**: Detailed error types and handling mechanisms
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install webview.io react-native-webview
21
+ # or
22
+ yarn add webview.io react-native-webview
23
+ ```
24
+
25
+ ### Peer Dependencies
26
+
27
+ This package requires the following peer dependencies:
28
+
29
+ ```json
30
+ {
31
+ "react": ">=16.8.0",
32
+ "react-native-webview": ">=11.0.0"
33
+ }
34
+ ```
35
+
36
+ Make sure these are installed in your React Native project.
37
+
38
+ ## Basic Usage
39
+
40
+ ### React Native Side (WEBVIEW peer)
41
+
42
+ ```javascript
43
+ import React, { useRef, useEffect } from 'react'
44
+ import { View } from 'react-native'
45
+ import { WebView } from 'react-native-webview'
46
+ import WIO from 'webview.io'
47
+
48
+ function MapComponent() {
49
+ const webViewRef = useRef(null)
50
+ const wioRef = useRef(null)
51
+
52
+ useEffect(() => {
53
+ // Initialize bridge
54
+ wioRef.current = new WIO({
55
+ type: 'WEBVIEW',
56
+ debug: true
57
+ })
58
+
59
+ // Establish connection
60
+ wioRef.current.initiate(webViewRef, 'https://your-webview-content.com')
61
+
62
+ // Listen for connection
63
+ wioRef.current.on('connect', () => {
64
+ console.log('Connected to WebView!')
65
+
66
+ // Send a message
67
+ wioRef.current.emit('hello', { message: 'Hello from React Native!' })
68
+ })
69
+
70
+ // Listen for messages
71
+ wioRef.current.on('response', (data) => {
72
+ console.log('Received:', data)
73
+ })
74
+
75
+ return () => {
76
+ wioRef.current?.disconnect()
77
+ }
78
+ }, [])
79
+
80
+ const handleMessage = (event) => {
81
+ wioRef.current?.handleMessage(event)
82
+ }
83
+
84
+ return (
85
+ <View style={{ flex: 1 }}>
86
+ <WebView
87
+ ref={webViewRef}
88
+ source={{ uri: 'https://your-webview-content.com' }}
89
+ onMessage={handleMessage}
90
+ injectedJavaScript={wioRef.current?.getInjectedJavaScript()}
91
+ javaScriptEnabled={true}
92
+ />
93
+ </View>
94
+ )
95
+ }
96
+ ```
97
+
98
+ ### WebView Content Side (EMBEDDED peer)
99
+
100
+ ```javascript
101
+ // In your HTML/JS loaded in the WebView
102
+
103
+ // The bridge is automatically available as window._wio
104
+ const wio = window._wio
105
+
106
+ // Handle connection
107
+ wio.on('connect', () => {
108
+ console.log('Connected to React Native!')
109
+ })
110
+
111
+ // Listen for messages
112
+ wio.on('hello', (data) => {
113
+ console.log('Received:', data)
114
+
115
+ // Send response
116
+ wio.emit('response', { received: true })
117
+ })
118
+
119
+ // Send messages to React Native
120
+ wio.emit('ready')
121
+ ```
122
+
123
+ ## Enhanced Configuration Options
124
+
125
+ ```javascript
126
+ const wio = new WIO({
127
+ type: 'WEBVIEW', // 'WEBVIEW' or 'EMBEDDED'
128
+ debug: false, // Enable debug logging
129
+ heartbeatInterval: 30000, // Heartbeat interval in ms (30s)
130
+ connectionTimeout: 10000, // Connection timeout in ms (10s)
131
+ maxMessageSize: 1024 * 1024, // Max message size in bytes (1MB)
132
+ maxMessagesPerSecond: 100, // Rate limit (100 messages/second)
133
+ autoReconnect: true, // Enable automatic reconnection
134
+ messageQueueSize: 50 // Max queued messages when disconnected
135
+ })
136
+ ```
137
+
138
+ ## Async/Await Support
139
+
140
+ ### Send Messages with Acknowledgments
141
+
142
+ ```javascript
143
+ // React Native side
144
+ try {
145
+ const response = await wioRef.current.emitAsync('getData', { id: 123 }, 10000)
146
+ console.log('Response:', response)
147
+ } catch (error) {
148
+ console.error('Request failed:', error.message)
149
+ }
150
+
151
+ // WebView side - Listen and acknowledge
152
+ window._wio.on('getData', async (data, ack) => {
153
+ try {
154
+ const result = await fetchData(data.id)
155
+ ack(false, result) // Success: ack(error, ...response)
156
+ } catch (error) {
157
+ ack(error.message) // Error: ack(errorMessage)
158
+ }
159
+ })
160
+ ```
161
+
162
+ ### Wait for Connection
163
+
164
+ ```javascript
165
+ // Wait for connection with timeout
166
+ try {
167
+ await wioRef.current.connectAsync(5000) // 5 second timeout
168
+ console.log('Connection established!')
169
+ } catch (error) {
170
+ console.error('Connection failed:', error.message)
171
+ }
172
+
173
+ // Wait for single event
174
+ const userData = await wioRef.current.onceAsync('userProfile')
175
+ console.log('User data received:', userData)
176
+ ```
177
+
178
+ ## Enhanced Connection Management
179
+
180
+ ### Auto-Reconnection
181
+
182
+ ```javascript
183
+ // Handle connection events
184
+ wio.on('disconnect', (data) => {
185
+ console.log('Disconnected:', data.reason)
186
+ })
187
+
188
+ wio.on('reconnecting', (data) => {
189
+ console.log(`Reconnection attempt ${data.attempt}, delay: ${data.delay}ms`)
190
+ })
191
+
192
+ wio.on('reconnection_failed', (data) => {
193
+ console.error(`Failed to reconnect after ${data.attempts} attempts`)
194
+ })
195
+ ```
196
+
197
+ ### Connection Statistics
198
+
199
+ ```javascript
200
+ const stats = wio.getStats()
201
+ console.log(stats)
202
+ // {
203
+ // connected: true,
204
+ // peerType: 'WEBVIEW',
205
+ // origin: 'https://example.com',
206
+ // lastHeartbeat: 1609459200000,
207
+ // queuedMessages: 0,
208
+ // reconnectAttempts: 0,
209
+ // activeListeners: 5,
210
+ // messageRate: 2
211
+ // }
212
+ ```
213
+
214
+ ## Security Features
215
+
216
+ ### Origin Validation
217
+
218
+ ```javascript
219
+ // Strict origin checking (WebView side)
220
+ window._wio.listen('react-native') // Only accept from React Native
221
+
222
+ // Error handling for invalid origins (React Native side)
223
+ wio.on('error', (error) => {
224
+ if (error.type === 'INVALID_ORIGIN') {
225
+ console.log(`Rejected message from ${error.received}`)
226
+ }
227
+ })
228
+ ```
229
+
230
+ ### Message Sanitization
231
+
232
+ ```javascript
233
+ // Automatic payload sanitization removes functions and undefined values
234
+ wio.emit('data', {
235
+ text: 'Hello',
236
+ func: () => {}, // Functions are automatically removed
237
+ undef: undefined // Undefined values are automatically removed
238
+ })
239
+ ```
240
+
241
+ ### Rate Limiting
242
+
243
+ ```javascript
244
+ const wio = new WIO({
245
+ maxMessagesPerSecond: 10 // Limit to 10 messages per second
246
+ })
247
+
248
+ wio.on('error', (error) => {
249
+ if (error.type === 'RATE_LIMIT_EXCEEDED') {
250
+ console.log(`Rate limited: ${error.current}/${error.limit}`)
251
+ }
252
+ })
253
+ ```
254
+
255
+ ## Comprehensive Error Handling
256
+
257
+ ```javascript
258
+ wio.on('error', (error) => {
259
+ switch (error.type) {
260
+ case 'INVALID_ORIGIN':
261
+ console.error(`Invalid origin: expected ${error.expected}, got ${error.received}`)
262
+ break
263
+ case 'ORIGIN_MISMATCH':
264
+ console.error(`Origin mismatch: expected ${error.expected}, got ${error.received}`)
265
+ break
266
+ case 'RATE_LIMIT_EXCEEDED':
267
+ console.warn(`Rate limit exceeded: ${error.current}/${error.limit} messages/second`)
268
+ break
269
+ case 'MESSAGE_HANDLING_ERROR':
270
+ console.error(`Error handling event ${error.event}: ${error.error}`)
271
+ break
272
+ case 'EMIT_ERROR':
273
+ console.error(`Error sending event ${error.event}: ${error.error}`)
274
+ break
275
+ case 'LISTENER_ERROR':
276
+ console.error(`Error in listener for ${error.event}: ${error.error}`)
277
+ break
278
+ case 'NO_CONNECTION':
279
+ console.error(`Attempted to send ${error.event} without connection`)
280
+ break
281
+ default:
282
+ console.error('Unknown error:', error)
283
+ }
284
+ })
285
+ ```
286
+
287
+ ## Message Queuing
288
+
289
+ ```javascript
290
+ // Messages are automatically queued when disconnected
291
+ wio.emit('important-data', { data: 'This will be queued if disconnected' })
292
+
293
+ // Clear queue manually if needed
294
+ wio.clearQueue()
295
+
296
+ // Check queue status
297
+ const stats = wio.getStats()
298
+ console.log(`${stats.queuedMessages} messages queued`)
299
+ ```
300
+
301
+ ## API Reference
302
+
303
+ ### Methods
304
+
305
+ #### Async Methods
306
+ - **`emitAsync(event, payload?, timeout?)`** - Send message and wait for response (Promise)
307
+ - **`connectAsync(timeout?)`** - Wait for connection with timeout (Promise)
308
+ - **`onceAsync(event)`** - Wait for single event (Promise)
309
+
310
+ #### Utility Methods
311
+ - **`getStats()`** - Get connection statistics
312
+ - **`clearQueue()`** - Clear queued messages
313
+ - **`getInjectedJavaScript()`** - Get JavaScript to inject into WebView
314
+
315
+ #### Connection Methods
316
+ - **`initiate(webViewRef, origin)`** - Establish connection (WEBVIEW peer only)
317
+ - **`listen(hostOrigin?)`** - Listen for connection (EMBEDDED peer only)
318
+ - **`handleMessage(event)`** - Handle incoming message from WebView
319
+ - **`disconnect(callback?)`** - Disconnect and cleanup
320
+ - **`isConnected()`** - Check connection status
321
+
322
+ #### Messaging Methods
323
+ - **`emit(event, payload?, callback?)`** - Send message
324
+ - **`on(event, listener)`** - Add event listener
325
+ - **`once(event, listener)`** - Add one-time event listener
326
+ - **`off(event, listener?)`** - Remove event listener(s)
327
+ - **`removeListeners(callback?)`** - Remove all listeners
328
+
329
+ ### Events
330
+
331
+ #### Connection Events
332
+ - **`connect`** - Connection established
333
+ - **`disconnect`** - Connection lost with reason
334
+ - **`reconnecting`** - Reconnection attempt started
335
+ - **`reconnection_failed`** - All reconnection attempts failed
336
+
337
+ #### Error Events
338
+ - **`error`** - Various error conditions with detailed error objects
339
+
340
+ ## Complete Example
341
+
342
+ ```javascript
343
+ import React, { useRef, useEffect, useState } from 'react'
344
+ import { View, TouchableOpacity, Text, StyleSheet } from 'react-native'
345
+ import { WebView } from 'react-native-webview'
346
+ import WIO from 'webview.io'
347
+
348
+ function App() {
349
+ const webViewRef = useRef(null)
350
+ const wioRef = useRef(null)
351
+ const [isConnected, setIsConnected] = useState(false)
352
+
353
+ useEffect(() => {
354
+ wioRef.current = new WIO({
355
+ type: 'WEBVIEW',
356
+ debug: true
357
+ })
358
+
359
+ wioRef.current.initiate(webViewRef, 'https://your-app.com')
360
+
361
+ wioRef.current
362
+ .on('connect', () => {
363
+ console.log('Connected!')
364
+ setIsConnected(true)
365
+ })
366
+ .on('disconnect', () => {
367
+ console.log('Disconnected!')
368
+ setIsConnected(false)
369
+ })
370
+ .on('location:picked', (location) => {
371
+ console.log('User picked location:', location)
372
+ })
373
+
374
+ return () => {
375
+ wioRef.current?.disconnect()
376
+ }
377
+ }, [])
378
+
379
+ const getLocation = async () => {
380
+ try {
381
+ const location = await wioRef.current.emitAsync('get:location')
382
+ console.log('Got location:', location)
383
+ } catch (error) {
384
+ console.error('Failed to get location:', error)
385
+ }
386
+ }
387
+
388
+ return (
389
+ <View style={styles.container}>
390
+ <WebView
391
+ ref={webViewRef}
392
+ source={{ uri: 'https://your-app.com' }}
393
+ onMessage={(event) => wioRef.current?.handleMessage(event)}
394
+ injectedJavaScript={wioRef.current?.getInjectedJavaScript()}
395
+ javaScriptEnabled={true}
396
+ />
397
+
398
+ <View style={styles.controls}>
399
+ <Text>Status: {isConnected ? 'Connected' : 'Disconnected'}</Text>
400
+ <TouchableOpacity onPress={getLocation}>
401
+ <Text>Get Location</Text>
402
+ </TouchableOpacity>
403
+ </View>
404
+ </View>
405
+ )
406
+ }
407
+
408
+ const styles = StyleSheet.create({
409
+ container: { flex: 1 },
410
+ controls: { padding: 16 }
411
+ })
412
+ ```
413
+
414
+ ## TypeScript Support
415
+
416
+ Full TypeScript support with comprehensive type definitions:
417
+
418
+ ```typescript
419
+ import WIO, { Options, Listener, AckFunction } from 'webview.io'
420
+
421
+ const options: Options = {
422
+ type: 'WEBVIEW',
423
+ debug: true,
424
+ heartbeatInterval: 30000,
425
+ maxMessageSize: 512 * 1024
426
+ }
427
+
428
+ const wio = new WIO(options)
429
+
430
+ // Typed event listeners
431
+ wio.on('userAction', (data: { action: string; userId: number }) => {
432
+ console.log(`User ${data.userId} performed ${data.action}`)
433
+ })
434
+
435
+ // Typed async responses
436
+ interface ApiResponse {
437
+ success: boolean
438
+ data: any[]
439
+ }
440
+
441
+ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
442
+ 'search',
443
+ { query: 'hello' },
444
+ 5000
445
+ )
446
+ ```
447
+
448
+ ## Error Types Reference
449
+
450
+ | Error Type | Description |
451
+ |------------|-------------|
452
+ | `INVALID_ORIGIN` | Message from unexpected origin |
453
+ | `ORIGIN_MISMATCH` | Origin changed during session |
454
+ | `MESSAGE_HANDLING_ERROR` | Error processing incoming message |
455
+ | `EMIT_ERROR` | Error sending message |
456
+ | `LISTENER_ERROR` | Error in event listener |
457
+ | `RATE_LIMIT_EXCEEDED` | Too many messages sent |
458
+ | `NO_CONNECTION` | Attempted to send without connection |
459
+
460
+ ## React Native Compatibility
461
+
462
+ - React Native 0.60+
463
+ - React 16.8+ (Hooks support)
464
+ - react-native-webview 11.0+
465
+
466
+ ## Performance Considerations
467
+
468
+ - **Message Size**: Keep messages under the configured `maxMessageSize` (default 1MB)
469
+ - **Rate Limiting**: Respect the `maxMessagesPerSecond` limit (default 100/sec)
470
+ - **Queue Size**: Monitor queued messages to avoid memory issues
471
+ - **Heartbeat**: Adjust `heartbeatInterval` based on your reliability needs
472
+ - **Battery Impact**: Consider disabling heartbeat or increasing interval for battery-sensitive applications
473
+
474
+ ## Differences from iframe.io
475
+
476
+ `webview.io` is adapted specifically for React Native and differs from `iframe.io` in the following ways:
477
+
478
+ - **Peer Types**: `WEBVIEW` (React Native) and `EMBEDDED` (WebView content) instead of `WINDOW` and `IFRAME`
479
+ - **Initialization**: Uses `RefObject<WebView>` instead of `Window` object
480
+ - **Message Handling**: Requires explicit `handleMessage()` call in `onMessage` prop
481
+ - **Injected Script**: Uses `getInjectedJavaScript()` to setup bridge in WebView
482
+ - **No DOM Dependencies**: Works in React Native environment without DOM APIs
483
+
484
+ ## License
485
+
486
+ MIT License - see LICENSE file for details.
487
+
488
+ ## Contributing
489
+
490
+ Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
491
+
492
+ ## Support
493
+
494
+ - Create an issue on GitHub for bug reports
495
+ - Check existing issues for common problems
496
+ - Review the documentation for usage examples
@@ -0,0 +1,107 @@
1
+ import type { RefObject } from 'react';
2
+ import type { WebView } from 'react-native-webview';
3
+ export type PeerType = 'WEBVIEW' | 'EMBEDDED';
4
+ export type AckFunction = (error: boolean | string, ...args: any[]) => void;
5
+ export type Listener = (payload?: any, ack?: AckFunction) => void;
6
+ export type Options = {
7
+ type?: PeerType;
8
+ debug?: boolean;
9
+ heartbeatInterval?: number;
10
+ connectionTimeout?: number;
11
+ maxMessageSize?: number;
12
+ maxMessagesPerSecond?: number;
13
+ autoReconnect?: boolean;
14
+ messageQueueSize?: number;
15
+ };
16
+ export interface RegisteredEvents {
17
+ [index: string]: Listener[];
18
+ }
19
+ export type Peer = {
20
+ type: PeerType;
21
+ webViewRef?: RefObject<WebView>;
22
+ origin?: string;
23
+ connected?: boolean;
24
+ lastHeartbeat?: number;
25
+ };
26
+ export type MessageData = {
27
+ _event: string;
28
+ payload: any;
29
+ cid: string | undefined;
30
+ timestamp?: number;
31
+ size?: number;
32
+ };
33
+ export type Message = {
34
+ data: MessageData;
35
+ };
36
+ export type QueuedMessage = {
37
+ _event: string;
38
+ payload: any;
39
+ fn?: AckFunction;
40
+ timestamp: number;
41
+ };
42
+ export default class WIO {
43
+ Events: RegisteredEvents;
44
+ peer: Peer;
45
+ options: Options;
46
+ private heartbeatTimer?;
47
+ private reconnectTimer?;
48
+ private messageQueue;
49
+ private messageRateTracker;
50
+ private reconnectAttempts;
51
+ private maxReconnectAttempts;
52
+ constructor(options?: Options);
53
+ debug(...args: any[]): void;
54
+ isConnected(): boolean;
55
+ private startHeartbeat;
56
+ private stopHeartbeat;
57
+ private handleConnectionLoss;
58
+ private attemptReconnection;
59
+ private checkRateLimit;
60
+ private queueMessage;
61
+ private processMessageQueue;
62
+ /**
63
+ * Establish a connection with WebView
64
+ */
65
+ initiate(webViewRef: RefObject<WebView>, origin: string): this;
66
+ /**
67
+ * Listening to connection from the WebView host
68
+ * Note: In React Native context, this is handled by injected JavaScript
69
+ */
70
+ listen(hostOrigin?: string): this;
71
+ /**
72
+ * Handle incoming message from WebView
73
+ * Called by React Native component via onMessage prop
74
+ */
75
+ handleMessage(event: {
76
+ nativeEvent: {
77
+ data: string;
78
+ };
79
+ }): void;
80
+ fire(_event: string, payload?: MessageData['payload'], cid?: string): void;
81
+ emit<T = any>(_event: string, payload?: T | AckFunction, fn?: AckFunction): this;
82
+ on(_event: string, fn: Listener): this;
83
+ once(_event: string, fn: Listener): this;
84
+ off(_event: string, fn?: Listener): this;
85
+ removeListeners(fn?: Listener): this;
86
+ emitAsync<T = any, R = any>(_event: string, payload?: T, timeout?: number): Promise<R>;
87
+ onceAsync<T = any>(_event: string): Promise<T>;
88
+ connectAsync(timeout?: number): Promise<void>;
89
+ private cleanup;
90
+ disconnect(fn?: () => void): this;
91
+ getStats(): {
92
+ connected: boolean;
93
+ peerType: PeerType;
94
+ origin: string | undefined;
95
+ lastHeartbeat: number | undefined;
96
+ queuedMessages: number;
97
+ reconnectAttempts: number;
98
+ activeListeners: number;
99
+ messageRate: number;
100
+ };
101
+ clearQueue(): this;
102
+ /**
103
+ * Get injected JavaScript for WebView
104
+ * Sets up the EMBEDDED side of the bridge
105
+ */
106
+ getInjectedJavaScript(): string;
107
+ }