@usions/sdk 2.0.1 → 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/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
- const Usion = {
23
- version: '2.0.1',
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 we are embedded (iframe or WebView)
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 = ++_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
- // User Module
181
- // ============================================
212
+ /**
213
+ * Usion SDK User Module — user info and authentication
214
+ */
182
215
 
183
- /**
184
- * User information and authentication
185
- */
186
- user: {
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
- // Storage Module
241
- // ============================================
242
-
243
- /**
244
- * Persistent storage (per-user, per-service)
245
- */
246
- storage: {
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
- // Wallet Module
304
- // ============================================
339
+ /**
340
+ * Usion SDK Wallet Module — wallet and payment operations
341
+ */
305
342
 
306
- /**
307
- * Wallet and payment operations
308
- */
309
- wallet: {
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 = ++_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
- // Session Module
415
- // ============================================
416
-
417
- /**
418
- * Session management (ephemeral data for current session)
419
- */
420
- session: {
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
- // Legacy Payment Method (for backwards compatibility)
476
- // ============================================
477
-
478
- /**
479
- * Request payment from user (legacy method)
480
- * @deprecated Use Usion.wallet.requestPayment instead
481
- */
482
- requestPayment: function(amount, reason, data) {
483
- return this.wallet.requestPayment(amount, reason, data);
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
- * Log message to native console (for debugging)
620
- * @param {string} msg - Message to log
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
- * Listen for messages from parent app
632
- * @param {string} type - Message type to listen for
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
- if (data.type === type) {
645
- callback(data);
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
- // UI Utilities
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
- // Game/Multiplayer Utilities
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
- * Game module for multiplayer real-time games
804
+ * Report an error to parent app
805
+ * @param {string} message - Error message
766
806
  */
767
- game: {
768
- socket: null,
769
- directSocket: null,
770
- roomId: null,
771
- playerId: null,
772
- connected: false,
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
- * Connect to the game socket server
788
- * @param {string} socketUrl - Socket.IO server URL (optional, uses config)
789
- * @param {string} token - JWT auth token (optional, uses user.getToken())
790
- * @returns {Promise} Resolves when connected
791
- */
792
- connect: function(socketUrl, token) {
793
- const self = this;
794
- var connectionMode = (Usion.config && Usion.config.connectionMode) || 'platform';
795
- if (connectionMode === 'direct') {
796
- return self.connectDirect();
797
- }
798
-
799
- // Use config values as defaults
800
- socketUrl = socketUrl || Usion.config.socketUrl;
801
- token = token || Usion.user.getToken();
802
-
803
- if (!socketUrl) {
804
- return Promise.reject(new Error('No socket URL provided'));
805
- }
806
- if (!token) {
807
- return Promise.reject(new Error('No auth token available'));
808
- }
809
-
810
- // If already connected (direct or proxy), return immediately
811
- if (self._useProxy && self.connected) {
812
- return Promise.resolve();
813
- }
814
- if (self.socket && self.connected) {
815
- return Promise.resolve();
816
- }
817
-
818
- // If currently connecting, return the existing promise
819
- if (self._connecting && self._connectPromise) {
820
- return self._connectPromise;
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
- // When running inside an iframe or WebView, always use the parent app
824
- // as a socket proxy. The parent already has an authenticated socket
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
- self._connecting = true;
842
- self._connectPromise = new Promise(function(resolve, reject) {
843
- // Check if socket.io-client is available
844
- if (typeof io === 'undefined') {
845
- // Load socket.io client try local copy first, fallback to CDN
846
- var script = document.createElement('script');
847
- script.src = '/socket.io.min.js';
848
- script.onload = function() {
849
- self._initSocket(socketUrl, token, resolve, reject);
850
- };
851
- script.onerror = function() {
852
- // Local file not available, try CDN as fallback
853
- var cdnScript = document.createElement('script');
854
- cdnScript.src = 'https://cdn.socket.io/4.7.2/socket.io.min.js';
855
- cdnScript.onload = function() {
856
- self._initSocket(socketUrl, token, resolve, reject);
857
- };
858
- cdnScript.onerror = function() {
859
- self._connecting = false;
860
- reject(new Error('Failed to load Socket.IO client'));
861
- };
862
- document.head.appendChild(cdnScript);
863
- };
864
- document.head.appendChild(script);
865
- } else {
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
- * Connect directly to creator-controlled WebSocket server.
875
- * Uses backend-issued short-lived room token.
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
- connectDirect: function(config) {
879
- var self = this;
880
- config = config || {};
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
- if (self.directMode && self.directSocket && self.connected) {
883
- return Promise.resolve();
884
- }
885
- if (self._connecting && self._connectPromise) {
886
- return self._connectPromise;
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
- self._connecting = true;
890
- self.directMode = true;
891
- self._connectPromise = self._fetchDirectAccess(config)
892
- .then(function(access) {
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
- _fetchDirectAccess: function(config) {
914
- var roomId = config.roomId || this.roomId || Usion.config.roomId;
915
- var serviceId = config.serviceId || Usion.config.serviceId;
916
- var apiUrl = config.apiUrl || Usion.config.apiUrl || '';
917
- var token = config.token || Usion.user.getToken();
918
-
919
- if (!roomId) return Promise.reject(new Error('No room ID provided'));
920
- if (!serviceId) return Promise.reject(new Error('No service ID provided'));
921
- if (!apiUrl) return Promise.reject(new Error('No API URL provided'));
922
- if (!token) return Promise.reject(new Error('No auth token available'));
923
-
924
- this.roomId = roomId;
925
- this.playerId = Usion.user.getId();
926
-
927
- var cleanApiUrl = String(apiUrl).replace(/\/$/, '');
928
- var endpoint = cleanApiUrl + '/games/rooms/' + encodeURIComponent(roomId) + '/access';
929
- return fetch(endpoint, {
930
- method: 'POST',
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
- return res.json();
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
- _initDirectSocket: function(access) {
951
- var self = this;
952
- return new Promise(function(resolve, reject) {
953
- if (!access || !access.ws_url || !access.access_token) {
954
- reject(new Error('Invalid direct access payload'));
955
- return;
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
- var wsUrl = access.ws_url;
959
- var separator = wsUrl.indexOf('?') === -1 ? '?' : '&';
960
- var urlWithToken = wsUrl + separator + 'token=' + encodeURIComponent(access.access_token);
961
- var ws = new WebSocket(urlWithToken);
962
- self.directSocket = ws;
963
-
964
- var opened = false;
965
- var joinSent = false;
966
- var timeout = setTimeout(function() {
967
- if (!opened) {
968
- try { ws.close(); } catch (e) {}
969
- reject(new Error('Direct WebSocket connection timeout'));
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
- }, 10000);
1084
+ }, 25000);
1085
+ resolve();
1086
+ };
972
1087
 
973
- ws.onopen = function() {
974
- opened = true;
1088
+ ws.onerror = function() {
1089
+ if (!opened) {
975
1090
  clearTimeout(timeout);
976
- if (!joinSent) {
977
- joinSent = true;
978
- self._sendDirect('join', {});
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
- ws.onerror = function() {
991
- if (!opened) {
992
- clearTimeout(timeout);
993
- reject(new Error('Direct WebSocket connection error'));
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
- ws.onclose = function(evt) {
998
- self.connected = false;
999
- self._joined = false;
1000
- self._joinPromise = null;
1001
- if (self._heartbeatInterval) {
1002
- clearInterval(self._heartbeatInterval);
1003
- self._heartbeatInterval = null;
1004
- }
1005
- if (self._eventHandlers.disconnect) {
1006
- self._eventHandlers.disconnect(evt && evt.reason ? evt.reason : 'direct socket closed');
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
- ws.onmessage = function(evt) {
1011
- self._handleDirectMessage(evt && evt.data);
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
- _sendDirect: function(type, payload) {
1017
- if (!this.directSocket || this.directSocket.readyState !== WebSocket.OPEN) return;
1018
- this._directSeq = this._directSeq + 1;
1019
- this.directSocket.send(JSON.stringify({
1020
- type: type,
1021
- room_id: this.roomId,
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
- _handleDirectMessage: function(raw) {
1031
- var data;
1032
- try {
1033
- data = typeof raw === 'string' ? JSON.parse(raw) : raw;
1034
- } catch (e) {
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
- if (data.type === 'joined') {
1041
- this._joined = true;
1042
- if (this._eventHandlers.joined) this._eventHandlers.joined(payload);
1043
- return;
1044
- }
1045
- if (data.type === 'player_joined') {
1046
- if (this._eventHandlers.playerJoined) this._eventHandlers.playerJoined(payload);
1047
- return;
1048
- }
1049
- if (data.type === 'player_left') {
1050
- if (this._eventHandlers.playerLeft) this._eventHandlers.playerLeft(payload);
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
- * Initialize socket connection
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
- resolve();
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
- self.socket.on('connect_error', function(err) {
1134
- self._connecting = false;
1135
- Usion.log('Game socket error: ' + err.message);
1136
- if (self._eventHandlers.connectionError) {
1137
- self._eventHandlers.connectionError(err);
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
- reject(err);
1140
- });
1223
+ }, 25000);
1141
1224
 
1142
- self.socket.on('disconnect', function(reason) {
1143
- self.connected = false;
1225
+ // Re-join room after reconnect
1226
+ if (self.roomId) {
1144
1227
  self._joined = false;
1145
1228
  self._joinPromise = null;
1146
- if (self._heartbeatInterval) {
1147
- clearInterval(self._heartbeatInterval);
1148
- self._heartbeatInterval = null;
1149
- }
1150
- Usion.log('Game socket disconnected: ' + reason);
1151
- if (self._eventHandlers.disconnect) {
1152
- self._eventHandlers.disconnect(reason);
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
- // Game event handlers
1164
- self.socket.on('game:joined', function(data) {
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
- self.socket.on('game:player_joined', function(data) {
1174
- if (self._eventHandlers.playerJoined) {
1175
- self._eventHandlers.playerJoined(data);
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
- self.socket.on('game:player_left', function(data) {
1180
- if (self._eventHandlers.playerLeft) {
1181
- self._eventHandlers.playerLeft(data);
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
- self.socket.on('game:state', function(data) {
1186
- if (data.sequence !== undefined) {
1187
- self._lastSequence = Math.max(self._lastSequence, data.sequence);
1188
- }
1189
- if (self._eventHandlers.stateUpdate) {
1190
- self._eventHandlers.stateUpdate(data);
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
- self.socket.on('game:sync', function(data) {
1195
- if (data.sequence !== undefined) {
1196
- self._lastSequence = data.sequence;
1197
- }
1198
- if (self._eventHandlers.sync) {
1199
- self._eventHandlers.sync(data);
1200
- }
1201
- // Also trigger stateUpdate for backwards compat
1202
- if (self._eventHandlers.stateUpdate) {
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
- self.socket.on('game:action', function(data) {
1208
- if (data.sequence !== undefined) {
1209
- self._lastSequence = Math.max(self._lastSequence, data.sequence);
1210
- }
1211
- if (self._eventHandlers.action) {
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
- self.socket.on('game:realtime', function(data) {
1217
- if (self._eventHandlers.realtime) {
1218
- self._eventHandlers.realtime(data);
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
- self.socket.on('game:finished', function(data) {
1223
- if (data.sequence !== undefined) {
1224
- self._lastSequence = data.sequence;
1225
- }
1226
- if (self._eventHandlers.finished) {
1227
- self._eventHandlers.finished(data);
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
- self.socket.on('game:error', function(data) {
1232
- Usion.log('Game error: ' + (data.message || data.code));
1233
- if (self._eventHandlers.error) {
1234
- self._eventHandlers.error(data);
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
- self.socket.on('game:rematch_request', function(data) {
1239
- if (self._eventHandlers.rematchRequest) {
1240
- self._eventHandlers.rematchRequest(data);
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
- self.socket.on('game:restarted', function(data) {
1245
- self._lastSequence = 0; // Reset sequence on rematch
1246
- if (self._eventHandlers.restarted) {
1247
- self._eventHandlers.restarted(data);
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
- } catch (err) {
1252
- reject(err);
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
- * Connect via parent app proxy (postMessage relay).
1258
- * Used when mixed content prevents direct socket connection.
1259
- * @private
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
- * Set up message listener for proxy game events from parent.
1298
- * @private
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
- if (!data || typeof data !== 'object' || !self._useProxy) return;
1313
-
1314
- switch (data.type) {
1315
- case 'GAME_CONNECTED':
1316
- if (self._proxyConnectResolve) {
1317
- self._proxyConnectResolve();
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
- * Join a game room
1397
- * @param {string} roomId - Game room ID (optional, uses config)
1398
- * @returns {Promise} Resolves with join data
1399
- */
1400
- join: function(roomId) {
1401
- const self = this;
1402
- roomId = roomId || Usion.config.roomId;
1403
-
1404
- // If already joined this room, return cached promise/data
1405
- if (self._joined && self.roomId === roomId && self._joinPromise) {
1406
- return self._joinPromise;
1407
- }
1408
-
1409
- self.roomId = roomId;
1410
- self.playerId = Usion.user.getId();
1411
-
1412
- if (self.directMode) {
1413
- self._joined = true;
1414
- self._joinPromise = Promise.resolve({
1415
- room_id: roomId,
1416
- player_id: self.playerId
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
- // Proxy mode: send join request to parent
1422
- if (self._useProxy) {
1423
- self._joinPromise = new Promise(function(resolve, reject) {
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
- self._joinPromise = new Promise(function(resolve, reject) {
1438
- if (!self.socket || !self.connected) {
1439
- reject(new Error('Not connected'));
1440
- return;
1441
- }
1388
+ self._useProxy = true;
1389
+ self._connecting = true;
1390
+ self._setupProxyListener();
1442
1391
 
1443
- if (!roomId) {
1444
- reject(new Error('No room ID provided'));
1445
- return;
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
- self.socket.emit('game:join', { room_id: roomId }, function(response) {
1449
- if (response.error) {
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
- * Leave the current game room
1467
- */
1468
- leave: function() {
1469
- const self = this;
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
- if (self.directMode) {
1472
- if (self.roomId) self._sendDirect('leave', {});
1473
- self.roomId = null;
1474
- self._lastSequence = 0;
1475
- self._joined = false;
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
- * Send a game action
1500
- * @param {string} actionType - Type of action (e.g., 'move')
1501
- * @param {object} actionData - Action data
1502
- * @returns {Promise} Resolves when action is processed
1503
- */
1504
- action: function(actionType, actionData) {
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
- if (self.directMode) {
1508
- self._sendDirect(actionType || 'action', actionData || {});
1509
- return Promise.resolve({ success: true });
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
- if (self._useProxy) {
1513
- Usion._post({ type: 'GAME_ACTION', room_id: self.roomId, action_type: actionType, action_data: actionData });
1514
- return Promise.resolve({ success: true });
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
- return new Promise(function(resolve, reject) {
1518
- if (!self.socket || !self.connected) {
1519
- reject(new Error('Not connected'));
1520
- return;
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
- self.socket.emit('game:action', {
1524
- room_id: self.roomId,
1525
- action_type: actionType,
1526
- action_data: actionData
1527
- }, function(response) {
1528
- if (response.error) {
1529
- reject(new Error(response.message || response.error));
1530
- } else {
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
- * Send a real-time game update (no locking, no rate limiting, fire-and-forget).
1542
- * Use this for high-frequency updates like position, input, state broadcasts.
1543
- * @param {string} actionType - Type of action (e.g., 'snake_input', 'position')
1544
- * @param {object} actionData - Action data
1545
- */
1546
- realtime: function(actionType, actionData) {
1547
- const self = this;
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
- if (self.directMode) {
1550
- self._sendDirect('input', {
1551
- action_type: actionType,
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 (self._useProxy) {
1558
- Usion._post({ type: 'GAME_REALTIME', room_id: self.roomId, action_type: actionType, action_data: actionData });
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:realtime', {
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
- * Request game state sync (for reconnection)
1575
- * @param {number} lastSequence - Last known sequence number
1576
- */
1577
- requestSync: function(lastSequence) {
1578
- const self = this;
1579
- lastSequence = lastSequence !== undefined ? lastSequence : self._lastSequence;
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
- if (self.directMode) {
1582
- self._sendDirect('ping', { last_sequence: lastSequence || 0 });
1583
- return;
1584
- }
1585
-
1586
- if (self._useProxy && self.roomId) {
1587
- Usion._post({ type: 'GAME_SYNC_REQUEST', room_id: self.roomId, last_sequence: lastSequence || 0 });
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
- * Request a rematch
1601
- */
1602
- requestRematch: function() {
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
- if (self.directMode) {
1606
- self._sendDirect('rematch', {});
1607
- return;
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
- * Forfeit the current game
1622
- * @returns {Promise}
1623
- */
1624
- forfeit: function() {
1625
- const self = this;
1696
+ self.socket.emit('game:realtime', {
1697
+ room_id: self.roomId,
1698
+ action_type: actionType,
1699
+ action_data: actionData
1700
+ });
1701
+ };
1626
1702
 
1627
- if (self.directMode) {
1628
- self._sendDirect('forfeit', {});
1629
- return Promise.resolve({ success: true });
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
- if (self._useProxy) {
1633
- Usion._post({ type: 'GAME_FORFEIT', room_id: self.roomId });
1634
- return Promise.resolve({ success: true });
1635
- }
1711
+ if (self.directMode) {
1712
+ self._sendDirect('ping', { last_sequence: lastSequence || 0 });
1713
+ return;
1714
+ }
1636
1715
 
1637
- return new Promise(function(resolve, reject) {
1638
- if (!self.socket || !self.connected) {
1639
- reject(new Error('Not connected'));
1640
- return;
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
- self.socket.emit('game:forfeit', { room_id: self.roomId }, function(response) {
1644
- if (response.error) {
1645
- reject(new Error(response.message || response.error));
1646
- } else {
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
- * Disconnect from the game socket
1655
- */
1656
- disconnect: function() {
1657
- const self = this;
1729
+ /**
1730
+ * Request a rematch
1731
+ */
1732
+ game.requestRematch = function() {
1733
+ const self = this;
1658
1734
 
1659
- // Always clear heartbeat
1660
- if (self._heartbeatInterval) {
1661
- clearInterval(self._heartbeatInterval);
1662
- self._heartbeatInterval = null;
1663
- }
1735
+ if (self.directMode) {
1736
+ self._sendDirect('rematch', {});
1737
+ return;
1738
+ }
1664
1739
 
1665
- if (self.directMode) {
1666
- if (self.directSocket) {
1667
- try { self.directSocket.close(); } catch (e) {}
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
- if (self._useProxy) {
1684
- Usion._post({ type: 'GAME_DISCONNECT' });
1685
- self.connected = false;
1686
- self.roomId = null;
1687
- self._lastSequence = 0;
1688
- self._connecting = false;
1689
- self._connectPromise = null;
1690
- self._joined = false;
1691
- self._joinPromise = null;
1692
- self._useProxy = false;
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
- if (self.socket) {
1697
- self.socket.disconnect();
1698
- self.socket = null;
1699
- self.connected = false;
1700
- self.roomId = null;
1701
- self._lastSequence = 0;
1702
- self._connecting = false;
1703
- self._connectPromise = null;
1704
- self._joined = false;
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
- * Get connection status
1711
- * @returns {boolean}
1712
- */
1713
- isConnected: function() {
1714
- if (this.directMode) {
1715
- return this.connected && this.directSocket && this.directSocket.readyState === WebSocket.OPEN;
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
- return this.connected && this.socket && this.socket.connected;
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
- // Event handler registrations
1721
- onJoined: function(callback) {
1722
- this._eventHandlers.joined = callback;
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
- onPlayerJoined: function(callback) {
1726
- this._eventHandlers.playerJoined = callback;
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
- onPlayerLeft: function(callback) {
1730
- this._eventHandlers.playerLeft = callback;
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
- onStateUpdate: function(callback) {
1734
- this._eventHandlers.stateUpdate = callback;
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
- onAction: function(callback) {
1742
- this._eventHandlers.action = callback;
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
- onRealtime: function(callback) {
1746
- this._eventHandlers.realtime = callback;
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
- onGameFinished: function(callback) {
1750
- this._eventHandlers.finished = callback;
1751
- },
1893
+ // Use config values as defaults
1894
+ socketUrl = socketUrl || Usion.config.socketUrl;
1895
+ token = token || Usion.user.getToken();
1752
1896
 
1753
- onGameRestarted: function(callback) {
1754
- this._eventHandlers.restarted = callback;
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
- onError: function(callback) {
1758
- this._eventHandlers.error = callback;
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
- onRematchRequest: function(callback) {
1762
- this._eventHandlers.rematchRequest = callback;
1763
- },
1912
+ // If currently connecting, return the existing promise
1913
+ if (self._connecting && self._connectPromise) {
1914
+ return self._connectPromise;
1915
+ }
1764
1916
 
1765
- onDisconnect: function(callback) {
1766
- this._eventHandlers.disconnect = callback;
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
- onReconnect: function(callback) {
1770
- this._eventHandlers.reconnect = callback;
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
- onConnectionError: function(callback) {
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
- // Expose to global
1791
- global.Usion = Usion;
1792
-
1793
- })(typeof window !== 'undefined' ? window : this);
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
+ }