webview.io 1.0.5 → 1.1.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 +71 -18
- package/dist/index.d.ts +14 -0
- package/dist/index.js +56 -25
- package/package.json +1 -1
- package/src/index.ts +121 -71
package/README.md
CHANGED
|
@@ -106,9 +106,11 @@ function MapComponent() {
|
|
|
106
106
|
|
|
107
107
|
```javascript
|
|
108
108
|
// In your HTML/JS loaded in the WebView
|
|
109
|
+
// The bridge is automatically available as window._wio after injection
|
|
109
110
|
|
|
110
|
-
//
|
|
111
|
+
// IMPORTANT: You must manually call listen() to start the connection
|
|
111
112
|
const wio = window._wio
|
|
113
|
+
wio.listen() // Required to initiate connection handshake
|
|
112
114
|
|
|
113
115
|
// Handle connection
|
|
114
116
|
wio.on('connect', () => {
|
|
@@ -140,7 +142,9 @@ const wio = new WIO({
|
|
|
140
142
|
maxMessageSize: 1024 * 1024, // Max message size in bytes (1MB)
|
|
141
143
|
maxMessagesPerSecond: 100, // Rate limit (100 messages/second)
|
|
142
144
|
autoReconnect: true, // Enable automatic reconnection
|
|
143
|
-
messageQueueSize: 50
|
|
145
|
+
messageQueueSize: 50, // Max queued messages when disconnected
|
|
146
|
+
allowedIncomingEvents: ['hello', 'response'], // Optional incoming event allowlist (non-reserved events)
|
|
147
|
+
validateIncoming: (event, payload) => true // Optional custom incoming validator
|
|
144
148
|
})
|
|
145
149
|
```
|
|
146
150
|
|
|
@@ -180,12 +184,28 @@ This prevents:
|
|
|
180
184
|
- Fires `connect_timeout` event if all attempts fail
|
|
181
185
|
|
|
182
186
|
**EMBEDDED Side:**
|
|
183
|
-
-
|
|
187
|
+
- Bridge is available as `window._wio` after injection
|
|
188
|
+
- **Must manually call `window._wio.listen()` to start connection**
|
|
189
|
+
- Automatically announces `__embedded_ready` when `listen()` is called
|
|
184
190
|
- Retries announcement every 2 seconds until connected
|
|
185
191
|
- Handles race condition if loaded before React Native
|
|
186
192
|
- Responds to `ping` with `pong` containing same token
|
|
187
193
|
- Confirms connection upon receiving `__connection_ack`
|
|
188
194
|
|
|
195
|
+
## Reserved Events
|
|
196
|
+
|
|
197
|
+
The library uses several internal events for connection management. These events are handled automatically and **bypass normal message queuing** to ensure reliable connection establishment:
|
|
198
|
+
|
|
199
|
+
- **`ping`** - Initial connection request from WEBVIEW
|
|
200
|
+
- **`pong`** - Connection response from EMBEDDED
|
|
201
|
+
- **`__heartbeat`** - Periodic health check request
|
|
202
|
+
- **`__heartbeat_response`** - Health check response
|
|
203
|
+
- **`__embedded_ready`** - EMBEDDED announces it's ready to connect
|
|
204
|
+
- **`__connection_ack`** - Final handshake acknowledgment from WEBVIEW
|
|
205
|
+
- **`__webview_ready`** - Signal that WEBVIEW peer is ready (currently logged but not actively used)
|
|
206
|
+
|
|
207
|
+
**Important:** These events are sent immediately even when disconnected, unlike regular events which are queued. Do not use these event names for your application logic.
|
|
208
|
+
|
|
189
209
|
## Async/Await Support
|
|
190
210
|
|
|
191
211
|
### Send Messages with Acknowledgments
|
|
@@ -231,7 +251,7 @@ console.log('User data received:', userData)
|
|
|
231
251
|
### Connection Timeout Handling
|
|
232
252
|
|
|
233
253
|
```javascript
|
|
234
|
-
// Handle connection timeout
|
|
254
|
+
// Handle connection timeout
|
|
235
255
|
wio.on('connect_timeout', ({ attempts }) => {
|
|
236
256
|
console.log(`Failed to connect after ${attempts} attempts`)
|
|
237
257
|
// Optionally retry manually or show error to user
|
|
@@ -267,13 +287,13 @@ const stats = wio.getStats()
|
|
|
267
287
|
console.log(stats)
|
|
268
288
|
// {
|
|
269
289
|
// connected: true,
|
|
270
|
-
// embeddedReady: true, //
|
|
290
|
+
// embeddedReady: true, // EMBEDDED peer readiness state
|
|
271
291
|
// peerType: 'WEBVIEW',
|
|
272
292
|
// origin: 'https://example.com',
|
|
273
293
|
// lastHeartbeat: 1609459200000,
|
|
274
294
|
// queuedMessages: 0,
|
|
275
295
|
// reconnectAttempts: 0,
|
|
276
|
-
// connectionAttempts: 0, //
|
|
296
|
+
// connectionAttempts: 0, // Current connection attempts
|
|
277
297
|
// activeListeners: 5,
|
|
278
298
|
// messageRate: 2
|
|
279
299
|
// }
|
|
@@ -288,7 +308,7 @@ Each connection uses a unique token for validation:
|
|
|
288
308
|
```javascript
|
|
289
309
|
// Automatically generated and validated during handshake
|
|
290
310
|
// Prevents accepting messages from previous connections
|
|
291
|
-
// Tokens are only used for connection
|
|
311
|
+
// Tokens are only used for reserved connection events
|
|
292
312
|
```
|
|
293
313
|
|
|
294
314
|
### Origin Validation
|
|
@@ -312,6 +332,29 @@ wio.emit('data', {
|
|
|
312
332
|
})
|
|
313
333
|
```
|
|
314
334
|
|
|
335
|
+
### Incoming Event Allowlist & Validation
|
|
336
|
+
|
|
337
|
+
For defense-in-depth, you can restrict which **application-level** events are accepted and/or validate incoming payloads. Reserved internal events (handshake/heartbeat/readiness) are always allowed.
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
const wio = new WIO({
|
|
341
|
+
type: 'WEBVIEW',
|
|
342
|
+
debug: true,
|
|
343
|
+
allowedIncomingEvents: ['get:location', 'location:picked'],
|
|
344
|
+
validateIncoming: (event, payload) => {
|
|
345
|
+
// Example: simple checks
|
|
346
|
+
if (event === 'location:picked') return payload && typeof payload.lat === 'number'
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
wio.on('error', (error) => {
|
|
352
|
+
if (error.type === 'DISALLOWED_EVENT' || error.type === 'INVALID_MESSAGE') {
|
|
353
|
+
console.warn('Dropped incoming message:', error)
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
```
|
|
357
|
+
|
|
315
358
|
### Rate Limiting
|
|
316
359
|
|
|
317
360
|
```javascript
|
|
@@ -356,6 +399,7 @@ wio.on('error', (error) => {
|
|
|
356
399
|
|
|
357
400
|
```javascript
|
|
358
401
|
// Messages are automatically queued when disconnected
|
|
402
|
+
// Reserved events bypass the queue for reliable connection handling
|
|
359
403
|
wio.emit('important-data', { data: 'This will be queued if disconnected' })
|
|
360
404
|
|
|
361
405
|
// Clear queue manually if needed
|
|
@@ -382,7 +426,7 @@ console.log(`${stats.queuedMessages} messages queued`)
|
|
|
382
426
|
|
|
383
427
|
#### Connection Methods
|
|
384
428
|
- **`initiate(webViewRef, origin)`** - Establish connection with retry logic (WEBVIEW peer only)
|
|
385
|
-
- **`listen(hostOrigin?)`** -
|
|
429
|
+
- **`listen(hostOrigin?)`** - Start listening for connection (EMBEDDED peer only). The `hostOrigin` parameter is informational only and used for logging - no validation is performed.
|
|
386
430
|
- **`handleMessage(event)`** - Handle incoming message from WebView
|
|
387
431
|
- **`disconnect(callback?)`** - Disconnect and cleanup all resources
|
|
388
432
|
- **`isConnected()`** - Check connection status
|
|
@@ -399,15 +443,19 @@ console.log(`${stats.queuedMessages} messages queued`)
|
|
|
399
443
|
#### Connection Events
|
|
400
444
|
- **`connect`** - Connection established (after 3-way handshake)
|
|
401
445
|
- **`disconnect`** - Connection lost with reason
|
|
402
|
-
- **`connect_timeout`** - Initial connection failed after all attempts
|
|
446
|
+
- **`connect_timeout`** - Initial connection failed after all attempts
|
|
403
447
|
- **`reconnecting`** - Reconnection attempt started
|
|
404
448
|
- **`reconnection_failed`** - All reconnection attempts failed
|
|
405
449
|
|
|
406
|
-
#### Internal Events
|
|
450
|
+
#### Reserved Internal Events
|
|
451
|
+
These events are handled automatically by the library. Do not use these names for your application events:
|
|
452
|
+
- **`ping`** - Connection initiation from WEBVIEW
|
|
453
|
+
- **`pong`** - Connection response from EMBEDDED
|
|
407
454
|
- **`__embedded_ready`** - EMBEDDED peer announces readiness
|
|
408
455
|
- **`__connection_ack`** - Final acknowledgment in 3-way handshake
|
|
409
|
-
- **`__heartbeat`** - Connection health check
|
|
456
|
+
- **`__heartbeat`** - Connection health check request
|
|
410
457
|
- **`__heartbeat_response`** - Heartbeat response
|
|
458
|
+
- **`__webview_ready`** - WEBVIEW ready signal (informational)
|
|
411
459
|
|
|
412
460
|
#### Error Events
|
|
413
461
|
- **`error`** - Various error conditions with detailed error objects
|
|
@@ -575,6 +623,8 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
|
|
|
575
623
|
| `MESSAGE_HANDLING_ERROR` | Error processing incoming message |
|
|
576
624
|
| `EMIT_ERROR` | Error sending message |
|
|
577
625
|
| `LISTENER_ERROR` | Error in event listener |
|
|
626
|
+
| `DISALLOWED_EVENT` | Incoming event rejected by `allowedIncomingEvents` |
|
|
627
|
+
| `INVALID_MESSAGE` | Incoming message rejected by `validateIncoming` |
|
|
578
628
|
| `RATE_LIMIT_EXCEEDED` | Too many messages sent |
|
|
579
629
|
| `NO_CONNECTION` | Attempted to send without connection |
|
|
580
630
|
|
|
@@ -600,12 +650,13 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
|
|
|
600
650
|
**Symptoms:** `connect_timeout` event fires, connection never succeeds
|
|
601
651
|
|
|
602
652
|
**Solutions:**
|
|
603
|
-
1.
|
|
604
|
-
2.
|
|
605
|
-
3.
|
|
606
|
-
4.
|
|
607
|
-
5.
|
|
608
|
-
6.
|
|
653
|
+
1. **Verify `listen()` is called** - The EMBEDDED side must manually call `window._wio.listen()` to start the connection
|
|
654
|
+
2. Check that `injectedJavaScript` is properly set on WebView
|
|
655
|
+
3. Verify `javaScriptEnabled={true}` is set
|
|
656
|
+
4. Check browser console for JavaScript errors in WebView
|
|
657
|
+
5. Ensure origin matches exactly (including protocol)
|
|
658
|
+
6. Increase `connectionTimeout` for slow networks
|
|
659
|
+
7. Check that WebView content loads successfully
|
|
609
660
|
|
|
610
661
|
### Messages Not Received
|
|
611
662
|
|
|
@@ -617,6 +668,7 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
|
|
|
617
668
|
3. Look for errors in `error` event listener
|
|
618
669
|
4. Check if rate limiting is being exceeded
|
|
619
670
|
5. Verify message size is under `maxMessageSize`
|
|
671
|
+
6. Ensure you're not using reserved event names
|
|
620
672
|
|
|
621
673
|
### Frequent Disconnections
|
|
622
674
|
|
|
@@ -637,7 +689,7 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
|
|
|
637
689
|
- Already handled! The three-way handshake with readiness announcements prevents this
|
|
638
690
|
- EMBEDDED announces readiness periodically until connected
|
|
639
691
|
- WEBVIEW retries connection attempts automatically
|
|
640
|
-
-
|
|
692
|
+
- Just ensure `window._wio.listen()` is called in your WebView code
|
|
641
693
|
|
|
642
694
|
## Differences from iframe.io
|
|
643
695
|
|
|
@@ -647,6 +699,7 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
|
|
|
647
699
|
- **Initialization**: Uses `RefObject<WebView>` instead of `Window` object
|
|
648
700
|
- **Message Handling**: Requires explicit `handleMessage()` call in `onMessage` prop
|
|
649
701
|
- **Injected Script**: Uses `getInjectedJavaScript()` to setup bridge in WebView
|
|
702
|
+
- **Manual Connection**: EMBEDDED side must call `listen()` to start connection
|
|
650
703
|
- **No DOM Dependencies**: Works in React Native environment without DOM APIs
|
|
651
704
|
- **Enhanced Handshake**: Three-way handshake with token validation for mobile reliability
|
|
652
705
|
- **Connection Retry**: Built-in retry logic for spotty mobile connections
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,16 @@ export type Options = {
|
|
|
14
14
|
messageQueueSize?: number;
|
|
15
15
|
connectionPingInterval?: number;
|
|
16
16
|
maxConnectionAttempts?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Optional allowlist of incoming application-level events.
|
|
19
|
+
* Reserved internal events (ping/pong/heartbeat/handshake) are always allowed.
|
|
20
|
+
*/
|
|
21
|
+
allowedIncomingEvents?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Optional custom validator for incoming messages.
|
|
24
|
+
* Return false to drop a message; an 'error' event will be emitted.
|
|
25
|
+
*/
|
|
26
|
+
validateIncoming?: (event: string, payload: any) => boolean;
|
|
17
27
|
};
|
|
18
28
|
export interface RegisteredEvents {
|
|
19
29
|
[index: string]: Listener[];
|
|
@@ -78,6 +88,9 @@ export default class WIO {
|
|
|
78
88
|
initiate(webViewRef: RefObject<WebView>, origin: string): this;
|
|
79
89
|
/**
|
|
80
90
|
* Listening to connection from the WebView host
|
|
91
|
+
*
|
|
92
|
+
* NOTE: This is called manually from page code,
|
|
93
|
+
* not auto-initialized
|
|
81
94
|
*/
|
|
82
95
|
listen(hostOrigin?: string): this;
|
|
83
96
|
/**
|
|
@@ -115,6 +128,7 @@ export default class WIO {
|
|
|
115
128
|
/**
|
|
116
129
|
* Get injected JavaScript for WebView
|
|
117
130
|
* Sets up the EMBEDDED side of the bridge
|
|
131
|
+
* NOTE: Does not auto-initialize - page must call window._wio.listen()
|
|
118
132
|
*/
|
|
119
133
|
getInjectedJavaScript(): string;
|
|
120
134
|
}
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,22 @@ var ackId = function () {
|
|
|
45
45
|
return "".concat(timestamp, "_").concat(random);
|
|
46
46
|
};
|
|
47
47
|
var generateToken = function () {
|
|
48
|
+
// Prefer cryptographically strong randomness when available
|
|
49
|
+
try {
|
|
50
|
+
var globalCrypto = (typeof crypto !== 'undefined'
|
|
51
|
+
? crypto
|
|
52
|
+
: (typeof window !== 'undefined' && window.crypto)
|
|
53
|
+
|| (typeof globalThis !== 'undefined' && globalThis.crypto));
|
|
54
|
+
if (globalCrypto && typeof globalCrypto.getRandomValues === 'function') {
|
|
55
|
+
var buffer = new Uint32Array(4);
|
|
56
|
+
globalCrypto.getRandomValues(buffer);
|
|
57
|
+
var randomPart = Array.from(buffer).map(function (n) { return n.toString(16); }).join('');
|
|
58
|
+
return "".concat(Date.now(), "_").concat(randomPart);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (_a) {
|
|
62
|
+
// Fall back to Math.random-based implementation below
|
|
63
|
+
}
|
|
48
64
|
return "".concat(Date.now(), "_").concat(Math.random().toString(36).substring(2, 15));
|
|
49
65
|
};
|
|
50
66
|
var RESERVED_EVENTS = [
|
|
@@ -143,13 +159,9 @@ var WIO = /** @class */ (function () {
|
|
|
143
159
|
_this.connectionAttempts = 0;
|
|
144
160
|
_this.connectionToken = generateToken();
|
|
145
161
|
// Re-initiate connection for WEBVIEW type
|
|
146
|
-
|
|
147
|
-
_this.startConnectionAttempt();
|
|
148
|
-
}
|
|
162
|
+
_this.peer.type === 'WEBVIEW' && _this.startConnectionAttempt();
|
|
149
163
|
// For EMBEDDED type, announce readiness
|
|
150
|
-
|
|
151
|
-
_this.announceEmbeddedReady();
|
|
152
|
-
}
|
|
164
|
+
_this.peer.type === 'EMBEDDED' && _this.announceEmbeddedReady();
|
|
153
165
|
// Set timeout for this reconnection attempt
|
|
154
166
|
setTimeout(function () {
|
|
155
167
|
if (_this.peer.connected)
|
|
@@ -181,9 +193,8 @@ var WIO = /** @class */ (function () {
|
|
|
181
193
|
_this.debug("[".concat(_this.peer.type, "] Connection attempt ").concat(_this.connectionAttempts, "/").concat(_this.options.maxConnectionAttempts));
|
|
182
194
|
_this.emit('ping', { token: _this.connectionToken });
|
|
183
195
|
}
|
|
184
|
-
else
|
|
196
|
+
else
|
|
185
197
|
_this.stopConnectionAttempt();
|
|
186
|
-
}
|
|
187
198
|
}, this.options.connectionPingInterval);
|
|
188
199
|
// Set overall timeout
|
|
189
200
|
this.connectionAttemptTimer = setTimeout(function () {
|
|
@@ -225,16 +236,15 @@ var WIO = /** @class */ (function () {
|
|
|
225
236
|
_this.debug("[".concat(_this.peer.type, "] Ready announcement attempt ").concat(attempts, "/").concat(maxAttempts));
|
|
226
237
|
_this.emit('__embedded_ready');
|
|
227
238
|
}
|
|
228
|
-
else
|
|
239
|
+
else
|
|
229
240
|
_this.stopEmbeddedReadyAnnouncement();
|
|
230
|
-
}
|
|
231
241
|
}, this.options.connectionPingInterval);
|
|
232
242
|
};
|
|
233
243
|
WIO.prototype.stopEmbeddedReadyAnnouncement = function () {
|
|
234
|
-
if (this.embeddedReadyCheckInterval)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
244
|
+
if (!this.embeddedReadyCheckInterval)
|
|
245
|
+
return;
|
|
246
|
+
clearInterval(this.embeddedReadyCheckInterval);
|
|
247
|
+
this.embeddedReadyCheckInterval = undefined;
|
|
238
248
|
};
|
|
239
249
|
// Message rate limiting
|
|
240
250
|
WIO.prototype.checkRateLimit = function () {
|
|
@@ -257,8 +267,8 @@ var WIO = /** @class */ (function () {
|
|
|
257
267
|
};
|
|
258
268
|
// Queue messages when not connected
|
|
259
269
|
WIO.prototype.queueMessage = function (_event, payload, fn) {
|
|
270
|
+
// Remove oldest message
|
|
260
271
|
if (this.messageQueue.length >= this.options.messageQueueSize) {
|
|
261
|
-
// Remove oldest message
|
|
262
272
|
var removed = this.messageQueue.shift();
|
|
263
273
|
this.debug("[".concat(this.peer.type, "] Message queue full, removed oldest message:"), removed === null || removed === void 0 ? void 0 : removed._event);
|
|
264
274
|
}
|
|
@@ -311,6 +321,9 @@ var WIO = /** @class */ (function () {
|
|
|
311
321
|
};
|
|
312
322
|
/**
|
|
313
323
|
* Listening to connection from the WebView host
|
|
324
|
+
*
|
|
325
|
+
* NOTE: This is called manually from page code,
|
|
326
|
+
* not auto-initialized
|
|
314
327
|
*/
|
|
315
328
|
WIO.prototype.listen = function (hostOrigin) {
|
|
316
329
|
var _this = this;
|
|
@@ -320,9 +333,7 @@ var WIO = /** @class */ (function () {
|
|
|
320
333
|
this.reconnectAttempts = 0;
|
|
321
334
|
this.debug("[".concat(this.peer.type, "] Listening to connect").concat(hostOrigin ? ": Host <".concat(hostOrigin, ">") : ''));
|
|
322
335
|
// Start announcing readiness
|
|
323
|
-
setTimeout(function () {
|
|
324
|
-
_this.announceEmbeddedReady();
|
|
325
|
-
}, 100);
|
|
336
|
+
setTimeout(function () { return _this.announceEmbeddedReady(); }, 100);
|
|
326
337
|
return this;
|
|
327
338
|
};
|
|
328
339
|
/**
|
|
@@ -359,9 +370,9 @@ var WIO = /** @class */ (function () {
|
|
|
359
370
|
this.peer.embeddedReady = true;
|
|
360
371
|
this.debug("[".concat(this.peer.type, "] Embedded peer ready"));
|
|
361
372
|
// If we're WEBVIEW and not connected, send ping
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
373
|
+
this.peer.type === 'WEBVIEW'
|
|
374
|
+
&& !this.peer.connected
|
|
375
|
+
&& this.emit('ping', { token: this.connectionToken });
|
|
365
376
|
return;
|
|
366
377
|
}
|
|
367
378
|
// Handle webview ready signal
|
|
@@ -424,6 +435,27 @@ var WIO = /** @class */ (function () {
|
|
|
424
435
|
}
|
|
425
436
|
return;
|
|
426
437
|
}
|
|
438
|
+
// Optional application-level incoming validation (non-reserved events only)
|
|
439
|
+
if (!RESERVED_EVENTS.includes(_event)) {
|
|
440
|
+
if (this.options.allowedIncomingEvents
|
|
441
|
+
&& !this.options.allowedIncomingEvents.includes(_event)) {
|
|
442
|
+
this.fire('error', {
|
|
443
|
+
type: 'DISALLOWED_EVENT',
|
|
444
|
+
direction: 'incoming',
|
|
445
|
+
event: _event
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (this.options.validateIncoming
|
|
450
|
+
&& !this.options.validateIncoming(_event, payload)) {
|
|
451
|
+
this.fire('error', {
|
|
452
|
+
type: 'INVALID_MESSAGE',
|
|
453
|
+
direction: 'incoming',
|
|
454
|
+
event: _event
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
427
459
|
// Fire available event listeners
|
|
428
460
|
this.fire(_event, payload, cid);
|
|
429
461
|
}
|
|
@@ -583,9 +615,7 @@ var WIO = /** @class */ (function () {
|
|
|
583
615
|
var _this = this;
|
|
584
616
|
if (timeout === void 0) { timeout = 5000; }
|
|
585
617
|
return new Promise(function (resolve, reject) {
|
|
586
|
-
var timeoutId = setTimeout(function () {
|
|
587
|
-
reject(new Error("Event '".concat(_event, "' acknowledgment timeout after ").concat(timeout, "ms")));
|
|
588
|
-
}, timeout);
|
|
618
|
+
var timeoutId = setTimeout(function () { return reject(new Error("Event '".concat(_event, "' acknowledgment timeout after ").concat(timeout, "ms"))); }, timeout);
|
|
589
619
|
try {
|
|
590
620
|
_this.emit(_event, payload, function (error) {
|
|
591
621
|
var args = [];
|
|
@@ -677,9 +707,10 @@ var WIO = /** @class */ (function () {
|
|
|
677
707
|
/**
|
|
678
708
|
* Get injected JavaScript for WebView
|
|
679
709
|
* Sets up the EMBEDDED side of the bridge
|
|
710
|
+
* NOTE: Does not auto-initialize - page must call window._wio.listen()
|
|
680
711
|
*/
|
|
681
712
|
WIO.prototype.getInjectedJavaScript = function () {
|
|
682
|
-
return "\n (function() {\n try {\n console.log('[EMBEDDED] Initializing WIO bridge...')\n \n const RESERVED_EVENTS = [\n 'ping',\n 'pong',\n '__heartbeat',\n '__heartbeat_response',\n '__embedded_ready',\n '__connection_ack',\n '__webview_ready'\n ]\n \n // Use closure variable to avoid 'this' binding issues\n window._wio = {\n type: 'EMBEDDED',\n connected: false,\n Events: {},\n messageQueue: [],\n connectionToken: null,\n setupComplete: false,\n\n listen: function(){\n console.log('[EMBEDDED] Setting up message listeners...')\n\n // Listen to messages from React Native\n window.addEventListener('message', function( event ){\n try {\n const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data\n window._wio.handleMessage( message ) // Use window._wio instead of this\n }\n catch( error ){ console.error('[EMBEDDED] Parse error:', error ) }\n })\n \n // Android support\n if( typeof document !== 'undefined' ){\n document.addEventListener('message', function( event ){\n try {\n const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data\n window._wio.handleMessage( message ) // Use window._wio instead of this\n }\n catch( error ){ console.error('[EMBEDDED] Parse error:', error ) }\n })\n }\n\n window._wio.setupComplete = true // Use window._wio instead of this\n console.log('[EMBEDDED] Setup complete')\n\n return window._wio\n },\n \n ackId: function(){\n const\n rmin = 100000,\n rmax = 999999,\n timestamp = Date.now(),\n random = Math.floor( Math.random() * ( rmax - rmin + 1 ) + rmin )\n\n return timestamp + '_' + random\n },\n \n fire: function( _event, payload, cid ){\n if( !window._wio.Events[_event] && !window._wio.Events[_event + '--@once'] ){\n console.log('[EMBEDDED] No listener for:', _event)\n return\n }\n \n const ackFn = cid\n ? ( error, ...args ) => window._wio.emit( _event + '--' + cid + '--@ack', { error: error || false, args } )\n : undefined\n \n let listeners = []\n if( window._wio.Events[_event + '--@once'] ){\n _event += '--@once'\n listeners = window._wio.Events[_event]\n\n delete window._wio.Events[_event]\n }\n else listeners = window._wio.Events[_event] || []\n \n listeners.forEach( fn => {\n try { payload !== undefined ? fn( payload, ackFn ) : fn( ackFn ) }\n catch( error ){ console.error('[EMBEDDED] Listener error:', error ) }\n })\n },\n \n emit: function( _event, payload, fn ){\n if( typeof payload === 'function' ){\n fn = payload\n payload = undefined\n }\n \n if( !window._wio.connected && !RESERVED_EVENTS.includes(_event) ){\n window._wio.messageQueue.push({ _event, payload, fn, timestamp: Date.now() })\n console.log('[EMBEDDED] Queued message:', _event)\n return\n }\n \n try {\n let cid\n if( typeof fn === 'function' ){\n cid = window._wio.ackId()\n window._wio.once( _event + '--' + cid + '--@ack', ({ error, args }) => fn( error, ...args ) )\n }\n \n const messageData = {\n _event,\n payload,\n cid,\n timestamp: Date.now(),\n token: RESERVED_EVENTS.includes(_event) ? window._wio.connectionToken : undefined\n }\n \n if( typeof window.ReactNativeWebView !== 'undefined' )\n window.ReactNativeWebView.postMessage( JSON.stringify( messageData ) )\n else console.error('[EMBEDDED] ReactNativeWebView not available')\n }\n catch( error ){\n console.error('[EMBEDDED] Emit error:', error )\n typeof fn === 'function' && fn( String(error) )\n }\n },\n \n on: function( _event, fn ){\n if( !window._wio.Events[_event] ) window._wio.Events[_event] = []\n window._wio.Events[_event].push( fn )\n\n return window._wio\n },\n \n once: function( _event, fn ){\n _event += '--@once'\n if( !window._wio.Events[_event] ) window._wio.Events[_event] = []\n\n window._wio.Events[_event].push( fn )\n\n return window._wio\n },\n \n off: function( _event, fn ){\n if( fn && window._wio.Events[_event] ){\n const index = window._wio.Events[_event].indexOf( fn )\n if( index > -1 ){\n window._wio.Events[_event].splice( index, 1 )\n if( window._wio.Events[_event].length === 0 ) delete window._wio.Events[_event]\n }\n }\n else delete window._wio.Events[_event]\n\n return window._wio\n },\n \n processMessageQueue: function(){\n if( !window._wio.connected || window._wio.messageQueue.length === 0 ) return\n \n console.log('[EMBEDDED] Processing', window._wio.messageQueue.length, 'queued messages')\n const queue = [ ...window._wio.messageQueue ]\n window._wio.messageQueue = []\n \n queue.forEach( msg => {\n try { window._wio.emit( msg._event, msg.payload, msg.fn ) }\n catch( error ){ console.error('[EMBEDDED] Queue process error:', error ) }\n })\n },\n \n handleMessage: function( data ){\n if( !data || !data._event ) return\n \n const { _event, payload, cid, token } = data\n \n console.log('[EMBEDDED] Received:', _event )\n \n // Handle heartbeat response\n if( _event === '__heartbeat_response' )\n return\n \n // Handle heartbeat request\n if( _event === '__heartbeat' ){\n window._wio.emit('__heartbeat_response', { timestamp: Date.now() })\n return\n }\n \n // Handle webview ready signal\n if( _event === '__webview_ready' ){\n console.log('[EMBEDDED] WebView ready signal received')\n return\n }\n \n // Handle ping from WEBVIEW\n if( _event === 'ping' ){\n console.log('[EMBEDDED] Received ping, sending pong')\n window._wio.connectionToken = token\n\n window._wio.emit('pong', { token: window._wio.connectionToken })\n return\n }\n \n // Handle connection acknowledgment\n if( _event === '__connection_ack' ){\n if( token && token !== window._wio.connectionToken ){\n console.error('[EMBEDDED] Invalid connection token in ack')\n return\n }\n \n console.log('[EMBEDDED] Connection established (received ack)')\n\n window._wio.connected = true\n window._wio.processMessageQueue()\n window._wio.fire('connect')\n\n return\n }\n \n // Fire event listeners\n window._wio.fire( _event, payload, cid )\n },\n \n announceReady: function(){\n let attempts = 0\n const maxAttempts = 5\n const interval = 2000\n \n console.log('[EMBEDDED] Starting ready announcements')\n \n const announce = () => {\n if( window._wio.connected ){\n console.log('[EMBEDDED] Connected, stopping announcements')\n return\n }\n \n attempts++\n if( attempts > maxAttempts ){\n console.log('[EMBEDDED] Max announcement attempts reached')\n return\n }\n \n console.log('[EMBEDDED] Announcing ready (attempt', attempts + '/' + maxAttempts + ')')\n window._wio.emit('__embedded_ready')\n \n setTimeout( announce, interval )\n }\n \n announce()\n }\n }\n\n // Initialize\n window._wio.listen()\n // Start announcing readiness after a short delay\n setTimeout(() => window._wio.announceReady(), 100)\n \n console.log('[EMBEDDED] WIO bridge initialized successfully')\n }\n catch( error ){\n console.error('[EMBEDDED] Setup failed:', error )\n \n // Create minimal fallback\n window._wio = {\n error: error.toString(),\n emit: function(){ console.error('[EMBEDDED] WIO failed to initialize') },\n on: function(){},\n once: function(){},\n off: function(){}\n }\n }\n\n true\n })()\n ";
|
|
713
|
+
return "\n (function() {\n try {\n console.debug('[EMBEDDED] Initializing WIO bridge...')\n \n const RESERVED_EVENTS = [\n 'ping',\n 'pong',\n '__heartbeat',\n '__heartbeat_response',\n '__embedded_ready',\n '__connection_ack',\n '__webview_ready'\n ]\n \n // Use closure variable to avoid 'this' binding issues\n window._wio = {\n type: 'EMBEDDED',\n connected: false,\n Events: {},\n messageQueue: [],\n connectionToken: null,\n setupComplete: false,\n\n listen: function(){\n if( this.setupComplete ){\n console.warn('[EMBEDDED] Already listening')\n return this\n }\n\n console.debug('[EMBEDDED] Setting up message listeners...')\n\n // Listen to messages from React Native\n window.addEventListener('message', function( event ){\n try {\n const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data\n window._wio.handleMessage( message )\n }\n catch( error ){ console.error('[EMBEDDED] Parse error:', error ) }\n })\n \n // Android support\n if( typeof document !== 'undefined' ){\n document.addEventListener('message', function( event ){\n try {\n const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data\n window._wio.handleMessage( message )\n }\n catch( error ){ console.error('[EMBEDDED] Parse error:', error ) }\n })\n }\n\n this.setupComplete = true\n console.debug('[EMBEDDED] Setup complete, starting ready announcements')\n\n // Start announcing readiness\n this.announceReady()\n\n return this\n },\n \n ackId: function(){\n const\n rmin = 100000,\n rmax = 999999,\n timestamp = Date.now(),\n random = Math.floor( Math.random() * ( rmax - rmin + 1 ) + rmin )\n\n return timestamp + '_' + random\n },\n \n fire: function( _event, payload, cid ){\n if( !window._wio.Events[_event] && !window._wio.Events[_event + '--@once'] ){\n console.debug('[EMBEDDED] No listener for:', _event)\n return\n }\n \n const ackFn = cid\n ? ( error, ...args ) => window._wio.emit( _event + '--' + cid + '--@ack', { error: error || false, args } )\n : undefined\n \n let listeners = []\n if( window._wio.Events[_event + '--@once'] ){\n _event += '--@once'\n listeners = window._wio.Events[_event]\n\n delete window._wio.Events[_event]\n }\n else listeners = window._wio.Events[_event] || []\n \n listeners.forEach( fn => {\n try { payload !== undefined ? fn( payload, ackFn ) : fn( ackFn ) }\n catch( error ){ console.error('[EMBEDDED] Listener error:', error ) }\n })\n },\n \n emit: function( _event, payload, fn ){\n if( typeof payload === 'function' ){\n fn = payload\n payload = undefined\n }\n \n if( !window._wio.connected && !RESERVED_EVENTS.includes(_event) ){\n window._wio.messageQueue.push({ _event, payload, fn, timestamp: Date.now() })\n console.debug('[EMBEDDED] Queued message:', _event)\n return\n }\n \n try {\n let cid\n if( typeof fn === 'function' ){\n cid = window._wio.ackId()\n window._wio.once( _event + '--' + cid + '--@ack', ({ error, args }) => fn( error, ...args ) )\n }\n \n const messageData = {\n _event,\n payload,\n cid,\n timestamp: Date.now(),\n token: RESERVED_EVENTS.includes(_event) ? window._wio.connectionToken : undefined\n }\n \n if( typeof window.ReactNativeWebView !== 'undefined' )\n window.ReactNativeWebView.postMessage( JSON.stringify( messageData ) )\n else console.error('[EMBEDDED] ReactNativeWebView not available')\n }\n catch( error ){\n console.error('[EMBEDDED] Emit error:', error )\n typeof fn === 'function' && fn( String(error) )\n }\n },\n \n on: function( _event, fn ){\n if( !window._wio.Events[_event] ) window._wio.Events[_event] = []\n window._wio.Events[_event].push( fn )\n\n return window._wio\n },\n \n once: function( _event, fn ){\n _event += '--@once'\n if( !window._wio.Events[_event] ) window._wio.Events[_event] = []\n\n window._wio.Events[_event].push( fn )\n\n return window._wio\n },\n \n off: function( _event, fn ){\n if( fn && window._wio.Events[_event] ){\n const index = window._wio.Events[_event].indexOf( fn )\n if( index > -1 ){\n window._wio.Events[_event].splice( index, 1 )\n if( window._wio.Events[_event].length === 0 ) delete window._wio.Events[_event]\n }\n }\n else delete window._wio.Events[_event]\n\n return window._wio\n },\n \n processMessageQueue: function(){\n if( !window._wio.connected || window._wio.messageQueue.length === 0 ) return\n \n console.debug('[EMBEDDED] Processing', window._wio.messageQueue.length, 'queued messages')\n const queue = [ ...window._wio.messageQueue ]\n window._wio.messageQueue = []\n \n queue.forEach( msg => {\n try { window._wio.emit( msg._event, msg.payload, msg.fn ) }\n catch( error ){ console.error('[EMBEDDED] Queue process error:', error ) }\n })\n },\n \n handleMessage: function( data ){\n if( !data || !data._event ) return\n \n const { _event, payload, cid, token } = data\n \n console.debug('[EMBEDDED] Received:', _event )\n \n // Handle heartbeat response\n if( _event === '__heartbeat_response' )\n return\n \n // Handle heartbeat request\n if( _event === '__heartbeat' ){\n window._wio.emit('__heartbeat_response', { timestamp: Date.now() })\n return\n }\n \n // Handle webview ready signal\n if( _event === '__webview_ready' ){\n console.debug('[EMBEDDED] WebView ready signal received')\n return\n }\n \n // Handle ping from WEBVIEW\n if( _event === 'ping' ){\n console.debug('[EMBEDDED] Received ping, sending pong')\n window._wio.connectionToken = token\n\n window._wio.emit('pong', { token: window._wio.connectionToken })\n return\n }\n \n // Handle connection acknowledgment\n if( _event === '__connection_ack' ){\n if( token && token !== window._wio.connectionToken ){\n console.error('[EMBEDDED] Invalid connection token in ack')\n return\n }\n \n console.debug('[EMBEDDED] Connection established (received ack)')\n\n window._wio.connected = true\n window._wio.processMessageQueue()\n window._wio.fire('connect')\n\n return\n }\n \n // Fire event listeners\n window._wio.fire( _event, payload, cid )\n },\n \n announceReady: function(){\n let attempts = 0\n const maxAttempts = 10\n const interval = 1000\n \n console.debug('[EMBEDDED] Starting ready announcements')\n \n const announce = () => {\n if( window._wio.connected ){\n console.debug('[EMBEDDED] Connected, stopping announcements')\n return\n }\n \n attempts++\n if( attempts > maxAttempts ){\n console.debug('[EMBEDDED] Max announcement attempts reached')\n return\n }\n \n console.debug('[EMBEDDED] Announcing ready (attempt', attempts + '/' + maxAttempts + ')')\n window._wio.emit('__embedded_ready')\n \n setTimeout( announce, interval )\n }\n \n announce()\n }\n }\n\n console.debug('[EMBEDDED] WIO bridge ready. Call window._wio.listen() to connect.')\n }\n catch( error ){\n console.error('[EMBEDDED] Setup failed:', error )\n \n // Create minimal fallback\n window._wio = {\n error: error.toString(),\n listen: function(){ console.error('[EMBEDDED] WIO failed to initialize') },\n emit: function(){ console.error('[EMBEDDED] WIO failed to initialize') },\n on: function(){},\n once: function(){},\n off: function(){}\n }\n }\n\n true\n })()\n ";
|
|
683
714
|
};
|
|
684
715
|
return WIO;
|
|
685
716
|
}());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webview.io",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Easy and friendly API to connect and interact between React Native and WebView with enhanced security, reliability, and modern async/await support.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,16 @@ export type Options = {
|
|
|
17
17
|
messageQueueSize?: number
|
|
18
18
|
connectionPingInterval?: number
|
|
19
19
|
maxConnectionAttempts?: number
|
|
20
|
+
/**
|
|
21
|
+
* Optional allowlist of incoming application-level events.
|
|
22
|
+
* Reserved internal events (ping/pong/heartbeat/handshake) are always allowed.
|
|
23
|
+
*/
|
|
24
|
+
allowedIncomingEvents?: string[]
|
|
25
|
+
/**
|
|
26
|
+
* Optional custom validator for incoming messages.
|
|
27
|
+
* Return false to drop a message; an 'error' event will be emitted.
|
|
28
|
+
*/
|
|
29
|
+
validateIncoming?: ( event: string, payload: any ) => boolean
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
export interface RegisteredEvents {
|
|
@@ -83,7 +93,26 @@ const ackId = () => {
|
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
const generateToken = () => {
|
|
86
|
-
|
|
96
|
+
// Prefer cryptographically strong randomness when available
|
|
97
|
+
try {
|
|
98
|
+
const globalCrypto = (typeof crypto !== 'undefined'
|
|
99
|
+
? crypto
|
|
100
|
+
: (typeof window !== 'undefined' && (window as any).crypto)
|
|
101
|
+
|| (typeof globalThis !== 'undefined' && (globalThis as any).crypto))
|
|
102
|
+
|
|
103
|
+
if( globalCrypto && typeof globalCrypto.getRandomValues === 'function' ){
|
|
104
|
+
const buffer = new Uint32Array(4)
|
|
105
|
+
globalCrypto.getRandomValues( buffer )
|
|
106
|
+
|
|
107
|
+
const randomPart = Array.from( buffer ).map( n => n.toString( 16 ) ).join('')
|
|
108
|
+
return `${Date.now()}_${randomPart}`
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch{
|
|
112
|
+
// Fall back to Math.random-based implementation below
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return `${Date.now()}_${Math.random().toString( 36 ).substring( 2, 15 )}`
|
|
87
116
|
}
|
|
88
117
|
|
|
89
118
|
const RESERVED_EVENTS = [
|
|
@@ -131,8 +160,7 @@ export default class WIO {
|
|
|
131
160
|
this.Events = {}
|
|
132
161
|
this.peer = { type: 'WEBVIEW', connected: false, embeddedReady: false }
|
|
133
162
|
|
|
134
|
-
if( options.type )
|
|
135
|
-
this.peer.type = options.type
|
|
163
|
+
if( options.type ) this.peer.type = options.type
|
|
136
164
|
}
|
|
137
165
|
|
|
138
166
|
debug( ...args: any[] ){
|
|
@@ -211,24 +239,19 @@ export default class WIO {
|
|
|
211
239
|
this.connectionToken = generateToken()
|
|
212
240
|
|
|
213
241
|
// Re-initiate connection for WEBVIEW type
|
|
214
|
-
|
|
215
|
-
this.startConnectionAttempt()
|
|
216
|
-
}
|
|
217
|
-
|
|
242
|
+
this.peer.type === 'WEBVIEW' && this.startConnectionAttempt()
|
|
218
243
|
// For EMBEDDED type, announce readiness
|
|
219
|
-
|
|
220
|
-
this.announceEmbeddedReady()
|
|
221
|
-
}
|
|
244
|
+
this.peer.type === 'EMBEDDED' && this.announceEmbeddedReady()
|
|
222
245
|
|
|
223
246
|
// Set timeout for this reconnection attempt
|
|
224
|
-
setTimeout(() => {
|
|
247
|
+
setTimeout( () => {
|
|
225
248
|
if( this.peer.connected ) return
|
|
226
249
|
|
|
227
250
|
this.reconnectAttempts < this.maxReconnectAttempts
|
|
228
251
|
? this.attemptReconnection()
|
|
229
252
|
: this.fire('reconnection_failed', { attempts: this.reconnectAttempts })
|
|
230
|
-
}, this.options.connectionTimeout!)
|
|
231
|
-
}, delay)
|
|
253
|
+
}, this.options.connectionTimeout! )
|
|
254
|
+
}, delay )
|
|
232
255
|
}
|
|
233
256
|
|
|
234
257
|
// Start connection attempt with timeout and retries
|
|
@@ -257,10 +280,8 @@ export default class WIO {
|
|
|
257
280
|
this.debug(`[${this.peer.type}] Connection attempt ${this.connectionAttempts}/${this.options.maxConnectionAttempts}`)
|
|
258
281
|
this.emit('ping', { token: this.connectionToken })
|
|
259
282
|
}
|
|
260
|
-
else
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
}, this.options.connectionPingInterval!)
|
|
283
|
+
else this.stopConnectionAttempt()
|
|
284
|
+
}, this.options.connectionPingInterval! )
|
|
264
285
|
|
|
265
286
|
// Set overall timeout
|
|
266
287
|
this.connectionAttemptTimer = setTimeout(() => {
|
|
@@ -310,17 +331,15 @@ export default class WIO {
|
|
|
310
331
|
this.debug(`[${this.peer.type}] Ready announcement attempt ${attempts}/${maxAttempts}`)
|
|
311
332
|
this.emit('__embedded_ready')
|
|
312
333
|
}
|
|
313
|
-
else
|
|
314
|
-
this.stopEmbeddedReadyAnnouncement()
|
|
315
|
-
}
|
|
334
|
+
else this.stopEmbeddedReadyAnnouncement()
|
|
316
335
|
}, this.options.connectionPingInterval!)
|
|
317
336
|
}
|
|
318
337
|
|
|
319
338
|
private stopEmbeddedReadyAnnouncement(){
|
|
320
|
-
if( this.embeddedReadyCheckInterval )
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
339
|
+
if( !this.embeddedReadyCheckInterval ) return
|
|
340
|
+
|
|
341
|
+
clearInterval( this.embeddedReadyCheckInterval )
|
|
342
|
+
this.embeddedReadyCheckInterval = undefined
|
|
324
343
|
}
|
|
325
344
|
|
|
326
345
|
// Message rate limiting
|
|
@@ -351,8 +370,8 @@ export default class WIO {
|
|
|
351
370
|
|
|
352
371
|
// Queue messages when not connected
|
|
353
372
|
private queueMessage( _event: string, payload?: any, fn?: AckFunction ){
|
|
373
|
+
// Remove oldest message
|
|
354
374
|
if( this.messageQueue.length >= this.options.messageQueueSize! ){
|
|
355
|
-
// Remove oldest message
|
|
356
375
|
const removed = this.messageQueue.shift()
|
|
357
376
|
this.debug(`[${this.peer.type}] Message queue full, removed oldest message:`, removed?._event)
|
|
358
377
|
}
|
|
@@ -413,6 +432,9 @@ export default class WIO {
|
|
|
413
432
|
|
|
414
433
|
/**
|
|
415
434
|
* Listening to connection from the WebView host
|
|
435
|
+
*
|
|
436
|
+
* NOTE: This is called manually from page code,
|
|
437
|
+
* not auto-initialized
|
|
416
438
|
*/
|
|
417
439
|
listen( hostOrigin?: string ){
|
|
418
440
|
this.peer.type = 'EMBEDDED'
|
|
@@ -423,9 +445,7 @@ export default class WIO {
|
|
|
423
445
|
this.debug(`[${this.peer.type}] Listening to connect${hostOrigin ? `: Host <${hostOrigin}>` : ''}`)
|
|
424
446
|
|
|
425
447
|
// Start announcing readiness
|
|
426
|
-
setTimeout(() =>
|
|
427
|
-
this.announceEmbeddedReady()
|
|
428
|
-
}, 100)
|
|
448
|
+
setTimeout( () => this.announceEmbeddedReady(), 100 )
|
|
429
449
|
|
|
430
450
|
return this
|
|
431
451
|
}
|
|
@@ -436,9 +456,9 @@ export default class WIO {
|
|
|
436
456
|
handleMessage( event: { nativeEvent: { data: string } } ){
|
|
437
457
|
try {
|
|
438
458
|
const data = JSON.parse( event.nativeEvent.data )
|
|
439
|
-
|
|
440
459
|
// Enhanced security: check valid message structure
|
|
441
|
-
if( typeof data !== 'object' || !data.hasOwnProperty('_event') )
|
|
460
|
+
if( typeof data !== 'object' || !data.hasOwnProperty('_event') )
|
|
461
|
+
return
|
|
442
462
|
|
|
443
463
|
const { _event, payload, cid, timestamp, token } = data as MessageData
|
|
444
464
|
|
|
@@ -470,9 +490,10 @@ export default class WIO {
|
|
|
470
490
|
this.debug(`[${this.peer.type}] Embedded peer ready`)
|
|
471
491
|
|
|
472
492
|
// If we're WEBVIEW and not connected, send ping
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
493
|
+
this.peer.type === 'WEBVIEW'
|
|
494
|
+
&& !this.peer.connected
|
|
495
|
+
&& this.emit('ping', { token: this.connectionToken })
|
|
496
|
+
|
|
476
497
|
return
|
|
477
498
|
}
|
|
478
499
|
|
|
@@ -494,6 +515,7 @@ export default class WIO {
|
|
|
494
515
|
// Don't set fully connected yet - wait for ack
|
|
495
516
|
this.debug(`[${this.peer.type}] Received ping, sent pong`)
|
|
496
517
|
}
|
|
518
|
+
|
|
497
519
|
return
|
|
498
520
|
}
|
|
499
521
|
|
|
@@ -522,6 +544,7 @@ export default class WIO {
|
|
|
522
544
|
|
|
523
545
|
this.debug(`[${this.peer.type}] Connected (3-way handshake complete)`)
|
|
524
546
|
}
|
|
547
|
+
|
|
525
548
|
return
|
|
526
549
|
}
|
|
527
550
|
|
|
@@ -546,9 +569,33 @@ export default class WIO {
|
|
|
546
569
|
|
|
547
570
|
this.debug(`[${this.peer.type}] Connected (received ack)`)
|
|
548
571
|
}
|
|
572
|
+
|
|
549
573
|
return
|
|
550
574
|
}
|
|
551
575
|
|
|
576
|
+
// Optional application-level incoming validation (non-reserved events only)
|
|
577
|
+
if( !RESERVED_EVENTS.includes( _event ) ){
|
|
578
|
+
if( this.options.allowedIncomingEvents
|
|
579
|
+
&& !this.options.allowedIncomingEvents.includes( _event ) ){
|
|
580
|
+
this.fire('error', {
|
|
581
|
+
type: 'DISALLOWED_EVENT',
|
|
582
|
+
direction: 'incoming',
|
|
583
|
+
event: _event
|
|
584
|
+
})
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if( this.options.validateIncoming
|
|
589
|
+
&& !this.options.validateIncoming( _event, payload ) ){
|
|
590
|
+
this.fire('error', {
|
|
591
|
+
type: 'INVALID_MESSAGE',
|
|
592
|
+
direction: 'incoming',
|
|
593
|
+
event: _event
|
|
594
|
+
})
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
552
599
|
// Fire available event listeners
|
|
553
600
|
this.fire( _event, payload, cid )
|
|
554
601
|
}
|
|
@@ -569,11 +616,11 @@ export default class WIO {
|
|
|
569
616
|
}
|
|
570
617
|
|
|
571
618
|
const ackFn = cid
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
619
|
+
? ( error: boolean | string, ...args: any[] ): void => {
|
|
620
|
+
this.emit(`${_event}--${cid}--@ack`, { error: error || false, args })
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
: undefined
|
|
577
624
|
let listeners: Listener[] = []
|
|
578
625
|
|
|
579
626
|
if( this.Events[_event + '--@once'] ){
|
|
@@ -717,9 +764,7 @@ export default class WIO {
|
|
|
717
764
|
|
|
718
765
|
emitAsync<T = any, R = any>( _event: string, payload?: T, timeout: number = 5000 ): Promise<R> {
|
|
719
766
|
return new Promise(( resolve, reject ) => {
|
|
720
|
-
const timeoutId = setTimeout(() => {
|
|
721
|
-
reject( new Error(`Event '${_event}' acknowledgment timeout after ${timeout}ms`) )
|
|
722
|
-
}, timeout )
|
|
767
|
+
const timeoutId = setTimeout(() => reject( new Error(`Event '${_event}' acknowledgment timeout after ${timeout}ms`) ), timeout )
|
|
723
768
|
|
|
724
769
|
try {
|
|
725
770
|
this.emit( _event, payload, ( error, ...args ) => {
|
|
@@ -745,17 +790,17 @@ export default class WIO {
|
|
|
745
790
|
return new Promise(( resolve, reject ) => {
|
|
746
791
|
if( this.isConnected() ) return resolve()
|
|
747
792
|
|
|
748
|
-
const timeoutId = setTimeout(() => {
|
|
749
|
-
this.off('connect', connectHandler)
|
|
793
|
+
const timeoutId = setTimeout( () => {
|
|
794
|
+
this.off('connect', connectHandler )
|
|
750
795
|
reject( new Error('Connection timeout') )
|
|
751
|
-
}, timeout || this.options.connectionTimeout)
|
|
796
|
+
}, timeout || this.options.connectionTimeout )
|
|
752
797
|
|
|
753
798
|
const connectHandler = () => {
|
|
754
799
|
clearTimeout( timeoutId )
|
|
755
800
|
resolve()
|
|
756
801
|
}
|
|
757
802
|
|
|
758
|
-
this.once('connect', connectHandler)
|
|
803
|
+
this.once('connect', connectHandler )
|
|
759
804
|
})
|
|
760
805
|
}
|
|
761
806
|
|
|
@@ -822,12 +867,13 @@ export default class WIO {
|
|
|
822
867
|
/**
|
|
823
868
|
* Get injected JavaScript for WebView
|
|
824
869
|
* Sets up the EMBEDDED side of the bridge
|
|
870
|
+
* NOTE: Does not auto-initialize - page must call window._wio.listen()
|
|
825
871
|
*/
|
|
826
872
|
getInjectedJavaScript(): string {
|
|
827
873
|
return `
|
|
828
874
|
(function() {
|
|
829
875
|
try {
|
|
830
|
-
console.
|
|
876
|
+
console.debug('[EMBEDDED] Initializing WIO bridge...')
|
|
831
877
|
|
|
832
878
|
const RESERVED_EVENTS = [
|
|
833
879
|
'ping',
|
|
@@ -849,13 +895,18 @@ export default class WIO {
|
|
|
849
895
|
setupComplete: false,
|
|
850
896
|
|
|
851
897
|
listen: function(){
|
|
852
|
-
|
|
898
|
+
if( this.setupComplete ){
|
|
899
|
+
console.warn('[EMBEDDED] Already listening')
|
|
900
|
+
return this
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
console.debug('[EMBEDDED] Setting up message listeners...')
|
|
853
904
|
|
|
854
905
|
// Listen to messages from React Native
|
|
855
906
|
window.addEventListener('message', function( event ){
|
|
856
907
|
try {
|
|
857
908
|
const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data
|
|
858
|
-
window._wio.handleMessage( message )
|
|
909
|
+
window._wio.handleMessage( message )
|
|
859
910
|
}
|
|
860
911
|
catch( error ){ console.error('[EMBEDDED] Parse error:', error ) }
|
|
861
912
|
})
|
|
@@ -865,16 +916,19 @@ export default class WIO {
|
|
|
865
916
|
document.addEventListener('message', function( event ){
|
|
866
917
|
try {
|
|
867
918
|
const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data
|
|
868
|
-
window._wio.handleMessage( message )
|
|
919
|
+
window._wio.handleMessage( message )
|
|
869
920
|
}
|
|
870
921
|
catch( error ){ console.error('[EMBEDDED] Parse error:', error ) }
|
|
871
922
|
})
|
|
872
923
|
}
|
|
873
924
|
|
|
874
|
-
|
|
875
|
-
console.
|
|
925
|
+
this.setupComplete = true
|
|
926
|
+
console.debug('[EMBEDDED] Setup complete, starting ready announcements')
|
|
876
927
|
|
|
877
|
-
|
|
928
|
+
// Start announcing readiness
|
|
929
|
+
this.announceReady()
|
|
930
|
+
|
|
931
|
+
return this
|
|
878
932
|
},
|
|
879
933
|
|
|
880
934
|
ackId: function(){
|
|
@@ -889,7 +943,7 @@ export default class WIO {
|
|
|
889
943
|
|
|
890
944
|
fire: function( _event, payload, cid ){
|
|
891
945
|
if( !window._wio.Events[_event] && !window._wio.Events[_event + '--@once'] ){
|
|
892
|
-
console.
|
|
946
|
+
console.debug('[EMBEDDED] No listener for:', _event)
|
|
893
947
|
return
|
|
894
948
|
}
|
|
895
949
|
|
|
@@ -920,7 +974,7 @@ export default class WIO {
|
|
|
920
974
|
|
|
921
975
|
if( !window._wio.connected && !RESERVED_EVENTS.includes(_event) ){
|
|
922
976
|
window._wio.messageQueue.push({ _event, payload, fn, timestamp: Date.now() })
|
|
923
|
-
console.
|
|
977
|
+
console.debug('[EMBEDDED] Queued message:', _event)
|
|
924
978
|
return
|
|
925
979
|
}
|
|
926
980
|
|
|
@@ -981,7 +1035,7 @@ export default class WIO {
|
|
|
981
1035
|
processMessageQueue: function(){
|
|
982
1036
|
if( !window._wio.connected || window._wio.messageQueue.length === 0 ) return
|
|
983
1037
|
|
|
984
|
-
console.
|
|
1038
|
+
console.debug('[EMBEDDED] Processing', window._wio.messageQueue.length, 'queued messages')
|
|
985
1039
|
const queue = [ ...window._wio.messageQueue ]
|
|
986
1040
|
window._wio.messageQueue = []
|
|
987
1041
|
|
|
@@ -996,7 +1050,7 @@ export default class WIO {
|
|
|
996
1050
|
|
|
997
1051
|
const { _event, payload, cid, token } = data
|
|
998
1052
|
|
|
999
|
-
console.
|
|
1053
|
+
console.debug('[EMBEDDED] Received:', _event )
|
|
1000
1054
|
|
|
1001
1055
|
// Handle heartbeat response
|
|
1002
1056
|
if( _event === '__heartbeat_response' )
|
|
@@ -1010,13 +1064,13 @@ export default class WIO {
|
|
|
1010
1064
|
|
|
1011
1065
|
// Handle webview ready signal
|
|
1012
1066
|
if( _event === '__webview_ready' ){
|
|
1013
|
-
console.
|
|
1067
|
+
console.debug('[EMBEDDED] WebView ready signal received')
|
|
1014
1068
|
return
|
|
1015
1069
|
}
|
|
1016
1070
|
|
|
1017
1071
|
// Handle ping from WEBVIEW
|
|
1018
1072
|
if( _event === 'ping' ){
|
|
1019
|
-
console.
|
|
1073
|
+
console.debug('[EMBEDDED] Received ping, sending pong')
|
|
1020
1074
|
window._wio.connectionToken = token
|
|
1021
1075
|
|
|
1022
1076
|
window._wio.emit('pong', { token: window._wio.connectionToken })
|
|
@@ -1030,7 +1084,7 @@ export default class WIO {
|
|
|
1030
1084
|
return
|
|
1031
1085
|
}
|
|
1032
1086
|
|
|
1033
|
-
console.
|
|
1087
|
+
console.debug('[EMBEDDED] Connection established (received ack)')
|
|
1034
1088
|
|
|
1035
1089
|
window._wio.connected = true
|
|
1036
1090
|
window._wio.processMessageQueue()
|
|
@@ -1045,24 +1099,24 @@ export default class WIO {
|
|
|
1045
1099
|
|
|
1046
1100
|
announceReady: function(){
|
|
1047
1101
|
let attempts = 0
|
|
1048
|
-
const maxAttempts =
|
|
1049
|
-
const interval =
|
|
1102
|
+
const maxAttempts = 10
|
|
1103
|
+
const interval = 1000
|
|
1050
1104
|
|
|
1051
|
-
console.
|
|
1105
|
+
console.debug('[EMBEDDED] Starting ready announcements')
|
|
1052
1106
|
|
|
1053
1107
|
const announce = () => {
|
|
1054
1108
|
if( window._wio.connected ){
|
|
1055
|
-
console.
|
|
1109
|
+
console.debug('[EMBEDDED] Connected, stopping announcements')
|
|
1056
1110
|
return
|
|
1057
1111
|
}
|
|
1058
1112
|
|
|
1059
1113
|
attempts++
|
|
1060
1114
|
if( attempts > maxAttempts ){
|
|
1061
|
-
console.
|
|
1115
|
+
console.debug('[EMBEDDED] Max announcement attempts reached')
|
|
1062
1116
|
return
|
|
1063
1117
|
}
|
|
1064
1118
|
|
|
1065
|
-
console.
|
|
1119
|
+
console.debug('[EMBEDDED] Announcing ready (attempt', attempts + '/' + maxAttempts + ')')
|
|
1066
1120
|
window._wio.emit('__embedded_ready')
|
|
1067
1121
|
|
|
1068
1122
|
setTimeout( announce, interval )
|
|
@@ -1072,12 +1126,7 @@ export default class WIO {
|
|
|
1072
1126
|
}
|
|
1073
1127
|
}
|
|
1074
1128
|
|
|
1075
|
-
|
|
1076
|
-
window._wio.listen()
|
|
1077
|
-
// Start announcing readiness after a short delay
|
|
1078
|
-
setTimeout(() => window._wio.announceReady(), 100)
|
|
1079
|
-
|
|
1080
|
-
console.log('[EMBEDDED] WIO bridge initialized successfully')
|
|
1129
|
+
console.debug('[EMBEDDED] WIO bridge ready. Call window._wio.listen() to connect.')
|
|
1081
1130
|
}
|
|
1082
1131
|
catch( error ){
|
|
1083
1132
|
console.error('[EMBEDDED] Setup failed:', error )
|
|
@@ -1085,6 +1134,7 @@ export default class WIO {
|
|
|
1085
1134
|
// Create minimal fallback
|
|
1086
1135
|
window._wio = {
|
|
1087
1136
|
error: error.toString(),
|
|
1137
|
+
listen: function(){ console.error('[EMBEDDED] WIO failed to initialize') },
|
|
1088
1138
|
emit: function(){ console.error('[EMBEDDED] WIO failed to initialize') },
|
|
1089
1139
|
on: function(){},
|
|
1090
1140
|
once: function(){},
|