@usions/sdk 2.0.2 → 2.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/package.json +16 -5
- package/src/browser.js +1392 -1139
- package/src/index.js +2 -47
- package/src/modules/back-button.js +26 -0
- package/src/modules/bot.js +52 -0
- package/src/modules/chat.js +35 -0
- package/src/modules/core.js +207 -0
- package/src/modules/file-storage.js +46 -0
- package/src/modules/game-core.js +149 -0
- package/src/modules/game-direct.js +219 -0
- package/src/modules/game-methods.js +334 -0
- package/src/modules/game-proxy.js +149 -0
- package/src/modules/game-socket.js +195 -0
- package/src/modules/index.js +54 -0
- package/src/modules/misc.js +123 -0
- package/src/modules/results.js +34 -0
- package/src/modules/session.js +62 -0
- package/src/modules/storage.js +64 -0
- package/src/modules/ui.js +111 -0
- package/src/modules/user.js +61 -0
- package/src/modules/wallet.js +114 -0
- package/types/index.d.ts +62 -2
package/src/browser.js
CHANGED
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
* Usion Mini App SDK v2.0
|
|
3
|
-
*
|
|
4
|
-
* JavaScript utilities for Mini Apps (Iframe Games & Services)
|
|
5
|
-
* Import via: <script src="https://usions.com/usion-sdk.js"></script>
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - User info and authentication
|
|
9
|
-
* - Persistent storage (per-user, per-service)
|
|
10
|
-
* - Wallet/payment integration
|
|
11
|
-
* - Session management
|
|
12
|
-
* - Real-time game support via Socket.IO
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
(function(global) {
|
|
1
|
+
var Usion = (function () {
|
|
16
2
|
'use strict';
|
|
17
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Usion SDK Core — init, _post, _request, message handling
|
|
6
|
+
*/
|
|
7
|
+
|
|
18
8
|
// Request ID counter for tracking async responses
|
|
19
9
|
let _requestId = 0;
|
|
20
10
|
const _pendingRequests = {};
|
|
21
11
|
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
function getNextRequestId() {
|
|
13
|
+
return ++_requestId;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Core Usion object with init, _post, _request
|
|
18
|
+
*/
|
|
19
|
+
const core = {
|
|
20
|
+
version: '2.1.0',
|
|
24
21
|
config: {},
|
|
25
22
|
_initialized: false,
|
|
26
23
|
_initCallback: null,
|
|
27
24
|
_messageHandlerRegistered: false,
|
|
25
|
+
_results: [],
|
|
26
|
+
_backButtonCallback: null,
|
|
28
27
|
|
|
29
28
|
/**
|
|
30
29
|
* Initialize the SDK with config from parent app
|
|
@@ -32,22 +31,22 @@
|
|
|
32
31
|
*/
|
|
33
32
|
init: function(callback) {
|
|
34
33
|
const self = this;
|
|
35
|
-
|
|
34
|
+
|
|
36
35
|
// Prevent double initialization - just update callback
|
|
37
36
|
if (self._initialized) {
|
|
38
37
|
if (callback) callback(self.config);
|
|
39
38
|
return;
|
|
40
39
|
}
|
|
41
|
-
|
|
40
|
+
|
|
42
41
|
// Store callback for when config arrives
|
|
43
42
|
self._initCallback = callback;
|
|
44
|
-
|
|
43
|
+
|
|
45
44
|
// Only register message handler once
|
|
46
45
|
if (self._messageHandlerRegistered) {
|
|
47
46
|
return;
|
|
48
47
|
}
|
|
49
48
|
self._messageHandlerRegistered = true;
|
|
50
|
-
|
|
49
|
+
|
|
51
50
|
// Setup global message handler
|
|
52
51
|
window.addEventListener('message', function(event) {
|
|
53
52
|
let data;
|
|
@@ -63,12 +62,12 @@
|
|
|
63
62
|
if (self._initialized) {
|
|
64
63
|
return;
|
|
65
64
|
}
|
|
66
|
-
|
|
65
|
+
|
|
67
66
|
self.config = data.config;
|
|
68
67
|
self._initialized = true;
|
|
69
|
-
// We received INIT from a parent
|
|
68
|
+
// We received INIT from a parent -> we are embedded (iframe or WebView)
|
|
70
69
|
self._isEmbedded = true;
|
|
71
|
-
|
|
70
|
+
|
|
72
71
|
// Initialize user module with config data
|
|
73
72
|
if (data.config.userId) {
|
|
74
73
|
self.user._id = data.config.userId;
|
|
@@ -76,36 +75,41 @@
|
|
|
76
75
|
self.user._avatar = data.config.userAvatar;
|
|
77
76
|
self.user._token = data.config.authToken;
|
|
78
77
|
}
|
|
79
|
-
|
|
78
|
+
|
|
80
79
|
// Initialize session module
|
|
81
80
|
if (data.config.sessionId) {
|
|
82
81
|
self.session._id = data.config.sessionId;
|
|
83
82
|
self.session._data = data.config.sessionData || {};
|
|
84
83
|
}
|
|
85
|
-
|
|
84
|
+
|
|
86
85
|
// Initialize wallet with balance if provided
|
|
87
86
|
if (data.config.balance !== undefined) {
|
|
88
87
|
self.wallet._balance = data.config.balance;
|
|
89
88
|
}
|
|
90
|
-
|
|
89
|
+
|
|
90
|
+
// Initialize results from server
|
|
91
|
+
if (data.config.results) {
|
|
92
|
+
self._results = data.config.results;
|
|
93
|
+
}
|
|
94
|
+
|
|
91
95
|
// Call the stored init callback
|
|
92
96
|
if (self._initCallback) {
|
|
93
97
|
self._initCallback(data.config);
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
|
-
|
|
100
|
+
|
|
97
101
|
// Handle response messages for async requests
|
|
98
102
|
if (data._requestId && _pendingRequests[data._requestId]) {
|
|
99
103
|
const { resolve, reject } = _pendingRequests[data._requestId];
|
|
100
104
|
delete _pendingRequests[data._requestId];
|
|
101
|
-
|
|
105
|
+
|
|
102
106
|
if (data.error) {
|
|
103
107
|
reject(new Error(data.error));
|
|
104
108
|
} else {
|
|
105
109
|
resolve(data);
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
|
-
|
|
112
|
+
|
|
109
113
|
// Handle balance updates
|
|
110
114
|
if (data.type === 'BALANCE_UPDATE') {
|
|
111
115
|
self.wallet._balance = data.balance;
|
|
@@ -113,25 +117,53 @@
|
|
|
113
117
|
self.wallet._balanceChangeHandler(data.balance);
|
|
114
118
|
}
|
|
115
119
|
}
|
|
120
|
+
|
|
121
|
+
// Handle back button pressed (one-time claim)
|
|
122
|
+
if (data.type === 'BACK_BUTTON_PRESSED' && self._backButtonCallback) {
|
|
123
|
+
var cb = self._backButtonCallback;
|
|
124
|
+
self._backButtonCallback = null; // one-time use
|
|
125
|
+
cb();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Handle bot messages forwarded from host app
|
|
129
|
+
if (data.type === 'BOT_MESSAGE' && self.bot && self.bot._messageHandler) {
|
|
130
|
+
self.bot._messageHandler(data.message);
|
|
131
|
+
}
|
|
116
132
|
});
|
|
117
133
|
|
|
118
134
|
// Signal ready to parent
|
|
119
135
|
this._post({ type: 'READY' });
|
|
120
136
|
},
|
|
121
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Get the current theme ('light' or 'dark')
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
getTheme: function() {
|
|
143
|
+
return this.config.theme || 'light';
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get the current language/locale (e.g. 'en', 'mn')
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
getLanguage: function() {
|
|
151
|
+
return this.config.language || 'en';
|
|
152
|
+
},
|
|
153
|
+
|
|
122
154
|
/**
|
|
123
155
|
* Send message to parent app
|
|
124
156
|
* @private
|
|
125
157
|
*/
|
|
126
158
|
_post: function(message) {
|
|
127
159
|
const msg = JSON.stringify(message);
|
|
128
|
-
|
|
160
|
+
|
|
129
161
|
// React Native WebView
|
|
130
162
|
if (window.ReactNativeWebView) {
|
|
131
163
|
window.ReactNativeWebView.postMessage(msg);
|
|
132
164
|
return;
|
|
133
165
|
}
|
|
134
|
-
|
|
166
|
+
|
|
135
167
|
// Web iframe
|
|
136
168
|
if (window.parent !== window) {
|
|
137
169
|
window.parent.postMessage(message, '*');
|
|
@@ -145,16 +177,16 @@
|
|
|
145
177
|
_request: function(type, data, timeout) {
|
|
146
178
|
const self = this;
|
|
147
179
|
timeout = timeout || 5000;
|
|
148
|
-
|
|
180
|
+
|
|
149
181
|
return new Promise(function(resolve, reject) {
|
|
150
|
-
const requestId =
|
|
151
|
-
|
|
182
|
+
const requestId = getNextRequestId();
|
|
183
|
+
|
|
152
184
|
// Setup timeout
|
|
153
185
|
const timer = setTimeout(function() {
|
|
154
186
|
delete _pendingRequests[requestId];
|
|
155
187
|
reject(new Error('Request timeout'));
|
|
156
188
|
}, timeout);
|
|
157
|
-
|
|
189
|
+
|
|
158
190
|
// Store pending request
|
|
159
191
|
_pendingRequests[requestId] = {
|
|
160
192
|
resolve: function(result) {
|
|
@@ -166,7 +198,7 @@
|
|
|
166
198
|
reject(error);
|
|
167
199
|
}
|
|
168
200
|
};
|
|
169
|
-
|
|
201
|
+
|
|
170
202
|
// Send request
|
|
171
203
|
self._post({
|
|
172
204
|
type: type,
|
|
@@ -174,16 +206,18 @@
|
|
|
174
206
|
...data
|
|
175
207
|
});
|
|
176
208
|
});
|
|
177
|
-
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
178
211
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Usion SDK User Module — user info and authentication
|
|
214
|
+
*/
|
|
182
215
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
216
|
+
/**
|
|
217
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
218
|
+
*/
|
|
219
|
+
function createUserModule(Usion) {
|
|
220
|
+
return {
|
|
187
221
|
_id: null,
|
|
188
222
|
_name: null,
|
|
189
223
|
_avatar: null,
|
|
@@ -234,16 +268,18 @@
|
|
|
234
268
|
};
|
|
235
269
|
});
|
|
236
270
|
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Usion SDK Storage Module — persistent storage (per-user, per-service)
|
|
276
|
+
*/
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
280
|
+
*/
|
|
281
|
+
function createStorageModule(Usion) {
|
|
282
|
+
return {
|
|
247
283
|
/**
|
|
248
284
|
* Get a stored value
|
|
249
285
|
* @param {string} key - Storage key
|
|
@@ -297,16 +333,19 @@
|
|
|
297
333
|
return response.keys || [];
|
|
298
334
|
});
|
|
299
335
|
}
|
|
300
|
-
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
301
338
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
339
|
+
/**
|
|
340
|
+
* Usion SDK Wallet Module — wallet and payment operations
|
|
341
|
+
*/
|
|
305
342
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
346
|
+
*/
|
|
347
|
+
function createWalletModule(Usion) {
|
|
348
|
+
return {
|
|
310
349
|
_balance: null,
|
|
311
350
|
_balanceChangeHandler: null,
|
|
312
351
|
|
|
@@ -316,12 +355,12 @@
|
|
|
316
355
|
*/
|
|
317
356
|
getBalance: function() {
|
|
318
357
|
const self = this;
|
|
319
|
-
|
|
358
|
+
|
|
320
359
|
// If we have cached balance, return it
|
|
321
360
|
if (self._balance !== null) {
|
|
322
361
|
return Promise.resolve(self._balance);
|
|
323
362
|
}
|
|
324
|
-
|
|
363
|
+
|
|
325
364
|
return Usion._request('GET_BALANCE', {}).then(function(response) {
|
|
326
365
|
self._balance = response.balance;
|
|
327
366
|
return response.balance;
|
|
@@ -348,9 +387,9 @@
|
|
|
348
387
|
*/
|
|
349
388
|
requestPayment: function(amount, reason, data) {
|
|
350
389
|
const self = this;
|
|
351
|
-
|
|
390
|
+
|
|
352
391
|
return new Promise(function(resolve, reject) {
|
|
353
|
-
const requestId =
|
|
392
|
+
const requestId = getNextRequestId();
|
|
354
393
|
const timeoutMs = 60000;
|
|
355
394
|
|
|
356
395
|
// Listen for response
|
|
@@ -408,16 +447,18 @@
|
|
|
408
447
|
onBalanceChange: function(callback) {
|
|
409
448
|
this._balanceChangeHandler = callback;
|
|
410
449
|
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Usion SDK Session Module — ephemeral session data management
|
|
455
|
+
*/
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
459
|
+
*/
|
|
460
|
+
function createSessionModule(Usion) {
|
|
461
|
+
return {
|
|
421
462
|
_id: null,
|
|
422
463
|
_data: {},
|
|
423
464
|
|
|
@@ -452,7 +493,7 @@
|
|
|
452
493
|
} else {
|
|
453
494
|
this._data[keyOrData] = value;
|
|
454
495
|
}
|
|
455
|
-
|
|
496
|
+
|
|
456
497
|
// Notify parent of session data change
|
|
457
498
|
Usion._post({
|
|
458
499
|
type: 'SESSION_DATA_UPDATE',
|
|
@@ -469,132 +510,24 @@
|
|
|
469
510
|
type: 'SESSION_DATA_CLEAR'
|
|
470
511
|
});
|
|
471
512
|
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
},
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Submit result and signal completion
|
|
488
|
-
* @param {object} data - Result data to send to parent
|
|
489
|
-
*/
|
|
490
|
-
submit: function(data) {
|
|
491
|
-
this._post({
|
|
492
|
-
type: 'SUBMIT',
|
|
493
|
-
data: data
|
|
494
|
-
});
|
|
495
|
-
},
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Report an error to parent app
|
|
499
|
-
* @param {string} message - Error message
|
|
500
|
-
*/
|
|
501
|
-
error: function(message) {
|
|
502
|
-
this._post({
|
|
503
|
-
type: 'ERROR',
|
|
504
|
-
message: message
|
|
505
|
-
});
|
|
506
|
-
},
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Request to close the mini app
|
|
510
|
-
*/
|
|
511
|
-
exit: function() {
|
|
512
|
-
this._post({ type: 'EXIT' });
|
|
513
|
-
},
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Share content through the app's native share and optionally post to Usions feed
|
|
517
|
-
*
|
|
518
|
-
* @param {string} contentType - Type of content: 'audio' | 'image' | 'video' | 'text' | 'mixed'
|
|
519
|
-
* @param {object} data - Content data to share:
|
|
520
|
-
* - text: Optional text/caption for the post
|
|
521
|
-
* - audioUrl: URL for audio content (when contentType is 'audio')
|
|
522
|
-
* - imageUrl: URL for image content (when contentType is 'image')
|
|
523
|
-
* - videoUrl: URL for video content (when contentType is 'video')
|
|
524
|
-
* - thumbnailUrl: Optional thumbnail URL for video/audio
|
|
525
|
-
* - width: Optional width for image/video
|
|
526
|
-
* - height: Optional height for image/video
|
|
527
|
-
* - duration: Optional duration in seconds for audio/video
|
|
528
|
-
* - media: Array of media items for 'mixed' content type
|
|
529
|
-
* - Each item: { type: 'image'|'video'|'audio', url: string, thumbnailUrl?, width?, height?, duration? }
|
|
530
|
-
*
|
|
531
|
-
* @example
|
|
532
|
-
* // Share audio
|
|
533
|
-
* Usion.share('audio', {
|
|
534
|
-
* text: 'Check out this AI voice!',
|
|
535
|
-
* audioUrl: 'https://cdn.example.com/audio.mp3',
|
|
536
|
-
* duration: 5.2
|
|
537
|
-
* });
|
|
538
|
-
*
|
|
539
|
-
* @example
|
|
540
|
-
* // Share image
|
|
541
|
-
* Usion.share('image', {
|
|
542
|
-
* text: 'AI-generated art',
|
|
543
|
-
* imageUrl: 'https://cdn.example.com/image.webp',
|
|
544
|
-
* thumbnailUrl: 'https://cdn.example.com/thumb.webp',
|
|
545
|
-
* width: 1024,
|
|
546
|
-
* height: 1024
|
|
547
|
-
* });
|
|
548
|
-
*
|
|
549
|
-
* @example
|
|
550
|
-
* // Share video
|
|
551
|
-
* Usion.share('video', {
|
|
552
|
-
* text: 'My AI video creation',
|
|
553
|
-
* videoUrl: 'https://cdn.example.com/video.mp4',
|
|
554
|
-
* thumbnailUrl: 'https://cdn.example.com/poster.jpg',
|
|
555
|
-
* duration: 30
|
|
556
|
-
* });
|
|
557
|
-
*
|
|
558
|
-
* @example
|
|
559
|
-
* // Share mixed content (multiple media)
|
|
560
|
-
* Usion.share('mixed', {
|
|
561
|
-
* text: 'Gallery of AI creations',
|
|
562
|
-
* media: [
|
|
563
|
-
* { type: 'image', url: 'https://cdn.example.com/1.webp' },
|
|
564
|
-
* { type: 'image', url: 'https://cdn.example.com/2.webp' },
|
|
565
|
-
* { type: 'video', url: 'https://cdn.example.com/video.mp4', thumbnailUrl: '...' }
|
|
566
|
-
* ]
|
|
567
|
-
* });
|
|
568
|
-
*/
|
|
569
|
-
share: function(contentType, data) {
|
|
570
|
-
var shareData = Object.assign({}, data, {
|
|
571
|
-
contentType: contentType,
|
|
572
|
-
serviceId: this.config.serviceId,
|
|
573
|
-
serviceName: this.config.serviceName
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
this._post({
|
|
577
|
-
type: 'SHARE',
|
|
578
|
-
contentType: contentType,
|
|
579
|
-
data: shareData
|
|
580
|
-
});
|
|
581
|
-
},
|
|
582
|
-
|
|
583
|
-
// ============================================
|
|
584
|
-
// Chat
|
|
585
|
-
// ============================================
|
|
586
|
-
|
|
587
|
-
chat: {
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Usion SDK Chat Module — messaging between users
|
|
518
|
+
*/
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
522
|
+
*/
|
|
523
|
+
function createChatModule(Usion) {
|
|
524
|
+
return {
|
|
588
525
|
/**
|
|
589
526
|
* Request to send a message to another user.
|
|
590
527
|
* The parent app will show a confirmation prompt to the user.
|
|
591
528
|
* @param {string} recipientId - Usion user ID of the recipient
|
|
592
529
|
* @param {string} message - Message content to send
|
|
593
530
|
* @returns {Promise<{success: boolean, reason?: string}>}
|
|
594
|
-
*
|
|
595
|
-
* @example
|
|
596
|
-
* const result = await Usion.chat.sendMessage('user_abc', '👋');
|
|
597
|
-
* if (result.success) console.log('Message sent!');
|
|
598
531
|
*/
|
|
599
532
|
sendMessage: function(recipientId, message) {
|
|
600
533
|
return Usion._request('SEND_MESSAGE_REQUEST', {
|
|
@@ -613,44 +546,129 @@
|
|
|
613
546
|
peerUserId: peerUserId
|
|
614
547
|
});
|
|
615
548
|
}
|
|
616
|
-
}
|
|
549
|
+
};
|
|
550
|
+
}
|
|
617
551
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
*/
|
|
622
|
-
log: function(msg) {
|
|
623
|
-
this._post({
|
|
624
|
-
type: 'LOG',
|
|
625
|
-
msg: msg
|
|
626
|
-
});
|
|
627
|
-
console.log('[Usion]', msg);
|
|
628
|
-
},
|
|
552
|
+
/**
|
|
553
|
+
* Usion SDK Bot Bridge — for inline bot iframes
|
|
554
|
+
*/
|
|
629
555
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
* @param {function} callback - Handler function
|
|
634
|
-
*/
|
|
635
|
-
on: function(type, callback) {
|
|
636
|
-
window.addEventListener('message', function(event) {
|
|
637
|
-
let data;
|
|
638
|
-
try {
|
|
639
|
-
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
640
|
-
} catch (e) {
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
556
|
+
function createBotModule(Usion) {
|
|
557
|
+
return {
|
|
558
|
+
_messageHandler: null,
|
|
643
559
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
560
|
+
/**
|
|
561
|
+
* Call a bot action. Delivered as an "iframe_action" webhook to the bot's server.
|
|
562
|
+
* @param {string} action - Action name (e.g., "submit_form", "select_item")
|
|
563
|
+
* @param {object} [data] - Action payload
|
|
564
|
+
* @returns {Promise} Resolves when the host app confirms delivery
|
|
565
|
+
*/
|
|
566
|
+
callAction: function(action, data) {
|
|
567
|
+
return Usion._request('CALL_BOT', { action: action, data: data || {} }, 30000);
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Send a message as if the user typed it. Triggers the normal bot webhook flow.
|
|
572
|
+
* @param {string} text - Message text
|
|
573
|
+
*/
|
|
574
|
+
sendMessage: function(text) {
|
|
575
|
+
Usion._post({ type: 'SEND_USER_MESSAGE', text: text });
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Update context metadata visible to the bot on the next webhook delivery.
|
|
580
|
+
* @param {object} ctx - Context key/value pairs
|
|
581
|
+
*/
|
|
582
|
+
updateContext: function(ctx) {
|
|
583
|
+
Usion._post({ type: 'UPDATE_CONTEXT', context: ctx });
|
|
584
|
+
},
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Close the iframe, optionally returning a result to the bot.
|
|
588
|
+
* @param {object} [result] - Optional result data
|
|
589
|
+
*/
|
|
590
|
+
close: function(result) {
|
|
591
|
+
Usion._post({ type: 'CLOSE', result: result });
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Listen for new bot messages in the conversation.
|
|
596
|
+
* Called whenever the bot sends a message (text, components, etc.).
|
|
597
|
+
* @param {function} callback - Called with { id, content, content_type, components, sender_id }
|
|
598
|
+
*/
|
|
599
|
+
onMessage: function(callback) {
|
|
600
|
+
this._messageHandler = callback;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Usion SDK Results — server-side result persistence across devices
|
|
607
|
+
*/
|
|
608
|
+
|
|
609
|
+
function createResultsMethods(Usion) {
|
|
610
|
+
return {
|
|
611
|
+
/**
|
|
612
|
+
* Save a result to server-side storage (persists across devices).
|
|
613
|
+
* @param {string} data - Result string (URL, JSON, etc.)
|
|
614
|
+
* @param {object} [metadata] - Optional metadata (thumbnail_url, title, type)
|
|
615
|
+
* @returns {Promise<object>} The saved result document
|
|
616
|
+
*/
|
|
617
|
+
saveResult: function(data, metadata) {
|
|
618
|
+
return Usion._request('SAVE_RESULT', { data: data, metadata: metadata || {} }, 15000);
|
|
619
|
+
},
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Delete a saved result by ID.
|
|
623
|
+
* @param {string} resultId - The result ID to delete
|
|
624
|
+
* @returns {Promise<void>}
|
|
625
|
+
*/
|
|
626
|
+
deleteResult: function(resultId) {
|
|
627
|
+
return Usion._request('DELETE_RESULT', { resultId: resultId });
|
|
628
|
+
},
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Get all saved results for this service (populated from INIT config).
|
|
632
|
+
* @returns {Array} Array of result objects
|
|
633
|
+
*/
|
|
634
|
+
getResults: function() {
|
|
635
|
+
return Usion._results || [];
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Usion SDK Back Button — claim/release host back button for in-app navigation
|
|
642
|
+
*/
|
|
643
|
+
|
|
644
|
+
function createBackButtonMethods(Usion) {
|
|
645
|
+
return {
|
|
646
|
+
/**
|
|
647
|
+
* Claim the host app's back button for one-time in-app navigation.
|
|
648
|
+
* When claimed, pressing back sends BACK_BUTTON_PRESSED to the mini app
|
|
649
|
+
* instead of closing it. Automatically resets after one press.
|
|
650
|
+
* @param {function} callback - Called when the user presses the claimed back button
|
|
651
|
+
*/
|
|
652
|
+
claimBackButton: function(callback) {
|
|
653
|
+
Usion._backButtonCallback = callback;
|
|
654
|
+
Usion._post({ type: 'CLAIM_BACK_BUTTON' });
|
|
655
|
+
},
|
|
649
656
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
657
|
+
/**
|
|
658
|
+
* Release a previously claimed back button, restoring default close behavior.
|
|
659
|
+
*/
|
|
660
|
+
releaseBackButton: function() {
|
|
661
|
+
Usion._backButtonCallback = null;
|
|
662
|
+
Usion._post({ type: 'RELEASE_BACK_BUTTON' });
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Usion SDK UI Utilities — DOM helpers for mini apps
|
|
669
|
+
*/
|
|
653
670
|
|
|
671
|
+
const uiMethods = {
|
|
654
672
|
/**
|
|
655
673
|
* Set button to loading state
|
|
656
674
|
* @param {HTMLElement|string} btn - Button element or selector
|
|
@@ -700,13 +718,13 @@
|
|
|
700
718
|
charCount: function(input, counter, max) {
|
|
701
719
|
const inputEl = typeof input === 'string' ? document.querySelector(input) : input;
|
|
702
720
|
const counterEl = typeof counter === 'string' ? document.querySelector(counter) : counter;
|
|
703
|
-
|
|
721
|
+
|
|
704
722
|
if (!inputEl || !counterEl) return;
|
|
705
723
|
|
|
706
724
|
function update() {
|
|
707
725
|
const count = inputEl.value.length;
|
|
708
726
|
counterEl.textContent = count + ' / ' + max;
|
|
709
|
-
|
|
727
|
+
|
|
710
728
|
counterEl.classList.remove('warning', 'error');
|
|
711
729
|
if (count > max * 0.9) {
|
|
712
730
|
counterEl.classList.add('error');
|
|
@@ -737,11 +755,11 @@
|
|
|
737
755
|
container.querySelectorAll(itemSelector).forEach(function(i) {
|
|
738
756
|
i.classList.remove('selected');
|
|
739
757
|
});
|
|
740
|
-
|
|
758
|
+
|
|
741
759
|
// Select this one
|
|
742
760
|
item.classList.add('selected');
|
|
743
761
|
selected = item.dataset.value || item.dataset.id;
|
|
744
|
-
|
|
762
|
+
|
|
745
763
|
if (onChange) onChange(selected, item);
|
|
746
764
|
});
|
|
747
765
|
});
|
|
@@ -755,1025 +773,1206 @@
|
|
|
755
773
|
selected = null;
|
|
756
774
|
}
|
|
757
775
|
};
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Usion SDK Misc — submit, error, exit, share, log, on, requestPayment (legacy)
|
|
781
|
+
*/
|
|
782
|
+
|
|
783
|
+
const miscMethods = {
|
|
784
|
+
/**
|
|
785
|
+
* Request payment from user (legacy method)
|
|
786
|
+
* @deprecated Use Usion.wallet.requestPayment instead
|
|
787
|
+
*/
|
|
788
|
+
requestPayment: function(amount, reason, data) {
|
|
789
|
+
return this.wallet.requestPayment(amount, reason, data);
|
|
758
790
|
},
|
|
759
791
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
792
|
+
/**
|
|
793
|
+
* Submit result and signal completion
|
|
794
|
+
* @param {object} data - Result data to send to parent
|
|
795
|
+
*/
|
|
796
|
+
submit: function(data) {
|
|
797
|
+
this._post({
|
|
798
|
+
type: 'SUBMIT',
|
|
799
|
+
data: data
|
|
800
|
+
});
|
|
801
|
+
},
|
|
763
802
|
|
|
764
803
|
/**
|
|
765
|
-
*
|
|
804
|
+
* Report an error to parent app
|
|
805
|
+
* @param {string} message - Error message
|
|
766
806
|
*/
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
directMode: false,
|
|
774
|
-
directConfig: null,
|
|
775
|
-
_directSeq: 0,
|
|
776
|
-
_eventHandlers: {},
|
|
777
|
-
_lastSequence: 0,
|
|
778
|
-
_connecting: false,
|
|
779
|
-
_connectPromise: null,
|
|
780
|
-
_joined: false,
|
|
781
|
-
_joinPromise: null,
|
|
782
|
-
_useProxy: false,
|
|
783
|
-
_proxyListenerSetup: false,
|
|
784
|
-
_heartbeatInterval: null,
|
|
807
|
+
error: function(message) {
|
|
808
|
+
this._post({
|
|
809
|
+
type: 'ERROR',
|
|
810
|
+
message: message
|
|
811
|
+
});
|
|
812
|
+
},
|
|
785
813
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
814
|
+
/**
|
|
815
|
+
* Request to close the mini app
|
|
816
|
+
* @param {object} [options] - Optional exit options
|
|
817
|
+
* @param {number} [options.backCount] - Number of screens to go back (default 1)
|
|
818
|
+
*/
|
|
819
|
+
exit: function(options) {
|
|
820
|
+
var msg = { type: 'EXIT' };
|
|
821
|
+
if (options && typeof options.backCount === 'number' && options.backCount > 1) {
|
|
822
|
+
msg.backCount = options.backCount;
|
|
823
|
+
}
|
|
824
|
+
this._post(msg);
|
|
825
|
+
},
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Share content through the app's native share and optionally post to Usions feed
|
|
829
|
+
*
|
|
830
|
+
* @param {string} contentType - Type of content: 'audio' | 'image' | 'video' | 'text' | 'mixed'
|
|
831
|
+
* @param {object} data - Content data to share:
|
|
832
|
+
* - text: Optional text/caption for the post
|
|
833
|
+
* - audioUrl: URL for audio content (when contentType is 'audio')
|
|
834
|
+
* - imageUrl: URL for image content (when contentType is 'image')
|
|
835
|
+
* - videoUrl: URL for video content (when contentType is 'video')
|
|
836
|
+
* - thumbnailUrl: Optional thumbnail URL for video/audio
|
|
837
|
+
* - width: Optional width for image/video
|
|
838
|
+
* - height: Optional height for image/video
|
|
839
|
+
* - duration: Optional duration in seconds for audio/video
|
|
840
|
+
* - media: Array of media items for 'mixed' content type
|
|
841
|
+
* - Each item: { type: 'image'|'video'|'audio', url: string, thumbnailUrl?, width?, height?, duration? }
|
|
842
|
+
*/
|
|
843
|
+
share: function(contentType, data) {
|
|
844
|
+
var shareData = Object.assign({}, data, {
|
|
845
|
+
contentType: contentType,
|
|
846
|
+
serviceId: this.config.serviceId,
|
|
847
|
+
serviceName: this.config.serviceName
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
this._post({
|
|
851
|
+
type: 'SHARE',
|
|
852
|
+
contentType: contentType,
|
|
853
|
+
data: shareData
|
|
854
|
+
});
|
|
855
|
+
},
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Download a file to the device's storage / gallery.
|
|
859
|
+
* @param {string} url - URL of the file to download
|
|
860
|
+
* @param {string} [filename] - Optional filename (default: 'download.mp4')
|
|
861
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
862
|
+
*/
|
|
863
|
+
download: function(url, filename) {
|
|
864
|
+
return this._request('DOWNLOAD_FILE', {
|
|
865
|
+
url: url,
|
|
866
|
+
filename: filename || 'download.mp4'
|
|
867
|
+
});
|
|
868
|
+
},
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Log message to native console (for debugging)
|
|
872
|
+
* @param {string} msg - Message to log
|
|
873
|
+
*/
|
|
874
|
+
log: function(msg) {
|
|
875
|
+
this._post({
|
|
876
|
+
type: 'LOG',
|
|
877
|
+
msg: msg
|
|
878
|
+
});
|
|
879
|
+
console.log('[Usion]', msg);
|
|
880
|
+
},
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Listen for messages from parent app
|
|
884
|
+
* @param {string} type - Message type to listen for
|
|
885
|
+
* @param {function} callback - Handler function
|
|
886
|
+
*/
|
|
887
|
+
on: function(type, callback) {
|
|
888
|
+
window.addEventListener('message', function(event) {
|
|
889
|
+
let data;
|
|
890
|
+
try {
|
|
891
|
+
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
892
|
+
} catch (e) {
|
|
893
|
+
return;
|
|
821
894
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
// connection, avoids CORS issues, and avoids mixed-content blocks.
|
|
826
|
-
// Detection order (first truthy wins):
|
|
827
|
-
// (1) __USION_PROXY__ injected by parent before page load (most reliable)
|
|
828
|
-
// (2) iframe check (window.parent !== window)
|
|
829
|
-
// (3) ReactNativeWebView global
|
|
830
|
-
// (4) _isEmbedded flag set when INIT message was received
|
|
831
|
-
var isInFrame = !!window.__USION_PROXY__
|
|
832
|
-
|| window.parent !== window
|
|
833
|
-
|| !!window.ReactNativeWebView
|
|
834
|
-
|| !!Usion._isEmbedded;
|
|
835
|
-
|
|
836
|
-
if (isInFrame) {
|
|
837
|
-
Usion.log('Running in iframe – using parent app as socket proxy');
|
|
838
|
-
return self._connectViaProxy();
|
|
895
|
+
|
|
896
|
+
if (data.type === type) {
|
|
897
|
+
callback(data);
|
|
839
898
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
self._initSocket(socketUrl, token, resolve, reject);
|
|
867
|
-
}
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
return self._connectPromise;
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Usion SDK File Storage — large binary data (IndexedDB / filesystem)
|
|
905
|
+
*
|
|
906
|
+
* Scoped per-user, per-service like regular storage.
|
|
907
|
+
* Uses IndexedDB (web) or filesystem (mobile) — no localStorage size limits.
|
|
908
|
+
*/
|
|
909
|
+
|
|
910
|
+
function createFileStorageModule(sdk) {
|
|
911
|
+
return {
|
|
912
|
+
/**
|
|
913
|
+
* Store a file (base64 encoded)
|
|
914
|
+
* @param {string} key - Storage key
|
|
915
|
+
* @param {string} base64Data - Base64-encoded file content (no data: prefix)
|
|
916
|
+
* @param {string} mimeType - MIME type (e.g. 'image/png')
|
|
917
|
+
* @returns {Promise<void>}
|
|
918
|
+
*/
|
|
919
|
+
set: function(key, base64Data, mimeType) {
|
|
920
|
+
return sdk._request('FILE_STORAGE_SET', {
|
|
921
|
+
key: key,
|
|
922
|
+
base64Data: base64Data,
|
|
923
|
+
mimeType: mimeType || 'application/octet-stream'
|
|
924
|
+
}, 30000).then(function() { return; });
|
|
871
925
|
},
|
|
872
926
|
|
|
873
927
|
/**
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
* @returns {Promise}
|
|
928
|
+
* Get a stored file
|
|
929
|
+
* @param {string} key - Storage key
|
|
930
|
+
* @returns {Promise<{base64Data: string, mimeType: string} | null>}
|
|
877
931
|
*/
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
932
|
+
get: function(key) {
|
|
933
|
+
return sdk._request('FILE_STORAGE_GET', { key: key }, 30000).then(function(response) {
|
|
934
|
+
if (!response || !response.base64Data) return null;
|
|
935
|
+
return { base64Data: response.base64Data, mimeType: response.mimeType };
|
|
936
|
+
});
|
|
937
|
+
},
|
|
881
938
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
939
|
+
/**
|
|
940
|
+
* Remove a stored file
|
|
941
|
+
* @param {string} key - Storage key
|
|
942
|
+
* @returns {Promise<void>}
|
|
943
|
+
*/
|
|
944
|
+
remove: function(key) {
|
|
945
|
+
return sdk._request('FILE_STORAGE_REMOVE', { key: key }).then(function() { return; });
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Usion SDK Game Direct — WebSocket direct connection to game server
|
|
952
|
+
*/
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Add direct WebSocket connection methods to game module
|
|
956
|
+
* @param {object} game - The game module object
|
|
957
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
958
|
+
*/
|
|
959
|
+
function applyGameDirect(game, Usion) {
|
|
960
|
+
/**
|
|
961
|
+
* Connect directly to creator-controlled WebSocket server.
|
|
962
|
+
* Uses backend-issued short-lived room token.
|
|
963
|
+
* @returns {Promise}
|
|
964
|
+
*/
|
|
965
|
+
game.connectDirect = function(config) {
|
|
966
|
+
var self = this;
|
|
967
|
+
config = config || {};
|
|
888
968
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
self.directConfig = access;
|
|
894
|
-
return self._initDirectSocket(access);
|
|
895
|
-
})
|
|
896
|
-
.then(function() {
|
|
897
|
-
self.connected = true;
|
|
898
|
-
self._connecting = false;
|
|
899
|
-
Usion.log('Direct game socket connected');
|
|
900
|
-
})
|
|
901
|
-
.catch(function(err) {
|
|
902
|
-
self._connecting = false;
|
|
903
|
-
self.connected = false;
|
|
904
|
-
self.directMode = false;
|
|
905
|
-
if (self._eventHandlers.connectionError) {
|
|
906
|
-
self._eventHandlers.connectionError(err);
|
|
907
|
-
}
|
|
908
|
-
throw err;
|
|
909
|
-
});
|
|
969
|
+
if (self.directMode && self.directSocket && self.connected) {
|
|
970
|
+
return Promise.resolve();
|
|
971
|
+
}
|
|
972
|
+
if (self._connecting && self._connectPromise) {
|
|
910
973
|
return self._connectPromise;
|
|
911
|
-
}
|
|
974
|
+
}
|
|
912
975
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
headers: {
|
|
932
|
-
'Content-Type': 'application/json',
|
|
933
|
-
'Authorization': 'Bearer ' + token
|
|
934
|
-
},
|
|
935
|
-
body: JSON.stringify({
|
|
936
|
-
service_id: serviceId,
|
|
937
|
-
client_type: 'iframe',
|
|
938
|
-
protocol_version: (config.protocolVersion || Usion.config.protocolVersion || Usion.config.protocol_version || '2')
|
|
939
|
-
})
|
|
940
|
-
}).then(function(res) {
|
|
941
|
-
if (!res.ok) {
|
|
942
|
-
return res.text().then(function(text) {
|
|
943
|
-
throw new Error(text || ('Direct access failed: HTTP ' + res.status));
|
|
944
|
-
});
|
|
976
|
+
self._connecting = true;
|
|
977
|
+
self.directMode = true;
|
|
978
|
+
self._connectPromise = self._fetchDirectAccess(config)
|
|
979
|
+
.then(function(access) {
|
|
980
|
+
self.directConfig = access;
|
|
981
|
+
return self._initDirectSocket(access);
|
|
982
|
+
})
|
|
983
|
+
.then(function() {
|
|
984
|
+
self.connected = true;
|
|
985
|
+
self._connecting = false;
|
|
986
|
+
Usion.log('Direct game socket connected');
|
|
987
|
+
})
|
|
988
|
+
.catch(function(err) {
|
|
989
|
+
self._connecting = false;
|
|
990
|
+
self.connected = false;
|
|
991
|
+
self.directMode = false;
|
|
992
|
+
if (self._eventHandlers.connectionError) {
|
|
993
|
+
self._eventHandlers.connectionError(err);
|
|
945
994
|
}
|
|
946
|
-
|
|
995
|
+
throw err;
|
|
947
996
|
});
|
|
948
|
-
|
|
997
|
+
return self._connectPromise;
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
game._fetchDirectAccess = function(config) {
|
|
1001
|
+
var roomId = config.roomId || this.roomId || Usion.config.roomId;
|
|
1002
|
+
var serviceId = config.serviceId || Usion.config.serviceId;
|
|
1003
|
+
var apiUrl = config.apiUrl || Usion.config.apiUrl || '';
|
|
1004
|
+
var token = config.token || Usion.user.getToken();
|
|
1005
|
+
|
|
1006
|
+
if (!roomId) return Promise.reject(new Error('No room ID provided'));
|
|
1007
|
+
if (!serviceId) return Promise.reject(new Error('No service ID provided'));
|
|
1008
|
+
|
|
1009
|
+
this.roomId = roomId;
|
|
1010
|
+
this.playerId = Usion.user.getId();
|
|
1011
|
+
|
|
1012
|
+
// When embedded (iframe/WebView), proxy through parent app to avoid
|
|
1013
|
+
// CORS / Private Network Access issues
|
|
1014
|
+
if (Usion._isEmbedded) {
|
|
1015
|
+
return Usion._request('GAME_ACCESS_REQUEST', {
|
|
1016
|
+
room_id: roomId,
|
|
1017
|
+
service_id: serviceId,
|
|
1018
|
+
protocol_version: (config.protocolVersion || Usion.config.protocolVersion || Usion.config.protocol_version || '2')
|
|
1019
|
+
}, 10000);
|
|
1020
|
+
}
|
|
949
1021
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1022
|
+
if (!apiUrl) return Promise.reject(new Error('No API URL provided'));
|
|
1023
|
+
if (!token) return Promise.reject(new Error('No auth token available'));
|
|
1024
|
+
|
|
1025
|
+
var cleanApiUrl = String(apiUrl).replace(/\/$/, '');
|
|
1026
|
+
var endpoint = cleanApiUrl + '/games/rooms/' + encodeURIComponent(roomId) + '/access';
|
|
1027
|
+
return fetch(endpoint, {
|
|
1028
|
+
method: 'POST',
|
|
1029
|
+
headers: {
|
|
1030
|
+
'Content-Type': 'application/json',
|
|
1031
|
+
'Authorization': 'Bearer ' + token
|
|
1032
|
+
},
|
|
1033
|
+
body: JSON.stringify({
|
|
1034
|
+
service_id: serviceId,
|
|
1035
|
+
client_type: 'iframe',
|
|
1036
|
+
protocol_version: (config.protocolVersion || Usion.config.protocolVersion || Usion.config.protocol_version || '2')
|
|
1037
|
+
})
|
|
1038
|
+
}).then(function(res) {
|
|
1039
|
+
if (!res.ok) {
|
|
1040
|
+
return res.text().then(function(text) {
|
|
1041
|
+
throw new Error(text || ('Direct access failed: HTTP ' + res.status));
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
return res.json();
|
|
1045
|
+
});
|
|
1046
|
+
};
|
|
957
1047
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1048
|
+
game._initDirectSocket = function(access) {
|
|
1049
|
+
var self = this;
|
|
1050
|
+
return new Promise(function(resolve, reject) {
|
|
1051
|
+
if (!access || !access.ws_url || !access.access_token) {
|
|
1052
|
+
reject(new Error('Invalid direct access payload'));
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
var wsUrl = access.ws_url;
|
|
1057
|
+
var separator = wsUrl.indexOf('?') === -1 ? '?' : '&';
|
|
1058
|
+
var urlWithToken = wsUrl + separator + 'token=' + encodeURIComponent(access.access_token);
|
|
1059
|
+
var ws = new WebSocket(urlWithToken);
|
|
1060
|
+
self.directSocket = ws;
|
|
1061
|
+
|
|
1062
|
+
var opened = false;
|
|
1063
|
+
var joinSent = false;
|
|
1064
|
+
var timeout = setTimeout(function() {
|
|
1065
|
+
if (!opened) {
|
|
1066
|
+
try { ws.close(); } catch (e) {}
|
|
1067
|
+
reject(new Error('Direct WebSocket connection timeout'));
|
|
1068
|
+
}
|
|
1069
|
+
}, 10000);
|
|
1070
|
+
|
|
1071
|
+
ws.onopen = function() {
|
|
1072
|
+
opened = true;
|
|
1073
|
+
clearTimeout(timeout);
|
|
1074
|
+
if (!joinSent) {
|
|
1075
|
+
joinSent = true;
|
|
1076
|
+
self._sendDirect('join', {});
|
|
1077
|
+
}
|
|
1078
|
+
// Start heartbeat for direct mode
|
|
1079
|
+
if (self._heartbeatInterval) clearInterval(self._heartbeatInterval);
|
|
1080
|
+
self._heartbeatInterval = setInterval(function() {
|
|
1081
|
+
if (self.directSocket && self.directSocket.readyState === WebSocket.OPEN) {
|
|
1082
|
+
self._sendDirect('heartbeat', {});
|
|
970
1083
|
}
|
|
971
|
-
},
|
|
1084
|
+
}, 25000);
|
|
1085
|
+
resolve();
|
|
1086
|
+
};
|
|
972
1087
|
|
|
973
|
-
|
|
974
|
-
|
|
1088
|
+
ws.onerror = function() {
|
|
1089
|
+
if (!opened) {
|
|
975
1090
|
clearTimeout(timeout);
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
980
|
-
// Start heartbeat for direct mode
|
|
981
|
-
if (self._heartbeatInterval) clearInterval(self._heartbeatInterval);
|
|
982
|
-
self._heartbeatInterval = setInterval(function() {
|
|
983
|
-
if (self.directSocket && self.directSocket.readyState === WebSocket.OPEN) {
|
|
984
|
-
self._sendDirect('heartbeat', {});
|
|
985
|
-
}
|
|
986
|
-
}, 25000);
|
|
987
|
-
resolve();
|
|
988
|
-
};
|
|
1091
|
+
reject(new Error('Direct WebSocket connection error'));
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
989
1094
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1095
|
+
ws.onclose = function(evt) {
|
|
1096
|
+
self.connected = false;
|
|
1097
|
+
self._joined = false;
|
|
1098
|
+
self._joinPromise = null;
|
|
1099
|
+
if (self._heartbeatInterval) {
|
|
1100
|
+
clearInterval(self._heartbeatInterval);
|
|
1101
|
+
self._heartbeatInterval = null;
|
|
1102
|
+
}
|
|
1103
|
+
if (self._eventHandlers.disconnect) {
|
|
1104
|
+
self._eventHandlers.disconnect(evt && evt.reason ? evt.reason : 'direct socket closed');
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
996
1107
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1108
|
+
ws.onmessage = function(evt) {
|
|
1109
|
+
self._handleDirectMessage(evt && evt.data);
|
|
1110
|
+
};
|
|
1111
|
+
});
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
game._sendDirect = function(type, payload) {
|
|
1115
|
+
if (!this.directSocket || this.directSocket.readyState !== WebSocket.OPEN) return;
|
|
1116
|
+
this._directSeq = this._directSeq + 1;
|
|
1117
|
+
this.directSocket.send(JSON.stringify({
|
|
1118
|
+
type: type,
|
|
1119
|
+
room_id: this.roomId,
|
|
1120
|
+
ts: Date.now(),
|
|
1121
|
+
seq: this._directSeq,
|
|
1122
|
+
session_id: (this.directConfig && this.directConfig.session_id) ? this.directConfig.session_id : null,
|
|
1123
|
+
protocol_version: (this.directConfig && this.directConfig.protocol_version) ? this.directConfig.protocol_version : '2',
|
|
1124
|
+
payload: payload || {}
|
|
1125
|
+
}));
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
game._handleDirectMessage = function(raw) {
|
|
1129
|
+
var data;
|
|
1130
|
+
try {
|
|
1131
|
+
data = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
1132
|
+
} catch (e) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (!data || !data.type) return;
|
|
1136
|
+
var payload = data.payload || {};
|
|
1009
1137
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
}
|
|
1138
|
+
if (data.type === 'joined') {
|
|
1139
|
+
this._joined = true;
|
|
1140
|
+
if (this._eventHandlers.joined) this._eventHandlers.joined(payload);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (data.type === 'player_joined') {
|
|
1144
|
+
if (this._eventHandlers.playerJoined) this._eventHandlers.playerJoined(payload);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (data.type === 'player_left') {
|
|
1148
|
+
if (this._eventHandlers.playerLeft) this._eventHandlers.playerLeft(payload);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (data.type === 'state_snapshot' || data.type === 'state_delta') {
|
|
1152
|
+
if (this._eventHandlers.realtime) this._eventHandlers.realtime(payload);
|
|
1153
|
+
if (this._eventHandlers.stateUpdate) this._eventHandlers.stateUpdate(payload);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (data.type === 'pong') {
|
|
1157
|
+
if (this._eventHandlers.sync) this._eventHandlers.sync(payload);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
if (data.type === 'match_end') {
|
|
1161
|
+
if (this._eventHandlers.finished) this._eventHandlers.finished(payload);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (data.type === 'error' && this._eventHandlers.error) {
|
|
1165
|
+
this._eventHandlers.error(payload);
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Usion SDK Game Socket — Socket.IO connection management
|
|
1172
|
+
*/
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Add Socket.IO connection methods to game module
|
|
1176
|
+
* @param {object} game - The game module object
|
|
1177
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
1178
|
+
*/
|
|
1179
|
+
function applyGameSocket(game, Usion) {
|
|
1180
|
+
/**
|
|
1181
|
+
* Initialize socket connection
|
|
1182
|
+
* @private
|
|
1183
|
+
*/
|
|
1184
|
+
game._initSocket = function(socketUrl, token, resolve, reject) {
|
|
1185
|
+
const self = this;
|
|
1015
1186
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
ts: Date.now(),
|
|
1023
|
-
seq: this._directSeq,
|
|
1024
|
-
session_id: (this.directConfig && this.directConfig.session_id) ? this.directConfig.session_id : null,
|
|
1025
|
-
protocol_version: (this.directConfig && this.directConfig.protocol_version) ? this.directConfig.protocol_version : '2',
|
|
1026
|
-
payload: payload || {}
|
|
1027
|
-
}));
|
|
1028
|
-
},
|
|
1187
|
+
// Prevent creating duplicate sockets
|
|
1188
|
+
if (self.socket && self.socket.connected) {
|
|
1189
|
+
self._connecting = false;
|
|
1190
|
+
resolve();
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1029
1193
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
if (!data || !data.type) return;
|
|
1038
|
-
var payload = data.payload || {};
|
|
1194
|
+
// Clean up any existing disconnected socket
|
|
1195
|
+
if (self.socket) {
|
|
1196
|
+
self.socket.disconnect();
|
|
1197
|
+
self.socket = null;
|
|
1198
|
+
}
|
|
1039
1199
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
if (data.type === 'state_snapshot' || data.type === 'state_delta') {
|
|
1054
|
-
if (this._eventHandlers.realtime) this._eventHandlers.realtime(payload);
|
|
1055
|
-
if (this._eventHandlers.stateUpdate) this._eventHandlers.stateUpdate(payload);
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
if (data.type === 'pong') {
|
|
1059
|
-
if (this._eventHandlers.sync) this._eventHandlers.sync(payload);
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
if (data.type === 'match_end') {
|
|
1063
|
-
if (this._eventHandlers.finished) this._eventHandlers.finished(payload);
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
if (data.type === 'error' && this._eventHandlers.error) {
|
|
1067
|
-
this._eventHandlers.error(payload);
|
|
1068
|
-
}
|
|
1069
|
-
},
|
|
1200
|
+
try {
|
|
1201
|
+
self.socket = io(socketUrl, {
|
|
1202
|
+
path: '/socket.io',
|
|
1203
|
+
transports: ['websocket', 'polling'],
|
|
1204
|
+
auth: { token: token },
|
|
1205
|
+
autoConnect: true,
|
|
1206
|
+
reconnection: true,
|
|
1207
|
+
reconnectionAttempts: 50,
|
|
1208
|
+
reconnectionDelay: 1000,
|
|
1209
|
+
reconnectionDelayMax: 10000
|
|
1210
|
+
});
|
|
1070
1211
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
* @private
|
|
1074
|
-
*/
|
|
1075
|
-
_initSocket: function(socketUrl, token, resolve, reject) {
|
|
1076
|
-
const self = this;
|
|
1077
|
-
|
|
1078
|
-
// Prevent creating duplicate sockets
|
|
1079
|
-
if (self.socket && self.socket.connected) {
|
|
1212
|
+
self.socket.on('connect', function() {
|
|
1213
|
+
self.connected = true;
|
|
1080
1214
|
self._connecting = false;
|
|
1081
|
-
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// Clean up any existing disconnected socket
|
|
1086
|
-
if (self.socket) {
|
|
1087
|
-
self.socket.disconnect();
|
|
1088
|
-
self.socket = null;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
try {
|
|
1092
|
-
self.socket = io(socketUrl, {
|
|
1093
|
-
path: '/socket.io',
|
|
1094
|
-
transports: ['websocket', 'polling'],
|
|
1095
|
-
auth: { token: token },
|
|
1096
|
-
autoConnect: true,
|
|
1097
|
-
reconnection: true,
|
|
1098
|
-
reconnectionAttempts: 50,
|
|
1099
|
-
reconnectionDelay: 1000,
|
|
1100
|
-
reconnectionDelayMax: 10000
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
self.socket.on('connect', function() {
|
|
1104
|
-
self.connected = true;
|
|
1105
|
-
self._connecting = false;
|
|
1106
|
-
Usion.log('Game socket connected');
|
|
1107
|
-
|
|
1108
|
-
// Start heartbeat to keep game session alive
|
|
1109
|
-
if (self._heartbeatInterval) clearInterval(self._heartbeatInterval);
|
|
1110
|
-
self._heartbeatInterval = setInterval(function() {
|
|
1111
|
-
if (self.socket && self.connected && self.roomId) {
|
|
1112
|
-
self.socket.emit('game:heartbeat', { room_id: self.roomId });
|
|
1113
|
-
}
|
|
1114
|
-
}, 25000);
|
|
1115
|
-
|
|
1116
|
-
// Re-join room after reconnect so this socket gets room broadcasts again
|
|
1117
|
-
if (self.roomId) {
|
|
1118
|
-
self._joined = false;
|
|
1119
|
-
self._joinPromise = null;
|
|
1120
|
-
self.join(self.roomId)
|
|
1121
|
-
.then(function() {
|
|
1122
|
-
Usion.log('Reconnected - joined room ' + self.roomId);
|
|
1123
|
-
self.requestSync(self._lastSequence || 0);
|
|
1124
|
-
})
|
|
1125
|
-
.catch(function(err) {
|
|
1126
|
-
Usion.log('Rejoin failed: ' + (err && err.message ? err.message : String(err)));
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
resolve();
|
|
1131
|
-
});
|
|
1215
|
+
Usion.log('Game socket connected');
|
|
1132
1216
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
if (self.
|
|
1137
|
-
self.
|
|
1217
|
+
// Start heartbeat to keep game session alive
|
|
1218
|
+
if (self._heartbeatInterval) clearInterval(self._heartbeatInterval);
|
|
1219
|
+
self._heartbeatInterval = setInterval(function() {
|
|
1220
|
+
if (self.socket && self.connected && self.roomId) {
|
|
1221
|
+
self.socket.emit('game:heartbeat', { room_id: self.roomId });
|
|
1138
1222
|
}
|
|
1139
|
-
|
|
1140
|
-
});
|
|
1223
|
+
}, 25000);
|
|
1141
1224
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1225
|
+
// Re-join room after reconnect
|
|
1226
|
+
if (self.roomId) {
|
|
1144
1227
|
self._joined = false;
|
|
1145
1228
|
self._joinPromise = null;
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
self.socket.on('reconnect', function(attemptNumber) {
|
|
1157
|
-
Usion.log('Game socket reconnected after ' + attemptNumber + ' attempts');
|
|
1158
|
-
if (self._eventHandlers.reconnect) {
|
|
1159
|
-
self._eventHandlers.reconnect(attemptNumber);
|
|
1160
|
-
}
|
|
1161
|
-
});
|
|
1229
|
+
self.join(self.roomId)
|
|
1230
|
+
.then(function() {
|
|
1231
|
+
Usion.log('Reconnected - joined room ' + self.roomId);
|
|
1232
|
+
self.requestSync(self._lastSequence || 0);
|
|
1233
|
+
})
|
|
1234
|
+
.catch(function(err) {
|
|
1235
|
+
Usion.log('Rejoin failed: ' + (err && err.message ? err.message : String(err)));
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1162
1238
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
if (data.sequence !== undefined) {
|
|
1166
|
-
self._lastSequence = data.sequence;
|
|
1167
|
-
}
|
|
1168
|
-
if (self._eventHandlers.joined) {
|
|
1169
|
-
self._eventHandlers.joined(data);
|
|
1170
|
-
}
|
|
1171
|
-
});
|
|
1239
|
+
resolve();
|
|
1240
|
+
});
|
|
1172
1241
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1242
|
+
self.socket.on('connect_error', function(err) {
|
|
1243
|
+
self._connecting = false;
|
|
1244
|
+
Usion.log('Game socket error: ' + err.message);
|
|
1245
|
+
if (self._eventHandlers.connectionError) {
|
|
1246
|
+
self._eventHandlers.connectionError(err);
|
|
1247
|
+
}
|
|
1248
|
+
reject(err);
|
|
1249
|
+
});
|
|
1178
1250
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1251
|
+
self.socket.on('disconnect', function(reason) {
|
|
1252
|
+
self.connected = false;
|
|
1253
|
+
self._joined = false;
|
|
1254
|
+
self._joinPromise = null;
|
|
1255
|
+
if (self._heartbeatInterval) {
|
|
1256
|
+
clearInterval(self._heartbeatInterval);
|
|
1257
|
+
self._heartbeatInterval = null;
|
|
1258
|
+
}
|
|
1259
|
+
Usion.log('Game socket disconnected: ' + reason);
|
|
1260
|
+
if (self._eventHandlers.disconnect) {
|
|
1261
|
+
self._eventHandlers.disconnect(reason);
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1184
1264
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1192
|
-
});
|
|
1265
|
+
self.socket.on('reconnect', function(attemptNumber) {
|
|
1266
|
+
Usion.log('Game socket reconnected after ' + attemptNumber + ' attempts');
|
|
1267
|
+
if (self._eventHandlers.reconnect) {
|
|
1268
|
+
self._eventHandlers.reconnect(attemptNumber);
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1193
1271
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
self._eventHandlers.stateUpdate(data);
|
|
1204
|
-
}
|
|
1205
|
-
});
|
|
1272
|
+
// Game event handlers
|
|
1273
|
+
self.socket.on('game:joined', function(data) {
|
|
1274
|
+
if (data.sequence !== undefined) {
|
|
1275
|
+
self._lastSequence = data.sequence;
|
|
1276
|
+
}
|
|
1277
|
+
if (self._eventHandlers.joined) {
|
|
1278
|
+
self._eventHandlers.joined(data);
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1206
1281
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
self._eventHandlers.action(data);
|
|
1213
|
-
}
|
|
1214
|
-
});
|
|
1282
|
+
self.socket.on('game:player_joined', function(data) {
|
|
1283
|
+
if (self._eventHandlers.playerJoined) {
|
|
1284
|
+
self._eventHandlers.playerJoined(data);
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1215
1287
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1288
|
+
self.socket.on('game:player_left', function(data) {
|
|
1289
|
+
if (self._eventHandlers.playerLeft) {
|
|
1290
|
+
self._eventHandlers.playerLeft(data);
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1221
1293
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1294
|
+
self.socket.on('game:state', function(data) {
|
|
1295
|
+
if (data.sequence !== undefined) {
|
|
1296
|
+
self._lastSequence = Math.max(self._lastSequence, data.sequence);
|
|
1297
|
+
}
|
|
1298
|
+
if (self._eventHandlers.stateUpdate) {
|
|
1299
|
+
self._eventHandlers.stateUpdate(data);
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1230
1302
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1303
|
+
self.socket.on('game:sync', function(data) {
|
|
1304
|
+
if (data.sequence !== undefined) {
|
|
1305
|
+
self._lastSequence = data.sequence;
|
|
1306
|
+
}
|
|
1307
|
+
if (self._eventHandlers.sync) {
|
|
1308
|
+
self._eventHandlers.sync(data);
|
|
1309
|
+
}
|
|
1310
|
+
// Also trigger stateUpdate for backwards compat
|
|
1311
|
+
if (self._eventHandlers.stateUpdate) {
|
|
1312
|
+
self._eventHandlers.stateUpdate(data);
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1237
1315
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1316
|
+
self.socket.on('game:action', function(data) {
|
|
1317
|
+
if (data.sequence !== undefined) {
|
|
1318
|
+
self._lastSequence = Math.max(self._lastSequence, data.sequence);
|
|
1319
|
+
}
|
|
1320
|
+
if (self._eventHandlers.action) {
|
|
1321
|
+
self._eventHandlers.action(data);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1243
1324
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
});
|
|
1325
|
+
self.socket.on('game:realtime', function(data) {
|
|
1326
|
+
if (self._eventHandlers.realtime) {
|
|
1327
|
+
self._eventHandlers.realtime(data);
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1250
1330
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1331
|
+
self.socket.on('game:finished', function(data) {
|
|
1332
|
+
if (data.sequence !== undefined) {
|
|
1333
|
+
self._lastSequence = data.sequence;
|
|
1334
|
+
}
|
|
1335
|
+
if (self._eventHandlers.finished) {
|
|
1336
|
+
self._eventHandlers.finished(data);
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1255
1339
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
_connectViaProxy: function() {
|
|
1262
|
-
var self = this;
|
|
1263
|
-
|
|
1264
|
-
if (self._useProxy && self.connected) {
|
|
1265
|
-
return Promise.resolve();
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
self._useProxy = true;
|
|
1269
|
-
self._connecting = true;
|
|
1270
|
-
self._setupProxyListener();
|
|
1271
|
-
|
|
1272
|
-
self._connectPromise = new Promise(function(resolve, reject) {
|
|
1273
|
-
// Listen for GAME_CONNECTED from parent
|
|
1274
|
-
self._proxyConnectResolve = function() {
|
|
1275
|
-
self.connected = true;
|
|
1276
|
-
self._connecting = false;
|
|
1277
|
-
Usion.log('Game socket connected via parent proxy');
|
|
1278
|
-
resolve();
|
|
1279
|
-
};
|
|
1280
|
-
|
|
1281
|
-
// Send connect request to parent
|
|
1282
|
-
Usion._post({ type: 'GAME_CONNECT' });
|
|
1283
|
-
|
|
1284
|
-
// Timeout after 10s
|
|
1285
|
-
setTimeout(function() {
|
|
1286
|
-
if (!self.connected) {
|
|
1287
|
-
self._connecting = false;
|
|
1288
|
-
reject(new Error('Proxy connection timeout'));
|
|
1289
|
-
}
|
|
1290
|
-
}, 10000);
|
|
1340
|
+
self.socket.on('game:error', function(data) {
|
|
1341
|
+
Usion.log('Game error: ' + (data.message || data.code));
|
|
1342
|
+
if (self._eventHandlers.error) {
|
|
1343
|
+
self._eventHandlers.error(data);
|
|
1344
|
+
}
|
|
1291
1345
|
});
|
|
1292
|
-
|
|
1293
|
-
return self._connectPromise;
|
|
1294
|
-
},
|
|
1295
1346
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
*/
|
|
1300
|
-
_setupProxyListener: function() {
|
|
1301
|
-
var self = this;
|
|
1302
|
-
if (self._proxyListenerSetup) return;
|
|
1303
|
-
self._proxyListenerSetup = true;
|
|
1304
|
-
|
|
1305
|
-
window.addEventListener('message', function(event) {
|
|
1306
|
-
var data;
|
|
1307
|
-
try {
|
|
1308
|
-
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
1309
|
-
} catch (e) {
|
|
1310
|
-
return;
|
|
1347
|
+
self.socket.on('game:rematch_request', function(data) {
|
|
1348
|
+
if (self._eventHandlers.rematchRequest) {
|
|
1349
|
+
self._eventHandlers.rematchRequest(data);
|
|
1311
1350
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
self._proxyConnectResolve = null;
|
|
1319
|
-
}
|
|
1320
|
-
break;
|
|
1321
|
-
|
|
1322
|
-
case 'GAME_CONNECT_ERROR':
|
|
1323
|
-
self.connected = false;
|
|
1324
|
-
self._connecting = false;
|
|
1325
|
-
break;
|
|
1326
|
-
|
|
1327
|
-
case 'GAME_JOINED':
|
|
1328
|
-
self._joined = true;
|
|
1329
|
-
if (data.sequence !== undefined) self._lastSequence = data.sequence;
|
|
1330
|
-
if (self._proxyJoinResolve) {
|
|
1331
|
-
self._proxyJoinResolve(data);
|
|
1332
|
-
self._proxyJoinResolve = null;
|
|
1333
|
-
}
|
|
1334
|
-
if (self._eventHandlers.joined) self._eventHandlers.joined(data);
|
|
1335
|
-
break;
|
|
1336
|
-
|
|
1337
|
-
case 'GAME_JOIN_ERROR':
|
|
1338
|
-
self._joined = false;
|
|
1339
|
-
if (self._proxyJoinReject) {
|
|
1340
|
-
self._proxyJoinReject(new Error(data.message || 'Join failed'));
|
|
1341
|
-
self._proxyJoinReject = null;
|
|
1342
|
-
}
|
|
1343
|
-
break;
|
|
1344
|
-
|
|
1345
|
-
case 'GAME_PLAYER_JOINED':
|
|
1346
|
-
if (self._eventHandlers.playerJoined) self._eventHandlers.playerJoined(data);
|
|
1347
|
-
break;
|
|
1348
|
-
|
|
1349
|
-
case 'GAME_PLAYER_LEFT':
|
|
1350
|
-
if (self._eventHandlers.playerLeft) self._eventHandlers.playerLeft(data);
|
|
1351
|
-
break;
|
|
1352
|
-
|
|
1353
|
-
case 'GAME_STATE':
|
|
1354
|
-
if (data.sequence !== undefined) self._lastSequence = Math.max(self._lastSequence, data.sequence);
|
|
1355
|
-
if (self._eventHandlers.stateUpdate) self._eventHandlers.stateUpdate(data);
|
|
1356
|
-
break;
|
|
1357
|
-
|
|
1358
|
-
case 'GAME_ACTION_DATA':
|
|
1359
|
-
if (data.sequence !== undefined) self._lastSequence = Math.max(self._lastSequence, data.sequence);
|
|
1360
|
-
if (self._eventHandlers.action) self._eventHandlers.action(data);
|
|
1361
|
-
break;
|
|
1362
|
-
|
|
1363
|
-
case 'GAME_REALTIME_DATA':
|
|
1364
|
-
if (self._eventHandlers.realtime) self._eventHandlers.realtime(data);
|
|
1365
|
-
break;
|
|
1366
|
-
|
|
1367
|
-
case 'GAME_FINISHED':
|
|
1368
|
-
if (data.sequence !== undefined) self._lastSequence = data.sequence;
|
|
1369
|
-
if (self._eventHandlers.finished) self._eventHandlers.finished(data);
|
|
1370
|
-
break;
|
|
1371
|
-
|
|
1372
|
-
case 'GAME_ERROR':
|
|
1373
|
-
Usion.log('Game error via proxy: ' + (data.message || data.code));
|
|
1374
|
-
if (self._eventHandlers.error) self._eventHandlers.error(data);
|
|
1375
|
-
break;
|
|
1376
|
-
|
|
1377
|
-
case 'GAME_RESTARTED':
|
|
1378
|
-
self._lastSequence = 0;
|
|
1379
|
-
if (self._eventHandlers.restarted) self._eventHandlers.restarted(data);
|
|
1380
|
-
break;
|
|
1381
|
-
|
|
1382
|
-
case 'GAME_REMATCH_REQUEST':
|
|
1383
|
-
if (self._eventHandlers.rematchRequest) self._eventHandlers.rematchRequest(data);
|
|
1384
|
-
break;
|
|
1385
|
-
|
|
1386
|
-
case 'GAME_SYNC':
|
|
1387
|
-
if (data.sequence !== undefined) self._lastSequence = data.sequence;
|
|
1388
|
-
if (self._eventHandlers.sync) self._eventHandlers.sync(data);
|
|
1389
|
-
if (self._eventHandlers.stateUpdate) self._eventHandlers.stateUpdate(data);
|
|
1390
|
-
break;
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
self.socket.on('game:restarted', function(data) {
|
|
1354
|
+
self._lastSequence = 0; // Reset sequence on rematch
|
|
1355
|
+
if (self._eventHandlers.restarted) {
|
|
1356
|
+
self._eventHandlers.restarted(data);
|
|
1391
1357
|
}
|
|
1392
1358
|
});
|
|
1393
|
-
},
|
|
1394
1359
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
return self._joinPromise;
|
|
1419
|
-
}
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
reject(err);
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Usion SDK Game Proxy — postMessage relay through parent app
|
|
1368
|
+
*/
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Add proxy connection methods to game module
|
|
1372
|
+
* @param {object} game - The game module object
|
|
1373
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
1374
|
+
*/
|
|
1375
|
+
function applyGameProxy(game, Usion) {
|
|
1376
|
+
/**
|
|
1377
|
+
* Connect via parent app proxy (postMessage relay).
|
|
1378
|
+
* Used when mixed content prevents direct socket connection.
|
|
1379
|
+
* @private
|
|
1380
|
+
*/
|
|
1381
|
+
game._connectViaProxy = function() {
|
|
1382
|
+
var self = this;
|
|
1420
1383
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
self._proxyJoinResolve = resolve;
|
|
1425
|
-
self._proxyJoinReject = reject;
|
|
1426
|
-
Usion._post({ type: 'GAME_JOIN', room_id: roomId });
|
|
1427
|
-
setTimeout(function() {
|
|
1428
|
-
if (!self._joined && self._proxyJoinReject) {
|
|
1429
|
-
self._proxyJoinReject = null;
|
|
1430
|
-
reject(new Error('Join timeout'));
|
|
1431
|
-
}
|
|
1432
|
-
}, 15000);
|
|
1433
|
-
});
|
|
1434
|
-
return self._joinPromise;
|
|
1435
|
-
}
|
|
1384
|
+
if (self._useProxy && self.connected) {
|
|
1385
|
+
return Promise.resolve();
|
|
1386
|
+
}
|
|
1436
1387
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
return;
|
|
1441
|
-
}
|
|
1388
|
+
self._useProxy = true;
|
|
1389
|
+
self._connecting = true;
|
|
1390
|
+
self._setupProxyListener();
|
|
1442
1391
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1392
|
+
self._connectPromise = new Promise(function(resolve, reject) {
|
|
1393
|
+
// Listen for GAME_CONNECTED from parent
|
|
1394
|
+
self._proxyConnectResolve = function() {
|
|
1395
|
+
self.connected = true;
|
|
1396
|
+
self._connecting = false;
|
|
1397
|
+
Usion.log('Game socket connected via parent proxy');
|
|
1398
|
+
resolve();
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
// Send connect request to parent
|
|
1402
|
+
Usion._post({ type: 'GAME_CONNECT' });
|
|
1403
|
+
|
|
1404
|
+
// Timeout after 10s
|
|
1405
|
+
setTimeout(function() {
|
|
1406
|
+
if (!self.connected) {
|
|
1407
|
+
self._connecting = false;
|
|
1408
|
+
reject(new Error('Proxy connection timeout'));
|
|
1446
1409
|
}
|
|
1410
|
+
}, 10000);
|
|
1411
|
+
});
|
|
1447
1412
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
self._joined = false;
|
|
1451
|
-
reject(new Error(response.message || response.error));
|
|
1452
|
-
} else {
|
|
1453
|
-
self._joined = true;
|
|
1454
|
-
if (response.sequence !== undefined) {
|
|
1455
|
-
self._lastSequence = response.sequence;
|
|
1456
|
-
}
|
|
1457
|
-
resolve(response);
|
|
1458
|
-
}
|
|
1459
|
-
});
|
|
1460
|
-
});
|
|
1461
|
-
|
|
1462
|
-
return self._joinPromise;
|
|
1463
|
-
},
|
|
1413
|
+
return self._connectPromise;
|
|
1414
|
+
};
|
|
1464
1415
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1416
|
+
/**
|
|
1417
|
+
* Set up message listener for proxy game events from parent.
|
|
1418
|
+
* @private
|
|
1419
|
+
*/
|
|
1420
|
+
game._setupProxyListener = function() {
|
|
1421
|
+
var self = this;
|
|
1422
|
+
if (self._proxyListenerSetup) return;
|
|
1423
|
+
self._proxyListenerSetup = true;
|
|
1470
1424
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
self._joinPromise = null;
|
|
1477
|
-
return;
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
if (self._useProxy) {
|
|
1481
|
-
if (self.roomId) Usion._post({ type: 'GAME_LEAVE', room_id: self.roomId });
|
|
1482
|
-
self.roomId = null;
|
|
1483
|
-
self._lastSequence = 0;
|
|
1484
|
-
self._joined = false;
|
|
1485
|
-
self._joinPromise = null;
|
|
1425
|
+
window.addEventListener('message', function(event) {
|
|
1426
|
+
var data;
|
|
1427
|
+
try {
|
|
1428
|
+
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
1429
|
+
} catch (e) {
|
|
1486
1430
|
return;
|
|
1487
1431
|
}
|
|
1488
|
-
|
|
1489
|
-
if (self.socket && self.connected && self.roomId) {
|
|
1490
|
-
self.socket.emit('game:leave', { room_id: self.roomId });
|
|
1491
|
-
self.roomId = null;
|
|
1492
|
-
self._lastSequence = 0;
|
|
1493
|
-
self._joined = false;
|
|
1494
|
-
self._joinPromise = null;
|
|
1495
|
-
}
|
|
1496
|
-
},
|
|
1432
|
+
if (!data || typeof data !== 'object' || !self._useProxy) return;
|
|
1497
1433
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const self = this;
|
|
1434
|
+
switch (data.type) {
|
|
1435
|
+
case 'GAME_CONNECTED':
|
|
1436
|
+
if (self._proxyConnectResolve) {
|
|
1437
|
+
self._proxyConnectResolve();
|
|
1438
|
+
self._proxyConnectResolve = null;
|
|
1439
|
+
}
|
|
1440
|
+
break;
|
|
1506
1441
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1442
|
+
case 'GAME_CONNECT_ERROR':
|
|
1443
|
+
self.connected = false;
|
|
1444
|
+
self._connecting = false;
|
|
1445
|
+
break;
|
|
1446
|
+
|
|
1447
|
+
case 'GAME_JOINED':
|
|
1448
|
+
self._joined = true;
|
|
1449
|
+
if (data.sequence !== undefined) self._lastSequence = data.sequence;
|
|
1450
|
+
if (self._proxyJoinResolve) {
|
|
1451
|
+
self._proxyJoinResolve(data);
|
|
1452
|
+
self._proxyJoinResolve = null;
|
|
1453
|
+
}
|
|
1454
|
+
if (self._eventHandlers.joined) self._eventHandlers.joined(data);
|
|
1455
|
+
break;
|
|
1511
1456
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1457
|
+
case 'GAME_JOIN_ERROR':
|
|
1458
|
+
self._joined = false;
|
|
1459
|
+
if (self._proxyJoinReject) {
|
|
1460
|
+
self._proxyJoinReject(new Error(data.message || 'Join failed'));
|
|
1461
|
+
self._proxyJoinReject = null;
|
|
1462
|
+
}
|
|
1463
|
+
break;
|
|
1464
|
+
|
|
1465
|
+
case 'GAME_PLAYER_JOINED':
|
|
1466
|
+
if (self._eventHandlers.playerJoined) self._eventHandlers.playerJoined(data);
|
|
1467
|
+
break;
|
|
1468
|
+
|
|
1469
|
+
case 'GAME_PLAYER_LEFT':
|
|
1470
|
+
if (self._eventHandlers.playerLeft) self._eventHandlers.playerLeft(data);
|
|
1471
|
+
break;
|
|
1472
|
+
|
|
1473
|
+
case 'GAME_STATE':
|
|
1474
|
+
if (data.sequence !== undefined) self._lastSequence = Math.max(self._lastSequence, data.sequence);
|
|
1475
|
+
if (self._eventHandlers.stateUpdate) self._eventHandlers.stateUpdate(data);
|
|
1476
|
+
break;
|
|
1477
|
+
|
|
1478
|
+
case 'GAME_ACTION_DATA':
|
|
1479
|
+
if (data.sequence !== undefined) self._lastSequence = Math.max(self._lastSequence, data.sequence);
|
|
1480
|
+
if (self._eventHandlers.action) self._eventHandlers.action(data);
|
|
1481
|
+
break;
|
|
1482
|
+
|
|
1483
|
+
case 'GAME_REALTIME_DATA':
|
|
1484
|
+
if (self._eventHandlers.realtime) self._eventHandlers.realtime(data);
|
|
1485
|
+
break;
|
|
1486
|
+
|
|
1487
|
+
case 'GAME_FINISHED':
|
|
1488
|
+
if (data.sequence !== undefined) self._lastSequence = data.sequence;
|
|
1489
|
+
if (self._eventHandlers.finished) self._eventHandlers.finished(data);
|
|
1490
|
+
break;
|
|
1491
|
+
|
|
1492
|
+
case 'GAME_ERROR':
|
|
1493
|
+
Usion.log('Game error via proxy: ' + (data.message || data.code));
|
|
1494
|
+
if (self._eventHandlers.error) self._eventHandlers.error(data);
|
|
1495
|
+
break;
|
|
1496
|
+
|
|
1497
|
+
case 'GAME_RESTARTED':
|
|
1498
|
+
self._lastSequence = 0;
|
|
1499
|
+
if (self._eventHandlers.restarted) self._eventHandlers.restarted(data);
|
|
1500
|
+
break;
|
|
1501
|
+
|
|
1502
|
+
case 'GAME_REMATCH_REQUEST':
|
|
1503
|
+
if (self._eventHandlers.rematchRequest) self._eventHandlers.rematchRequest(data);
|
|
1504
|
+
break;
|
|
1505
|
+
|
|
1506
|
+
case 'GAME_SYNC':
|
|
1507
|
+
if (data.sequence !== undefined) self._lastSequence = data.sequence;
|
|
1508
|
+
if (self._eventHandlers.sync) self._eventHandlers.sync(data);
|
|
1509
|
+
if (self._eventHandlers.stateUpdate) self._eventHandlers.stateUpdate(data);
|
|
1510
|
+
break;
|
|
1515
1511
|
}
|
|
1512
|
+
});
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Usion SDK Game Methods — join, leave, action, realtime, sync, etc.
|
|
1518
|
+
*/
|
|
1519
|
+
|
|
1520
|
+
/**
|
|
1521
|
+
* Add game action methods to game module
|
|
1522
|
+
* @param {object} game - The game module object
|
|
1523
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
1524
|
+
*/
|
|
1525
|
+
function applyGameMethods(game, Usion) {
|
|
1526
|
+
/**
|
|
1527
|
+
* Join a game room
|
|
1528
|
+
* @param {string} roomId - Game room ID (optional, uses config)
|
|
1529
|
+
* @returns {Promise} Resolves with join data
|
|
1530
|
+
*/
|
|
1531
|
+
game.join = function(roomId) {
|
|
1532
|
+
const self = this;
|
|
1533
|
+
roomId = roomId || Usion.config.roomId;
|
|
1516
1534
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
}
|
|
1535
|
+
// If already joined this room, return cached promise/data
|
|
1536
|
+
if (self._joined && self.roomId === roomId && self._joinPromise) {
|
|
1537
|
+
return self._joinPromise;
|
|
1538
|
+
}
|
|
1522
1539
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
if (response.sequence !== undefined) {
|
|
1532
|
-
self._lastSequence = response.sequence;
|
|
1533
|
-
}
|
|
1534
|
-
resolve(response);
|
|
1535
|
-
}
|
|
1536
|
-
});
|
|
1540
|
+
self.roomId = roomId;
|
|
1541
|
+
self.playerId = Usion.user.getId();
|
|
1542
|
+
|
|
1543
|
+
if (self.directMode) {
|
|
1544
|
+
self._joined = true;
|
|
1545
|
+
self._joinPromise = Promise.resolve({
|
|
1546
|
+
room_id: roomId,
|
|
1547
|
+
player_id: self.playerId
|
|
1537
1548
|
});
|
|
1538
|
-
|
|
1549
|
+
return self._joinPromise;
|
|
1550
|
+
}
|
|
1539
1551
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1552
|
+
// Proxy mode: send join request to parent
|
|
1553
|
+
if (self._useProxy) {
|
|
1554
|
+
self._joinPromise = new Promise(function(resolve, reject) {
|
|
1555
|
+
self._proxyJoinResolve = resolve;
|
|
1556
|
+
self._proxyJoinReject = reject;
|
|
1557
|
+
Usion._post({ type: 'GAME_JOIN', room_id: roomId });
|
|
1558
|
+
setTimeout(function() {
|
|
1559
|
+
if (!self._joined && self._proxyJoinReject) {
|
|
1560
|
+
self._proxyJoinReject = null;
|
|
1561
|
+
reject(new Error('Join timeout'));
|
|
1562
|
+
}
|
|
1563
|
+
}, 15000);
|
|
1564
|
+
});
|
|
1565
|
+
return self._joinPromise;
|
|
1566
|
+
}
|
|
1548
1567
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
action_data: actionData || {}
|
|
1553
|
-
});
|
|
1568
|
+
self._joinPromise = new Promise(function(resolve, reject) {
|
|
1569
|
+
if (!self.socket || !self.connected) {
|
|
1570
|
+
reject(new Error('Not connected'));
|
|
1554
1571
|
return;
|
|
1555
1572
|
}
|
|
1556
1573
|
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
1574
|
+
if (!roomId) {
|
|
1575
|
+
reject(new Error('No room ID provided'));
|
|
1559
1576
|
return;
|
|
1560
1577
|
}
|
|
1561
1578
|
|
|
1579
|
+
self.socket.emit('game:join', { room_id: roomId }, function(response) {
|
|
1580
|
+
if (response.error) {
|
|
1581
|
+
self._joined = false;
|
|
1582
|
+
reject(new Error(response.message || response.error));
|
|
1583
|
+
} else {
|
|
1584
|
+
self._joined = true;
|
|
1585
|
+
if (response.sequence !== undefined) {
|
|
1586
|
+
self._lastSequence = response.sequence;
|
|
1587
|
+
}
|
|
1588
|
+
resolve(response);
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
return self._joinPromise;
|
|
1594
|
+
};
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Leave the current game room
|
|
1598
|
+
*/
|
|
1599
|
+
game.leave = function() {
|
|
1600
|
+
const self = this;
|
|
1601
|
+
|
|
1602
|
+
if (self.directMode) {
|
|
1603
|
+
if (self.roomId) self._sendDirect('leave', {});
|
|
1604
|
+
self.roomId = null;
|
|
1605
|
+
self._lastSequence = 0;
|
|
1606
|
+
self._joined = false;
|
|
1607
|
+
self._joinPromise = null;
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (self._useProxy) {
|
|
1612
|
+
if (self.roomId) Usion._post({ type: 'GAME_LEAVE', room_id: self.roomId });
|
|
1613
|
+
self.roomId = null;
|
|
1614
|
+
self._lastSequence = 0;
|
|
1615
|
+
self._joined = false;
|
|
1616
|
+
self._joinPromise = null;
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
if (self.socket && self.connected && self.roomId) {
|
|
1621
|
+
self.socket.emit('game:leave', { room_id: self.roomId });
|
|
1622
|
+
self.roomId = null;
|
|
1623
|
+
self._lastSequence = 0;
|
|
1624
|
+
self._joined = false;
|
|
1625
|
+
self._joinPromise = null;
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* Send a game action
|
|
1631
|
+
* @param {string} actionType - Type of action (e.g., 'move')
|
|
1632
|
+
* @param {object} actionData - Action data
|
|
1633
|
+
* @returns {Promise} Resolves when action is processed
|
|
1634
|
+
*/
|
|
1635
|
+
game.action = function(actionType, actionData) {
|
|
1636
|
+
const self = this;
|
|
1637
|
+
|
|
1638
|
+
if (self.directMode) {
|
|
1639
|
+
self._sendDirect(actionType || 'action', actionData || {});
|
|
1640
|
+
return Promise.resolve({ success: true });
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
if (self._useProxy) {
|
|
1644
|
+
Usion._post({ type: 'GAME_ACTION', room_id: self.roomId, action_type: actionType, action_data: actionData });
|
|
1645
|
+
return Promise.resolve({ success: true });
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
return new Promise(function(resolve, reject) {
|
|
1562
1649
|
if (!self.socket || !self.connected) {
|
|
1650
|
+
reject(new Error('Not connected'));
|
|
1563
1651
|
return;
|
|
1564
1652
|
}
|
|
1565
1653
|
|
|
1566
|
-
self.socket.emit('game:
|
|
1654
|
+
self.socket.emit('game:action', {
|
|
1567
1655
|
room_id: self.roomId,
|
|
1568
1656
|
action_type: actionType,
|
|
1569
1657
|
action_data: actionData
|
|
1658
|
+
}, function(response) {
|
|
1659
|
+
if (response.error) {
|
|
1660
|
+
reject(new Error(response.message || response.error));
|
|
1661
|
+
} else {
|
|
1662
|
+
if (response.sequence !== undefined) {
|
|
1663
|
+
self._lastSequence = response.sequence;
|
|
1664
|
+
}
|
|
1665
|
+
resolve(response);
|
|
1666
|
+
}
|
|
1570
1667
|
});
|
|
1571
|
-
}
|
|
1668
|
+
});
|
|
1669
|
+
};
|
|
1572
1670
|
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1671
|
+
/**
|
|
1672
|
+
* Send a real-time game update (fire-and-forget).
|
|
1673
|
+
* @param {string} actionType - Type of action
|
|
1674
|
+
* @param {object} actionData - Action data
|
|
1675
|
+
*/
|
|
1676
|
+
game.realtime = function(actionType, actionData) {
|
|
1677
|
+
const self = this;
|
|
1580
1678
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
if (self.socket && self.connected && self.roomId) {
|
|
1592
|
-
self.socket.emit('game:sync_request', {
|
|
1593
|
-
room_id: self.roomId,
|
|
1594
|
-
last_sequence: lastSequence || 0
|
|
1595
|
-
});
|
|
1596
|
-
}
|
|
1597
|
-
},
|
|
1679
|
+
if (self.directMode) {
|
|
1680
|
+
self._sendDirect('input', {
|
|
1681
|
+
action_type: actionType,
|
|
1682
|
+
action_data: actionData || {}
|
|
1683
|
+
});
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1598
1686
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
const self = this;
|
|
1687
|
+
if (self._useProxy) {
|
|
1688
|
+
Usion._post({ type: 'GAME_REALTIME', room_id: self.roomId, action_type: actionType, action_data: actionData });
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1604
1691
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
if (self._useProxy && self.roomId) {
|
|
1611
|
-
Usion._post({ type: 'GAME_REMATCH', room_id: self.roomId });
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
if (self.socket && self.connected && self.roomId) {
|
|
1616
|
-
self.socket.emit('game:rematch', { room_id: self.roomId });
|
|
1617
|
-
}
|
|
1618
|
-
},
|
|
1692
|
+
if (!self.socket || !self.connected) {
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1619
1695
|
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1696
|
+
self.socket.emit('game:realtime', {
|
|
1697
|
+
room_id: self.roomId,
|
|
1698
|
+
action_type: actionType,
|
|
1699
|
+
action_data: actionData
|
|
1700
|
+
});
|
|
1701
|
+
};
|
|
1626
1702
|
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1703
|
+
/**
|
|
1704
|
+
* Request game state sync (for reconnection)
|
|
1705
|
+
* @param {number} lastSequence - Last known sequence number
|
|
1706
|
+
*/
|
|
1707
|
+
game.requestSync = function(lastSequence) {
|
|
1708
|
+
const self = this;
|
|
1709
|
+
lastSequence = lastSequence !== undefined ? lastSequence : self._lastSequence;
|
|
1631
1710
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1711
|
+
if (self.directMode) {
|
|
1712
|
+
self._sendDirect('ping', { last_sequence: lastSequence || 0 });
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1636
1715
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
}
|
|
1716
|
+
if (self._useProxy && self.roomId) {
|
|
1717
|
+
Usion._post({ type: 'GAME_SYNC_REQUEST', room_id: self.roomId, last_sequence: lastSequence || 0 });
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1642
1720
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
resolve(response);
|
|
1648
|
-
}
|
|
1649
|
-
});
|
|
1721
|
+
if (self.socket && self.connected && self.roomId) {
|
|
1722
|
+
self.socket.emit('game:sync_request', {
|
|
1723
|
+
room_id: self.roomId,
|
|
1724
|
+
last_sequence: lastSequence || 0
|
|
1650
1725
|
});
|
|
1651
|
-
}
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1652
1728
|
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1729
|
+
/**
|
|
1730
|
+
* Request a rematch
|
|
1731
|
+
*/
|
|
1732
|
+
game.requestRematch = function() {
|
|
1733
|
+
const self = this;
|
|
1658
1734
|
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
}
|
|
1735
|
+
if (self.directMode) {
|
|
1736
|
+
self._sendDirect('rematch', {});
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1664
1739
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
self.directSocket = null;
|
|
1670
|
-
self.connected = false;
|
|
1671
|
-
self.roomId = null;
|
|
1672
|
-
self._lastSequence = 0;
|
|
1673
|
-
self._connecting = false;
|
|
1674
|
-
self._connectPromise = null;
|
|
1675
|
-
self._joined = false;
|
|
1676
|
-
self._joinPromise = null;
|
|
1677
|
-
self.directMode = false;
|
|
1678
|
-
self.directConfig = null;
|
|
1679
|
-
self._directSeq = 0;
|
|
1680
|
-
return;
|
|
1681
|
-
}
|
|
1740
|
+
if (self._useProxy && self.roomId) {
|
|
1741
|
+
Usion._post({ type: 'GAME_REMATCH', room_id: self.roomId });
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1682
1744
|
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1745
|
+
if (self.socket && self.connected && self.roomId) {
|
|
1746
|
+
self.socket.emit('game:rematch', { room_id: self.roomId });
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* Forfeit the current game
|
|
1752
|
+
* @returns {Promise}
|
|
1753
|
+
*/
|
|
1754
|
+
game.forfeit = function() {
|
|
1755
|
+
const self = this;
|
|
1756
|
+
|
|
1757
|
+
if (self.directMode) {
|
|
1758
|
+
self._sendDirect('forfeit', {});
|
|
1759
|
+
return Promise.resolve({ success: true });
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
if (self._useProxy) {
|
|
1763
|
+
Usion._post({ type: 'GAME_FORFEIT', room_id: self.roomId });
|
|
1764
|
+
return Promise.resolve({ success: true });
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
return new Promise(function(resolve, reject) {
|
|
1768
|
+
if (!self.socket || !self.connected) {
|
|
1769
|
+
reject(new Error('Not connected'));
|
|
1693
1770
|
return;
|
|
1694
1771
|
}
|
|
1695
1772
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
self._joinPromise = null;
|
|
1706
|
-
}
|
|
1707
|
-
},
|
|
1773
|
+
self.socket.emit('game:forfeit', { room_id: self.roomId }, function(response) {
|
|
1774
|
+
if (response.error) {
|
|
1775
|
+
reject(new Error(response.message || response.error));
|
|
1776
|
+
} else {
|
|
1777
|
+
resolve(response);
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
});
|
|
1781
|
+
};
|
|
1708
1782
|
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1783
|
+
/**
|
|
1784
|
+
* Disconnect from the game socket
|
|
1785
|
+
*/
|
|
1786
|
+
game.disconnect = function() {
|
|
1787
|
+
const self = this;
|
|
1788
|
+
|
|
1789
|
+
// Always clear heartbeat
|
|
1790
|
+
if (self._heartbeatInterval) {
|
|
1791
|
+
clearInterval(self._heartbeatInterval);
|
|
1792
|
+
self._heartbeatInterval = null;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
if (self.directMode) {
|
|
1796
|
+
if (self.directSocket) {
|
|
1797
|
+
try { self.directSocket.close(); } catch (e) {}
|
|
1716
1798
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1799
|
+
self.directSocket = null;
|
|
1800
|
+
self.connected = false;
|
|
1801
|
+
self.roomId = null;
|
|
1802
|
+
self._lastSequence = 0;
|
|
1803
|
+
self._connecting = false;
|
|
1804
|
+
self._connectPromise = null;
|
|
1805
|
+
self._joined = false;
|
|
1806
|
+
self._joinPromise = null;
|
|
1807
|
+
self.directMode = false;
|
|
1808
|
+
self.directConfig = null;
|
|
1809
|
+
self._directSeq = 0;
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1719
1812
|
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1813
|
+
if (self._useProxy) {
|
|
1814
|
+
Usion._post({ type: 'GAME_DISCONNECT' });
|
|
1815
|
+
self.connected = false;
|
|
1816
|
+
self.roomId = null;
|
|
1817
|
+
self._lastSequence = 0;
|
|
1818
|
+
self._connecting = false;
|
|
1819
|
+
self._connectPromise = null;
|
|
1820
|
+
self._joined = false;
|
|
1821
|
+
self._joinPromise = null;
|
|
1822
|
+
self._useProxy = false;
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1724
1825
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1826
|
+
if (self.socket) {
|
|
1827
|
+
self.socket.disconnect();
|
|
1828
|
+
self.socket = null;
|
|
1829
|
+
self.connected = false;
|
|
1830
|
+
self.roomId = null;
|
|
1831
|
+
self._lastSequence = 0;
|
|
1832
|
+
self._connecting = false;
|
|
1833
|
+
self._connectPromise = null;
|
|
1834
|
+
self._joined = false;
|
|
1835
|
+
self._joinPromise = null;
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1728
1838
|
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1839
|
+
/**
|
|
1840
|
+
* Get connection status
|
|
1841
|
+
* @returns {boolean}
|
|
1842
|
+
*/
|
|
1843
|
+
game.isConnected = function() {
|
|
1844
|
+
if (this.directMode) {
|
|
1845
|
+
return this.connected && this.directSocket && this.directSocket.readyState === WebSocket.OPEN;
|
|
1846
|
+
}
|
|
1847
|
+
return this.connected && this.socket && this.socket.connected;
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1732
1850
|
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1851
|
+
/**
|
|
1852
|
+
* Usion SDK Game Core — game module base, connect routing, event registrations
|
|
1853
|
+
*/
|
|
1736
1854
|
|
|
1737
|
-
onSync: function(callback) {
|
|
1738
|
-
this._eventHandlers.sync = callback;
|
|
1739
|
-
},
|
|
1740
1855
|
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1856
|
+
/**
|
|
1857
|
+
* Create the game module with all sub-modules applied
|
|
1858
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
1859
|
+
*/
|
|
1860
|
+
function createGameModule(Usion) {
|
|
1861
|
+
const game = {
|
|
1862
|
+
socket: null,
|
|
1863
|
+
directSocket: null,
|
|
1864
|
+
roomId: null,
|
|
1865
|
+
playerId: null,
|
|
1866
|
+
connected: false,
|
|
1867
|
+
directMode: false,
|
|
1868
|
+
directConfig: null,
|
|
1869
|
+
_directSeq: 0,
|
|
1870
|
+
_eventHandlers: {},
|
|
1871
|
+
_lastSequence: 0,
|
|
1872
|
+
_connecting: false,
|
|
1873
|
+
_connectPromise: null,
|
|
1874
|
+
_joined: false,
|
|
1875
|
+
_joinPromise: null,
|
|
1876
|
+
_useProxy: false,
|
|
1877
|
+
_proxyListenerSetup: false,
|
|
1878
|
+
_heartbeatInterval: null,
|
|
1744
1879
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1880
|
+
/**
|
|
1881
|
+
* Connect to the game socket server
|
|
1882
|
+
* @param {string} socketUrl - Socket.IO server URL (optional, uses config)
|
|
1883
|
+
* @param {string} token - JWT auth token (optional, uses user.getToken())
|
|
1884
|
+
* @returns {Promise} Resolves when connected
|
|
1885
|
+
*/
|
|
1886
|
+
connect: function(socketUrl, token) {
|
|
1887
|
+
const self = this;
|
|
1888
|
+
var connectionMode = (Usion.config && Usion.config.connectionMode) || 'platform';
|
|
1889
|
+
if (connectionMode === 'direct') {
|
|
1890
|
+
return self.connectDirect();
|
|
1891
|
+
}
|
|
1748
1892
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1893
|
+
// Use config values as defaults
|
|
1894
|
+
socketUrl = socketUrl || Usion.config.socketUrl;
|
|
1895
|
+
token = token || Usion.user.getToken();
|
|
1752
1896
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1897
|
+
if (!socketUrl) {
|
|
1898
|
+
return Promise.reject(new Error('No socket URL provided'));
|
|
1899
|
+
}
|
|
1900
|
+
if (!token) {
|
|
1901
|
+
return Promise.reject(new Error('No auth token available'));
|
|
1902
|
+
}
|
|
1756
1903
|
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1904
|
+
// If already connected (direct or proxy), return immediately
|
|
1905
|
+
if (self._useProxy && self.connected) {
|
|
1906
|
+
return Promise.resolve();
|
|
1907
|
+
}
|
|
1908
|
+
if (self.socket && self.connected) {
|
|
1909
|
+
return Promise.resolve();
|
|
1910
|
+
}
|
|
1760
1911
|
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1912
|
+
// If currently connecting, return the existing promise
|
|
1913
|
+
if (self._connecting && self._connectPromise) {
|
|
1914
|
+
return self._connectPromise;
|
|
1915
|
+
}
|
|
1764
1916
|
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1917
|
+
// When running inside an iframe or WebView, use parent as socket proxy
|
|
1918
|
+
var isInFrame = !!window.__USION_PROXY__
|
|
1919
|
+
|| window.parent !== window
|
|
1920
|
+
|| !!window.ReactNativeWebView
|
|
1921
|
+
|| !!Usion._isEmbedded;
|
|
1768
1922
|
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1923
|
+
if (isInFrame) {
|
|
1924
|
+
Usion.log('Running in iframe \u2013 using parent app as socket proxy');
|
|
1925
|
+
return self._connectViaProxy();
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
self._connecting = true;
|
|
1929
|
+
self._connectPromise = new Promise(function(resolve, reject) {
|
|
1930
|
+
// Check if socket.io-client is available
|
|
1931
|
+
if (typeof io === 'undefined') {
|
|
1932
|
+
// Load socket.io client
|
|
1933
|
+
var script = document.createElement('script');
|
|
1934
|
+
script.src = '/socket.io.min.js';
|
|
1935
|
+
script.onload = function() {
|
|
1936
|
+
self._initSocket(socketUrl, token, resolve, reject);
|
|
1937
|
+
};
|
|
1938
|
+
script.onerror = function() {
|
|
1939
|
+
// Local file not available, try CDN as fallback
|
|
1940
|
+
var cdnScript = document.createElement('script');
|
|
1941
|
+
cdnScript.src = 'https://cdn.socket.io/4.7.2/socket.io.min.js';
|
|
1942
|
+
cdnScript.onload = function() {
|
|
1943
|
+
self._initSocket(socketUrl, token, resolve, reject);
|
|
1944
|
+
};
|
|
1945
|
+
cdnScript.onerror = function() {
|
|
1946
|
+
self._connecting = false;
|
|
1947
|
+
reject(new Error('Failed to load Socket.IO client'));
|
|
1948
|
+
};
|
|
1949
|
+
document.head.appendChild(cdnScript);
|
|
1950
|
+
};
|
|
1951
|
+
document.head.appendChild(script);
|
|
1952
|
+
} else {
|
|
1953
|
+
self._initSocket(socketUrl, token, resolve, reject);
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1772
1956
|
|
|
1773
|
-
|
|
1774
|
-
this._eventHandlers.connectionError = callback;
|
|
1957
|
+
return self._connectPromise;
|
|
1775
1958
|
},
|
|
1776
1959
|
|
|
1960
|
+
// Event handler registrations
|
|
1961
|
+
onJoined: function(callback) { this._eventHandlers.joined = callback; },
|
|
1962
|
+
onPlayerJoined: function(callback) { this._eventHandlers.playerJoined = callback; },
|
|
1963
|
+
onPlayerLeft: function(callback) { this._eventHandlers.playerLeft = callback; },
|
|
1964
|
+
onStateUpdate: function(callback) { this._eventHandlers.stateUpdate = callback; },
|
|
1965
|
+
onSync: function(callback) { this._eventHandlers.sync = callback; },
|
|
1966
|
+
onAction: function(callback) { this._eventHandlers.action = callback; },
|
|
1967
|
+
onRealtime: function(callback) { this._eventHandlers.realtime = callback; },
|
|
1968
|
+
onGameFinished: function(callback) { this._eventHandlers.finished = callback; },
|
|
1969
|
+
onGameRestarted: function(callback) { this._eventHandlers.restarted = callback; },
|
|
1970
|
+
onError: function(callback) { this._eventHandlers.error = callback; },
|
|
1971
|
+
onRematchRequest: function(callback) { this._eventHandlers.rematchRequest = callback; },
|
|
1972
|
+
onDisconnect: function(callback) { this._eventHandlers.disconnect = callback; },
|
|
1973
|
+
onReconnect: function(callback) { this._eventHandlers.reconnect = callback; },
|
|
1974
|
+
onConnectionError: function(callback) { this._eventHandlers.connectionError = callback; },
|
|
1975
|
+
|
|
1777
1976
|
/**
|
|
1778
1977
|
* Register a generic event handler
|
|
1779
1978
|
* @param {string} event - Event name
|
|
@@ -1784,10 +1983,64 @@
|
|
|
1784
1983
|
this.socket.on(event, callback);
|
|
1785
1984
|
}
|
|
1786
1985
|
}
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1988
|
+
// Apply sub-modules
|
|
1989
|
+
applyGameDirect(game, Usion);
|
|
1990
|
+
applyGameSocket(game, Usion);
|
|
1991
|
+
applyGameProxy(game, Usion);
|
|
1992
|
+
applyGameMethods(game, Usion);
|
|
1993
|
+
|
|
1994
|
+
return game;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
/**
|
|
1998
|
+
* Usion Mini App SDK v2.1
|
|
1999
|
+
*
|
|
2000
|
+
* JavaScript utilities for Mini Apps (Iframe Games & Services)
|
|
2001
|
+
* Import via: <script src="https://usions.com/usion-sdk.js"></script>
|
|
2002
|
+
*
|
|
2003
|
+
* Features:
|
|
2004
|
+
* - User info and authentication
|
|
2005
|
+
* - Persistent storage (per-user, per-service)
|
|
2006
|
+
* - Wallet/payment integration
|
|
2007
|
+
* - Session management
|
|
2008
|
+
* - Real-time game support via Socket.IO
|
|
2009
|
+
*/
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
// Build the Usion object from core
|
|
2013
|
+
const Usion = Object.assign({}, core);
|
|
2014
|
+
|
|
2015
|
+
// Attach sub-modules (these reference Usion internally)
|
|
2016
|
+
Usion.user = createUserModule(Usion);
|
|
2017
|
+
Usion.storage = createStorageModule(Usion);
|
|
2018
|
+
Usion.wallet = createWalletModule(Usion);
|
|
2019
|
+
Usion.session = createSessionModule(Usion);
|
|
2020
|
+
Usion.chat = createChatModule(Usion);
|
|
2021
|
+
Usion.bot = createBotModule(Usion);
|
|
2022
|
+
Usion.fileStorage = createFileStorageModule(Usion);
|
|
2023
|
+
Usion.game = createGameModule(Usion);
|
|
2024
|
+
|
|
2025
|
+
// Attach results methods directly on Usion
|
|
2026
|
+
Object.assign(Usion, createResultsMethods(Usion));
|
|
2027
|
+
|
|
2028
|
+
// Attach back button methods directly on Usion
|
|
2029
|
+
Object.assign(Usion, createBackButtonMethods(Usion));
|
|
2030
|
+
|
|
2031
|
+
// Attach UI utilities directly on Usion
|
|
2032
|
+
Object.assign(Usion, uiMethods);
|
|
2033
|
+
|
|
2034
|
+
// Attach misc methods (submit, error, exit, share, log, on, requestPayment)
|
|
2035
|
+
Object.assign(Usion, miscMethods);
|
|
2036
|
+
|
|
2037
|
+
return Usion;
|
|
2038
|
+
|
|
2039
|
+
})();
|
|
2040
|
+
|
|
2041
|
+
// Expose to global
|
|
2042
|
+
if (typeof window !== 'undefined') {
|
|
2043
|
+
window.Usion = Usion;
|
|
2044
|
+
} else if (typeof globalThis !== 'undefined') {
|
|
2045
|
+
globalThis.Usion = Usion;
|
|
2046
|
+
}
|