@usions/sdk 2.13.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usions/sdk",
3
- "version": "2.13.0",
3
+ "version": "2.16.0",
4
4
  "description": "Usion Mini App SDK for iframe games and services",
5
5
  "type": "module",
6
6
  "main": "src/modules/index.js",
@@ -60,4 +60,4 @@
60
60
  "publishConfig": {
61
61
  "access": "public"
62
62
  }
63
- }
63
+ }
package/src/browser.js CHANGED
@@ -69,7 +69,7 @@ var Usion = (function () {
69
69
  * Core Usion object with init, _post, _request
70
70
  */
71
71
  const core = {
72
- version: '2.13.0', // injected from package.json at build
72
+ version: '2.16.0', // injected from package.json at build
73
73
  config: {},
74
74
  _initialized: false,
75
75
  _initCallback: null,
@@ -192,10 +192,56 @@ var Usion = (function () {
192
192
  }
193
193
  });
194
194
 
195
+ // Report the user's first real interaction to the host (see below).
196
+ this._setupInteractionBeacon();
197
+
195
198
  // Signal ready to parent
196
199
  this._post({ type: 'READY' });
197
200
  },
198
201
 
202
+ /**
203
+ * Tell the host the moment the user FIRST genuinely interacts with the
204
+ * mini-app — a tap, click, key press, or touch — and only once.
205
+ *
206
+ * The host uses this as the SOLE signal to surface the mini-app in the user's
207
+ * chat list — only after real engagement (opening alone, and automatic
208
+ * load-time SDK calls, never count). It works for a fully self-contained
209
+ * app/game that never calls any other SDK method: no `Usion.*` call is
210
+ * required for the host to know the user is engaged.
211
+ *
212
+ * Standalone (non-embedded) apps are unaffected — `_post` no-ops when there
213
+ * is no host. Note: input that produces no DOM gesture (pure device-motion,
214
+ * gamepad) won't trigger this until the first tap/click/key.
215
+ * @private
216
+ */
217
+ _setupInteractionBeacon: function() {
218
+ const self = this;
219
+ if (self._interactionBeaconSetup) return;
220
+ self._interactionBeaconSetup = true;
221
+ if (typeof window === 'undefined' || !window.addEventListener) return;
222
+
223
+ const events = ['pointerdown', 'mousedown', 'touchstart', 'keydown'];
224
+ function fire(event) {
225
+ // Only count real user gestures, never programmatically dispatched ones.
226
+ if (event && event.isTrusted === false) return;
227
+ if (self._interactionReported) return;
228
+ self._interactionReported = true;
229
+ for (let i = 0; i < events.length; i++) {
230
+ try { window.removeEventListener(events[i], fire, true); } catch (e) { /* noop */ }
231
+ }
232
+ self._post({ type: 'USER_INTERACTION' });
233
+ }
234
+ for (let i = 0; i < events.length; i++) {
235
+ // Capture phase + passive so we observe the gesture without interfering
236
+ // with the app's own handlers or scroll performance.
237
+ try {
238
+ window.addEventListener(events[i], fire, { capture: true, passive: true });
239
+ } catch (e) {
240
+ try { window.addEventListener(events[i], fire, true); } catch (e2) { /* noop */ }
241
+ }
242
+ }
243
+ },
244
+
199
245
  /**
200
246
  * Get the current theme ('light' or 'dark')
201
247
  * @returns {string}
@@ -426,6 +472,13 @@ var Usion = (function () {
426
472
  _balance: null,
427
473
  _balanceChangeHandler: null,
428
474
 
475
+ // Payment timing knobs (overridable, mainly for tests). The defaults are
476
+ // the production values.
477
+ _paymentTimeoutMs: 60000, // wait this long for the direct PAYMENT_SUCCESS
478
+ _recoveryPollMs: 2000, // gap between result-recovery re-queries
479
+ _recoveryMaxPolls: 10, // bounded recovery window after the timeout
480
+ _recoveryQueryTimeoutMs: 8000, // per re-query round-trip budget
481
+
429
482
  /**
430
483
  * Get current wallet balance
431
484
  * @returns {Promise<number>} Balance in credits
@@ -456,20 +509,69 @@ var Usion = (function () {
456
509
  },
457
510
 
458
511
  /**
459
- * Request payment from user with balance check
512
+ * Request payment from user with balance check.
513
+ *
514
+ * The charge is debited immediately by the host (escrow); the resolved
515
+ * value carries a `receiptToken` the mini-app server later settles/refunds.
516
+ *
517
+ * Reliability: if the host's PAYMENT_SUCCESS message is lost (network blip,
518
+ * backgrounded tab, race) the user may ALREADY be charged. Instead of
519
+ * rejecting at the timeout, this re-queries the host for the result and
520
+ * recovers the receipt token — so a dropped message never strands a paid
521
+ * charge. Pass `idempotencyKey` to additionally guarantee that any retry of
522
+ * the SAME intent reuses the SAME charge (the host + backend dedupe on it),
523
+ * so the user can never be charged twice even across reloads.
524
+ *
460
525
  * @param {number} amount - Credit amount to charge
461
526
  * @param {string} reason - Description shown to user
462
- * @param {object} data - Optional additional data
527
+ * @param {object} [data] - Optional options/payload. `data.idempotencyKey`
528
+ * (string) makes the charge safely retryable. Any other fields are
529
+ * forwarded to the host verbatim (backward compatible).
463
530
  * @returns {Promise} Resolves on payment success, rejects on failure
464
531
  */
465
532
  requestPayment: function(amount, reason, data) {
466
533
  const self = this;
467
534
 
535
+ const opts = (data && typeof data === 'object') ? data : null;
536
+ const idempotencyKey = (opts && typeof opts.idempotencyKey === 'string' && opts.idempotencyKey)
537
+ ? opts.idempotencyKey
538
+ : null;
539
+
468
540
  return new Promise(function(resolve, reject) {
469
541
  const requestId = getNextRequestId();
470
- const timeoutMs = 60000;
542
+ const timeoutMs = self._paymentTimeoutMs || 60000;
543
+ let settled = false;
544
+ let recoverTimer = null;
545
+
546
+ function cleanup() {
547
+ clearTimeout(timer);
548
+ if (recoverTimer) clearTimeout(recoverTimer);
549
+ window.removeEventListener('message', handler);
550
+ }
551
+
552
+ function finishResolve(response) {
553
+ if (settled) return;
554
+ settled = true;
555
+ cleanup();
556
+ // Update cached balance from the authoritative new balance, else
557
+ // best-effort subtract (no-op if balance is unknown).
558
+ if (response && response.newBalance !== undefined) {
559
+ self._balance = response.newBalance;
560
+ } else if (self._balance !== null) {
561
+ self._balance -= amount;
562
+ }
563
+ resolve(response);
564
+ }
565
+
566
+ function finishReject(error) {
567
+ if (settled) return;
568
+ settled = true;
569
+ cleanup();
570
+ reject(error);
571
+ }
471
572
 
472
- // Listen for response
573
+ // Direct response from the host. Kept registered through the recovery
574
+ // window so a late PAYMENT_SUCCESS still resolves.
473
575
  function handler(event) {
474
576
  // Only honor payment results from the trusted host shell — a forged
475
577
  // PAYMENT_SUCCESS must never resolve this promise.
@@ -483,32 +585,97 @@ var Usion = (function () {
483
585
  }
484
586
 
485
587
  // Only accept responses for this specific payment request.
486
- if (response._requestId !== requestId) {
588
+ if (!response || response._requestId !== requestId) {
487
589
  return;
488
590
  }
489
591
 
490
592
  if (response.type === 'PAYMENT_SUCCESS') {
491
- clearTimeout(timer);
492
- window.removeEventListener('message', handler);
493
- // Update cached balance
494
- if (response.newBalance !== undefined) {
495
- self._balance = response.newBalance;
496
- } else if (self._balance !== null) {
497
- self._balance -= amount;
498
- }
499
- resolve(response);
593
+ finishResolve(response);
500
594
  } else if (response.type === 'PAYMENT_FAILED') {
501
- clearTimeout(timer);
502
- window.removeEventListener('message', handler);
503
- reject(new Error(response.reason || 'Payment failed'));
595
+ finishReject(new Error(response.reason || 'Payment failed'));
504
596
  }
505
597
  }
506
598
 
599
+ // After the direct deadline, re-query the host for this payment's
600
+ // result instead of rejecting.
601
+ // succeeded -> resolve with the recovered token
602
+ // failed -> reject
603
+ // pending -> the modal is STILL OPEN / charge still in progress, so
604
+ // the charge is fully recoverable: keep waiting (do NOT
605
+ // count this toward the give-up cap). A slow user who
606
+ // confirms minutes later still resolves — via the next
607
+ // 'succeeded' poll or the direct handler, which stays
608
+ // registered. Without this, a charge made after we gave
609
+ // up would be stranded (the original bug, shifted to the
610
+ // slow-confirm case).
611
+ // none/other -> likely no charge; confirm twice then reject cleanly so
612
+ // a fresh requestPayment is safe.
613
+ // The bounded cap only governs the give-up paths (none-streak and a host
614
+ // that never answers the query at all, e.g. an older host shell).
615
+ function recoverPaymentResult() {
616
+ const pollMs = self._recoveryPollMs || 2000;
617
+ const maxPolls = self._recoveryMaxPolls || 10;
618
+ const queryTimeoutMs = self._recoveryQueryTimeoutMs || 8000;
619
+ let noneStreak = 0;
620
+ let errorStreak = 0;
621
+
622
+ function poll() {
623
+ if (settled) return;
624
+ Usion._request('PAYMENT_RESULT_QUERY', {
625
+ paymentRequestId: requestId,
626
+ idempotencyKey: idempotencyKey || undefined,
627
+ }, queryTimeoutMs).then(function(res) {
628
+ if (settled) return;
629
+ const status = res && res.status;
630
+ if (status === 'succeeded') {
631
+ finishResolve({
632
+ type: 'PAYMENT_SUCCESS',
633
+ _requestId: requestId,
634
+ newBalance: res.newBalance,
635
+ transactionId: res.transactionId,
636
+ receiptToken: res.receiptToken,
637
+ });
638
+ return;
639
+ }
640
+ if (status === 'failed') {
641
+ finishReject(new Error(res.reason || 'Payment failed'));
642
+ return;
643
+ }
644
+ if (status === 'pending') {
645
+ // Charge still recoverable — keep waiting, no give-up budget.
646
+ noneStreak = 0;
647
+ errorStreak = 0;
648
+ recoverTimer = setTimeout(poll, pollMs);
649
+ return;
650
+ }
651
+ // 'none' or unknown — host is confident no charge exists. Confirm
652
+ // twice (guards a host that simply hasn't recorded yet) then reject.
653
+ errorStreak = 0;
654
+ noneStreak += 1;
655
+ if (noneStreak >= 2) {
656
+ finishReject(new Error('Payment confirmation timeout'));
657
+ return;
658
+ }
659
+ recoverTimer = setTimeout(poll, pollMs);
660
+ }).catch(function() {
661
+ // Host didn't answer the query (e.g. an older host shell without
662
+ // PAYMENT_RESULT_QUERY support). Retry a bounded number of times,
663
+ // then fall back to the original timeout rejection.
664
+ if (settled) return;
665
+ errorStreak += 1;
666
+ if (errorStreak >= maxPolls) {
667
+ finishReject(new Error('Payment confirmation timeout'));
668
+ return;
669
+ }
670
+ recoverTimer = setTimeout(poll, pollMs);
671
+ });
672
+ }
673
+
674
+ poll();
675
+ }
676
+
507
677
  window.addEventListener('message', handler);
508
- const timer = setTimeout(function() {
509
- window.removeEventListener('message', handler);
510
- reject(new Error('Payment confirmation timeout'));
511
- }, timeoutMs);
678
+ const timer = setTimeout(recoverPaymentResult, timeoutMs);
512
679
 
513
680
  // Send payment request
514
681
  Usion._post({
@@ -516,7 +683,8 @@ var Usion = (function () {
516
683
  _requestId: requestId,
517
684
  amount: amount,
518
685
  reason: reason,
519
- data: data
686
+ data: data,
687
+ idempotencyKey: idempotencyKey || undefined,
520
688
  });
521
689
  });
522
690
  },
@@ -189,10 +189,56 @@ export const core = {
189
189
  }
190
190
  });
191
191
 
192
+ // Report the user's first real interaction to the host (see below).
193
+ this._setupInteractionBeacon();
194
+
192
195
  // Signal ready to parent
193
196
  this._post({ type: 'READY' });
194
197
  },
195
198
 
199
+ /**
200
+ * Tell the host the moment the user FIRST genuinely interacts with the
201
+ * mini-app — a tap, click, key press, or touch — and only once.
202
+ *
203
+ * The host uses this as the SOLE signal to surface the mini-app in the user's
204
+ * chat list — only after real engagement (opening alone, and automatic
205
+ * load-time SDK calls, never count). It works for a fully self-contained
206
+ * app/game that never calls any other SDK method: no `Usion.*` call is
207
+ * required for the host to know the user is engaged.
208
+ *
209
+ * Standalone (non-embedded) apps are unaffected — `_post` no-ops when there
210
+ * is no host. Note: input that produces no DOM gesture (pure device-motion,
211
+ * gamepad) won't trigger this until the first tap/click/key.
212
+ * @private
213
+ */
214
+ _setupInteractionBeacon: function() {
215
+ const self = this;
216
+ if (self._interactionBeaconSetup) return;
217
+ self._interactionBeaconSetup = true;
218
+ if (typeof window === 'undefined' || !window.addEventListener) return;
219
+
220
+ const events = ['pointerdown', 'mousedown', 'touchstart', 'keydown'];
221
+ function fire(event) {
222
+ // Only count real user gestures, never programmatically dispatched ones.
223
+ if (event && event.isTrusted === false) return;
224
+ if (self._interactionReported) return;
225
+ self._interactionReported = true;
226
+ for (let i = 0; i < events.length; i++) {
227
+ try { window.removeEventListener(events[i], fire, true); } catch (e) { /* noop */ }
228
+ }
229
+ self._post({ type: 'USER_INTERACTION' });
230
+ }
231
+ for (let i = 0; i < events.length; i++) {
232
+ // Capture phase + passive so we observe the gesture without interfering
233
+ // with the app's own handlers or scroll performance.
234
+ try {
235
+ window.addEventListener(events[i], fire, { capture: true, passive: true });
236
+ } catch (e) {
237
+ try { window.addEventListener(events[i], fire, true); } catch (e2) { /* noop */ }
238
+ }
239
+ }
240
+ },
241
+
196
242
  /**
197
243
  * Get the current theme ('light' or 'dark')
198
244
  * @returns {string}
@@ -12,6 +12,13 @@ export function createWalletModule(Usion) {
12
12
  _balance: null,
13
13
  _balanceChangeHandler: null,
14
14
 
15
+ // Payment timing knobs (overridable, mainly for tests). The defaults are
16
+ // the production values.
17
+ _paymentTimeoutMs: 60000, // wait this long for the direct PAYMENT_SUCCESS
18
+ _recoveryPollMs: 2000, // gap between result-recovery re-queries
19
+ _recoveryMaxPolls: 10, // bounded recovery window after the timeout
20
+ _recoveryQueryTimeoutMs: 8000, // per re-query round-trip budget
21
+
15
22
  /**
16
23
  * Get current wallet balance
17
24
  * @returns {Promise<number>} Balance in credits
@@ -42,20 +49,69 @@ export function createWalletModule(Usion) {
42
49
  },
43
50
 
44
51
  /**
45
- * Request payment from user with balance check
52
+ * Request payment from user with balance check.
53
+ *
54
+ * The charge is debited immediately by the host (escrow); the resolved
55
+ * value carries a `receiptToken` the mini-app server later settles/refunds.
56
+ *
57
+ * Reliability: if the host's PAYMENT_SUCCESS message is lost (network blip,
58
+ * backgrounded tab, race) the user may ALREADY be charged. Instead of
59
+ * rejecting at the timeout, this re-queries the host for the result and
60
+ * recovers the receipt token — so a dropped message never strands a paid
61
+ * charge. Pass `idempotencyKey` to additionally guarantee that any retry of
62
+ * the SAME intent reuses the SAME charge (the host + backend dedupe on it),
63
+ * so the user can never be charged twice even across reloads.
64
+ *
46
65
  * @param {number} amount - Credit amount to charge
47
66
  * @param {string} reason - Description shown to user
48
- * @param {object} data - Optional additional data
67
+ * @param {object} [data] - Optional options/payload. `data.idempotencyKey`
68
+ * (string) makes the charge safely retryable. Any other fields are
69
+ * forwarded to the host verbatim (backward compatible).
49
70
  * @returns {Promise} Resolves on payment success, rejects on failure
50
71
  */
51
72
  requestPayment: function(amount, reason, data) {
52
73
  const self = this;
53
74
 
75
+ const opts = (data && typeof data === 'object') ? data : null;
76
+ const idempotencyKey = (opts && typeof opts.idempotencyKey === 'string' && opts.idempotencyKey)
77
+ ? opts.idempotencyKey
78
+ : null;
79
+
54
80
  return new Promise(function(resolve, reject) {
55
81
  const requestId = getNextRequestId();
56
- const timeoutMs = 60000;
82
+ const timeoutMs = self._paymentTimeoutMs || 60000;
83
+ let settled = false;
84
+ let recoverTimer = null;
85
+
86
+ function cleanup() {
87
+ clearTimeout(timer);
88
+ if (recoverTimer) clearTimeout(recoverTimer);
89
+ window.removeEventListener('message', handler);
90
+ }
91
+
92
+ function finishResolve(response) {
93
+ if (settled) return;
94
+ settled = true;
95
+ cleanup();
96
+ // Update cached balance from the authoritative new balance, else
97
+ // best-effort subtract (no-op if balance is unknown).
98
+ if (response && response.newBalance !== undefined) {
99
+ self._balance = response.newBalance;
100
+ } else if (self._balance !== null) {
101
+ self._balance -= amount;
102
+ }
103
+ resolve(response);
104
+ }
57
105
 
58
- // Listen for response
106
+ function finishReject(error) {
107
+ if (settled) return;
108
+ settled = true;
109
+ cleanup();
110
+ reject(error);
111
+ }
112
+
113
+ // Direct response from the host. Kept registered through the recovery
114
+ // window so a late PAYMENT_SUCCESS still resolves.
59
115
  function handler(event) {
60
116
  // Only honor payment results from the trusted host shell — a forged
61
117
  // PAYMENT_SUCCESS must never resolve this promise.
@@ -69,32 +125,97 @@ export function createWalletModule(Usion) {
69
125
  }
70
126
 
71
127
  // Only accept responses for this specific payment request.
72
- if (response._requestId !== requestId) {
128
+ if (!response || response._requestId !== requestId) {
73
129
  return;
74
130
  }
75
131
 
76
132
  if (response.type === 'PAYMENT_SUCCESS') {
77
- clearTimeout(timer);
78
- window.removeEventListener('message', handler);
79
- // Update cached balance
80
- if (response.newBalance !== undefined) {
81
- self._balance = response.newBalance;
82
- } else if (self._balance !== null) {
83
- self._balance -= amount;
84
- }
85
- resolve(response);
133
+ finishResolve(response);
86
134
  } else if (response.type === 'PAYMENT_FAILED') {
87
- clearTimeout(timer);
88
- window.removeEventListener('message', handler);
89
- reject(new Error(response.reason || 'Payment failed'));
135
+ finishReject(new Error(response.reason || 'Payment failed'));
136
+ }
137
+ }
138
+
139
+ // After the direct deadline, re-query the host for this payment's
140
+ // result instead of rejecting.
141
+ // succeeded -> resolve with the recovered token
142
+ // failed -> reject
143
+ // pending -> the modal is STILL OPEN / charge still in progress, so
144
+ // the charge is fully recoverable: keep waiting (do NOT
145
+ // count this toward the give-up cap). A slow user who
146
+ // confirms minutes later still resolves — via the next
147
+ // 'succeeded' poll or the direct handler, which stays
148
+ // registered. Without this, a charge made after we gave
149
+ // up would be stranded (the original bug, shifted to the
150
+ // slow-confirm case).
151
+ // none/other -> likely no charge; confirm twice then reject cleanly so
152
+ // a fresh requestPayment is safe.
153
+ // The bounded cap only governs the give-up paths (none-streak and a host
154
+ // that never answers the query at all, e.g. an older host shell).
155
+ function recoverPaymentResult() {
156
+ const pollMs = self._recoveryPollMs || 2000;
157
+ const maxPolls = self._recoveryMaxPolls || 10;
158
+ const queryTimeoutMs = self._recoveryQueryTimeoutMs || 8000;
159
+ let noneStreak = 0;
160
+ let errorStreak = 0;
161
+
162
+ function poll() {
163
+ if (settled) return;
164
+ Usion._request('PAYMENT_RESULT_QUERY', {
165
+ paymentRequestId: requestId,
166
+ idempotencyKey: idempotencyKey || undefined,
167
+ }, queryTimeoutMs).then(function(res) {
168
+ if (settled) return;
169
+ const status = res && res.status;
170
+ if (status === 'succeeded') {
171
+ finishResolve({
172
+ type: 'PAYMENT_SUCCESS',
173
+ _requestId: requestId,
174
+ newBalance: res.newBalance,
175
+ transactionId: res.transactionId,
176
+ receiptToken: res.receiptToken,
177
+ });
178
+ return;
179
+ }
180
+ if (status === 'failed') {
181
+ finishReject(new Error(res.reason || 'Payment failed'));
182
+ return;
183
+ }
184
+ if (status === 'pending') {
185
+ // Charge still recoverable — keep waiting, no give-up budget.
186
+ noneStreak = 0;
187
+ errorStreak = 0;
188
+ recoverTimer = setTimeout(poll, pollMs);
189
+ return;
190
+ }
191
+ // 'none' or unknown — host is confident no charge exists. Confirm
192
+ // twice (guards a host that simply hasn't recorded yet) then reject.
193
+ errorStreak = 0;
194
+ noneStreak += 1;
195
+ if (noneStreak >= 2) {
196
+ finishReject(new Error('Payment confirmation timeout'));
197
+ return;
198
+ }
199
+ recoverTimer = setTimeout(poll, pollMs);
200
+ }).catch(function() {
201
+ // Host didn't answer the query (e.g. an older host shell without
202
+ // PAYMENT_RESULT_QUERY support). Retry a bounded number of times,
203
+ // then fall back to the original timeout rejection.
204
+ if (settled) return;
205
+ errorStreak += 1;
206
+ if (errorStreak >= maxPolls) {
207
+ finishReject(new Error('Payment confirmation timeout'));
208
+ return;
209
+ }
210
+ recoverTimer = setTimeout(poll, pollMs);
211
+ });
90
212
  }
213
+
214
+ poll();
91
215
  }
92
216
 
93
217
  window.addEventListener('message', handler);
94
- const timer = setTimeout(function() {
95
- window.removeEventListener('message', handler);
96
- reject(new Error('Payment confirmation timeout'));
97
- }, timeoutMs);
218
+ const timer = setTimeout(recoverPaymentResult, timeoutMs);
98
219
 
99
220
  // Send payment request
100
221
  Usion._post({
@@ -102,7 +223,8 @@ export function createWalletModule(Usion) {
102
223
  _requestId: requestId,
103
224
  amount: amount,
104
225
  reason: reason,
105
- data: data
226
+ data: data,
227
+ idempotencyKey: idempotencyKey || undefined,
106
228
  });
107
229
  });
108
230
  },
package/types/index.d.ts CHANGED
@@ -43,6 +43,17 @@ export interface PaymentResponse {
43
43
  transactionId?: string;
44
44
  }
45
45
 
46
+ export interface PaymentOptions {
47
+ /**
48
+ * Optional idempotency key. When set, the host + backend dedupe on it so any
49
+ * retry of the SAME intent (network error, reload, double-tap) reuses the
50
+ * SAME charge instead of debiting again, and returns the same receiptToken.
51
+ * Omit for the legacy one-shot behavior.
52
+ */
53
+ idempotencyKey?: string;
54
+ [key: string]: any;
55
+ }
56
+
46
57
  // ─── User ────────────────────────────────────────────────────────
47
58
 
48
59
  export interface UserProfile {
@@ -82,7 +93,7 @@ export interface FileStorageModule {
82
93
  export interface WalletModule {
83
94
  getBalance(): Promise<number>;
84
95
  hasCredits(amount: number): Promise<boolean>;
85
- requestPayment(amount: number, reason: string, data?: Record<string, any>): Promise<PaymentResponse>;
96
+ requestPayment(amount: number, reason: string, data?: PaymentOptions): Promise<PaymentResponse>;
86
97
  onBalanceChange(callback: (balance: number) => void): UnsubscribeFn;
87
98
  }
88
99
 
@@ -768,7 +779,7 @@ export interface UsionSDK {
768
779
  getLaunchParams(): { path: string | null; ref: string | null; roomId: string | null };
769
780
 
770
781
  // Payments
771
- requestPayment(amount: number, reason: string, data?: Record<string, any>): Promise<PaymentResponse>;
782
+ requestPayment(amount: number, reason: string, data?: PaymentOptions): Promise<PaymentResponse>;
772
783
 
773
784
  // Service communication
774
785
  submit(data: Record<string, any>): void;