@usions/sdk 2.16.0 → 2.17.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.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "Usion Mini App SDK for iframe games and services",
5
5
  "type": "module",
6
6
  "main": "src/modules/index.js",
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.16.0', // injected from package.json at build
72
+ version: '2.17.0', // injected from package.json at build
73
73
  config: {},
74
74
  _initialized: false,
75
75
  _initCallback: null,
@@ -5090,6 +5090,163 @@ var Usion = (function () {
5090
5090
  };
5091
5091
  }
5092
5092
 
5093
+ /**
5094
+ * Usion SDK Permissions — a mini-app asks the user to allow capabilities.
5095
+ *
5096
+ * Works exactly like asking for money: the app requests, the HOST shows a modal,
5097
+ * the user **allows or cancels**, and the result flows back. The user can later
5098
+ * change any grant in the Usion app's settings for this app.
5099
+ *
5100
+ * const res = await Usion.permissions.request(['notifications']);
5101
+ * if (res.permissions.notifications) { ... } // granted
5102
+ *
5103
+ * const state = await Usion.permissions.query(['notifications']);
5104
+ * const ok = await Usion.permissions.has('notifications');
5105
+ *
5106
+ * Permissions are enforced by the PLATFORM, not by this return value — e.g.
5107
+ * `Usion.notify.send` is dropped (`delivered: 'blocked'`) until the user grants
5108
+ * `notifications`. So the pattern is: request first, then notify.
5109
+ *
5110
+ * This is an EMBEDDED feature. Running standalone (outside the Usion host) there
5111
+ * is no modal — request()/query() resolve to a benign "not granted" default and
5112
+ * the user manages the grant inside the Usion app.
5113
+ */
5114
+
5115
+ function createPermissionsModule(Usion) {
5116
+ function normalizeList(perms) {
5117
+ if (typeof perms === 'string') return perms ? [perms] : [];
5118
+ if (Array.isArray(perms)) {
5119
+ return perms.filter(function (p) { return typeof p === 'string' && p; });
5120
+ }
5121
+ return [];
5122
+ }
5123
+
5124
+ function isEmbedded() {
5125
+ return typeof window !== 'undefined' &&
5126
+ (!!window.ReactNativeWebView || (!!window.parent && window.parent !== window));
5127
+ }
5128
+
5129
+ function denyAll(list) {
5130
+ const out = {};
5131
+ list.forEach(function (p) { out[p] = false; });
5132
+ return out;
5133
+ }
5134
+
5135
+ function summarize(list, map) {
5136
+ const perms = map || {};
5137
+ const granted = list.length > 0 && list.every(function (p) { return !!perms[p]; });
5138
+ return { granted: granted, permissions: perms };
5139
+ }
5140
+
5141
+ const api = {
5142
+ // How long to wait for the host's PERMISSION_RESULT before falling back to a
5143
+ // state query — the user may sit on the modal. We NEVER reject: a dropped
5144
+ // result must not strand the caller (mirrors wallet.requestPayment recovery).
5145
+ // Overridable, mainly for tests.
5146
+ _requestTimeoutMs: 120000,
5147
+
5148
+ /**
5149
+ * Ask the user to allow one or more permissions. Shows the host modal
5150
+ * (unless every requested permission is already granted, in which case the
5151
+ * host resolves immediately).
5152
+ *
5153
+ * @param {string|string[]} permissions e.g. 'notifications' or ['notifications']
5154
+ * @param {{reason?: string}} [opts] optional one-line context shown in the modal
5155
+ * @returns {Promise<{granted: boolean, permissions: Object}>}
5156
+ * `granted` is true only if EVERY requested permission ended granted;
5157
+ * `permissions` maps each requested key to its resulting boolean state.
5158
+ */
5159
+ request: function (permissions, opts) {
5160
+ const list = normalizeList(permissions);
5161
+ const reason = (opts && typeof opts.reason === 'string') ? opts.reason : undefined;
5162
+
5163
+ if (!isEmbedded()) {
5164
+ try { console.warn('[Usion] permissions.request needs the Usion app; resolving as not granted.'); } catch (e) { /* noop */ }
5165
+ return Promise.resolve(summarize(list, denyAll(list)));
5166
+ }
5167
+
5168
+ return new Promise(function (resolve) {
5169
+ const requestId = getNextRequestId();
5170
+ let settled = false;
5171
+ let timer = null;
5172
+
5173
+ function cleanup() {
5174
+ if (timer) clearTimeout(timer);
5175
+ window.removeEventListener('message', handler);
5176
+ }
5177
+
5178
+ function finish(result) {
5179
+ if (settled) return;
5180
+ settled = true;
5181
+ cleanup();
5182
+ resolve(result);
5183
+ }
5184
+
5185
+ function handler(event) {
5186
+ // Only honor results from the trusted host shell. (A forged result can
5187
+ // only mislead this app's UI — the backend gate is the real guard.)
5188
+ if (!isTrustedMessageSource(event)) return;
5189
+ let response;
5190
+ try {
5191
+ response = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
5192
+ } catch (e) { return; }
5193
+ if (!response || response._requestId !== requestId) return;
5194
+ if (response.type === 'PERMISSION_RESULT') {
5195
+ finish(summarize(list, response.permissions));
5196
+ }
5197
+ }
5198
+
5199
+ window.addEventListener('message', handler);
5200
+
5201
+ // Host never answered (lost message / older host shell): fall back to the
5202
+ // current persisted state so we still resolve with the truth.
5203
+ timer = setTimeout(function () {
5204
+ if (settled) return;
5205
+ api.query(list).then(function (map) {
5206
+ finish(summarize(list, map));
5207
+ }).catch(function () {
5208
+ finish(summarize(list, denyAll(list)));
5209
+ });
5210
+ }, api._requestTimeoutMs || 120000);
5211
+
5212
+ Usion._post({
5213
+ type: 'PERMISSION_REQUEST',
5214
+ _requestId: requestId,
5215
+ permissions: list,
5216
+ reason: reason,
5217
+ });
5218
+ });
5219
+ },
5220
+
5221
+ /**
5222
+ * Read current permission state WITHOUT prompting the user.
5223
+ * @param {string|string[]} [permissions]
5224
+ * @returns {Promise<Object>} map of permission key -> boolean (granted)
5225
+ */
5226
+ query: function (permissions) {
5227
+ const list = normalizeList(permissions);
5228
+ if (!isEmbedded()) {
5229
+ return Promise.resolve(denyAll(list));
5230
+ }
5231
+ return Usion._request('PERMISSION_QUERY', { permissions: list }, 8000)
5232
+ .then(function (res) { return (res && res.permissions) || {}; });
5233
+ },
5234
+
5235
+ /**
5236
+ * Convenience: is a single permission currently granted?
5237
+ * @param {string} permission
5238
+ * @returns {Promise<boolean>}
5239
+ */
5240
+ has: function (permission) {
5241
+ return api.query([permission]).then(function (map) {
5242
+ return !!(map && map[permission]);
5243
+ });
5244
+ },
5245
+ };
5246
+
5247
+ return api;
5248
+ }
5249
+
5093
5250
  /**
5094
5251
  * Usion SDK — unified backend channel.
5095
5252
  *
@@ -5228,6 +5385,8 @@ var Usion = (function () {
5228
5385
  Usion.cloud = createCloudModule(Usion);
5229
5386
  Usion.matchmaking = createMatchmakingModule(Usion);
5230
5387
  Usion.notify = createNotifyModule(Usion);
5388
+ // Permission requests (host-mediated modal; embedded feature).
5389
+ Usion.permissions = createPermissionsModule(Usion);
5231
5390
 
5232
5391
  // Netcode toolkit (transport-agnostic, zero-dependency).
5233
5392
  Usion.netcode = netcode;
@@ -30,6 +30,7 @@ import { createLeaderboardModule } from './leaderboard.js';
30
30
  import { createCloudModule } from './cloud.js';
31
31
  import { createMatchmakingModule } from './matchmaking.js';
32
32
  import { createNotifyModule } from './notify.js';
33
+ import { createPermissionsModule } from './permissions.js';
33
34
  import { applyBackendChannel } from './backend-channel.js';
34
35
  import { netcode } from './netcode/index.js';
35
36
  import { UsionError, ERROR_CODES } from './errors.js';
@@ -56,6 +57,8 @@ Usion.leaderboard = createLeaderboardModule(Usion);
56
57
  Usion.cloud = createCloudModule(Usion);
57
58
  Usion.matchmaking = createMatchmakingModule(Usion);
58
59
  Usion.notify = createNotifyModule(Usion);
60
+ // Permission requests (host-mediated modal; embedded feature).
61
+ Usion.permissions = createPermissionsModule(Usion);
59
62
 
60
63
  // Netcode toolkit (transport-agnostic, zero-dependency).
61
64
  Usion.netcode = netcode;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Usion SDK Permissions — a mini-app asks the user to allow capabilities.
3
+ *
4
+ * Works exactly like asking for money: the app requests, the HOST shows a modal,
5
+ * the user **allows or cancels**, and the result flows back. The user can later
6
+ * change any grant in the Usion app's settings for this app.
7
+ *
8
+ * const res = await Usion.permissions.request(['notifications']);
9
+ * if (res.permissions.notifications) { ... } // granted
10
+ *
11
+ * const state = await Usion.permissions.query(['notifications']);
12
+ * const ok = await Usion.permissions.has('notifications');
13
+ *
14
+ * Permissions are enforced by the PLATFORM, not by this return value — e.g.
15
+ * `Usion.notify.send` is dropped (`delivered: 'blocked'`) until the user grants
16
+ * `notifications`. So the pattern is: request first, then notify.
17
+ *
18
+ * This is an EMBEDDED feature. Running standalone (outside the Usion host) there
19
+ * is no modal — request()/query() resolve to a benign "not granted" default and
20
+ * the user manages the grant inside the Usion app.
21
+ */
22
+ import { getNextRequestId, isTrustedMessageSource } from './core.js';
23
+
24
+ export function createPermissionsModule(Usion) {
25
+ function normalizeList(perms) {
26
+ if (typeof perms === 'string') return perms ? [perms] : [];
27
+ if (Array.isArray(perms)) {
28
+ return perms.filter(function (p) { return typeof p === 'string' && p; });
29
+ }
30
+ return [];
31
+ }
32
+
33
+ function isEmbedded() {
34
+ return typeof window !== 'undefined' &&
35
+ (!!window.ReactNativeWebView || (!!window.parent && window.parent !== window));
36
+ }
37
+
38
+ function denyAll(list) {
39
+ const out = {};
40
+ list.forEach(function (p) { out[p] = false; });
41
+ return out;
42
+ }
43
+
44
+ function summarize(list, map) {
45
+ const perms = map || {};
46
+ const granted = list.length > 0 && list.every(function (p) { return !!perms[p]; });
47
+ return { granted: granted, permissions: perms };
48
+ }
49
+
50
+ const api = {
51
+ // How long to wait for the host's PERMISSION_RESULT before falling back to a
52
+ // state query — the user may sit on the modal. We NEVER reject: a dropped
53
+ // result must not strand the caller (mirrors wallet.requestPayment recovery).
54
+ // Overridable, mainly for tests.
55
+ _requestTimeoutMs: 120000,
56
+
57
+ /**
58
+ * Ask the user to allow one or more permissions. Shows the host modal
59
+ * (unless every requested permission is already granted, in which case the
60
+ * host resolves immediately).
61
+ *
62
+ * @param {string|string[]} permissions e.g. 'notifications' or ['notifications']
63
+ * @param {{reason?: string}} [opts] optional one-line context shown in the modal
64
+ * @returns {Promise<{granted: boolean, permissions: Object}>}
65
+ * `granted` is true only if EVERY requested permission ended granted;
66
+ * `permissions` maps each requested key to its resulting boolean state.
67
+ */
68
+ request: function (permissions, opts) {
69
+ const list = normalizeList(permissions);
70
+ const reason = (opts && typeof opts.reason === 'string') ? opts.reason : undefined;
71
+
72
+ if (!isEmbedded()) {
73
+ try { console.warn('[Usion] permissions.request needs the Usion app; resolving as not granted.'); } catch (e) { /* noop */ }
74
+ return Promise.resolve(summarize(list, denyAll(list)));
75
+ }
76
+
77
+ return new Promise(function (resolve) {
78
+ const requestId = getNextRequestId();
79
+ let settled = false;
80
+ let timer = null;
81
+
82
+ function cleanup() {
83
+ if (timer) clearTimeout(timer);
84
+ window.removeEventListener('message', handler);
85
+ }
86
+
87
+ function finish(result) {
88
+ if (settled) return;
89
+ settled = true;
90
+ cleanup();
91
+ resolve(result);
92
+ }
93
+
94
+ function handler(event) {
95
+ // Only honor results from the trusted host shell. (A forged result can
96
+ // only mislead this app's UI — the backend gate is the real guard.)
97
+ if (!isTrustedMessageSource(event)) return;
98
+ let response;
99
+ try {
100
+ response = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
101
+ } catch (e) { return; }
102
+ if (!response || response._requestId !== requestId) return;
103
+ if (response.type === 'PERMISSION_RESULT') {
104
+ finish(summarize(list, response.permissions));
105
+ }
106
+ }
107
+
108
+ window.addEventListener('message', handler);
109
+
110
+ // Host never answered (lost message / older host shell): fall back to the
111
+ // current persisted state so we still resolve with the truth.
112
+ timer = setTimeout(function () {
113
+ if (settled) return;
114
+ api.query(list).then(function (map) {
115
+ finish(summarize(list, map));
116
+ }).catch(function () {
117
+ finish(summarize(list, denyAll(list)));
118
+ });
119
+ }, api._requestTimeoutMs || 120000);
120
+
121
+ Usion._post({
122
+ type: 'PERMISSION_REQUEST',
123
+ _requestId: requestId,
124
+ permissions: list,
125
+ reason: reason,
126
+ });
127
+ });
128
+ },
129
+
130
+ /**
131
+ * Read current permission state WITHOUT prompting the user.
132
+ * @param {string|string[]} [permissions]
133
+ * @returns {Promise<Object>} map of permission key -> boolean (granted)
134
+ */
135
+ query: function (permissions) {
136
+ const list = normalizeList(permissions);
137
+ if (!isEmbedded()) {
138
+ return Promise.resolve(denyAll(list));
139
+ }
140
+ return Usion._request('PERMISSION_QUERY', { permissions: list }, 8000)
141
+ .then(function (res) { return (res && res.permissions) || {}; });
142
+ },
143
+
144
+ /**
145
+ * Convenience: is a single permission currently granted?
146
+ * @param {string} permission
147
+ * @returns {Promise<boolean>}
148
+ */
149
+ has: function (permission) {
150
+ return api.query([permission]).then(function (map) {
151
+ return !!(map && map[permission]);
152
+ });
153
+ },
154
+ };
155
+
156
+ return api;
157
+ }
package/types/index.d.ts CHANGED
@@ -857,6 +857,7 @@ export interface UsionSDK {
857
857
  matchmaking: MatchmakingModule;
858
858
  cloud: CloudModule;
859
859
  notify: NotifyModule;
860
+ permissions: PermissionsModule;
860
861
  netcode: NetcodeModule;
861
862
  }
862
863
 
@@ -874,6 +875,29 @@ export interface NotifyModule {
874
875
  isMuted(opts?: { serviceId?: string }): Promise<boolean>;
875
876
  }
876
877
 
878
+ /** Result of a permission request: per-key state + whether ALL were granted. */
879
+ export interface PermissionResult {
880
+ /** True only if every requested permission ended granted. */
881
+ granted: boolean;
882
+ /** Map of each requested permission key to its resulting state. */
883
+ permissions: Record<string, boolean>;
884
+ }
885
+
886
+ /**
887
+ * Ask the user to allow capabilities (the host shows a modal — like asking for
888
+ * money). The platform enforces grants: e.g. `notify.send` is dropped until
889
+ * `notifications` is granted. Embedded feature; standalone resolves "not
890
+ * granted". Permission keys today: `'notifications'`.
891
+ */
892
+ export interface PermissionsModule {
893
+ /** Prompt the user to allow one or more permissions. */
894
+ request(permissions: string | string[], opts?: { reason?: string }): Promise<PermissionResult>;
895
+ /** Read current state without prompting; map of key -> granted. */
896
+ query(permissions?: string | string[]): Promise<Record<string, boolean>>;
897
+ /** Convenience: is a single permission currently granted? */
898
+ has(permission: string): Promise<boolean>;
899
+ }
900
+
877
901
  /** Scoped server-persisted KV operations (per-user or shared). */
878
902
  export interface CloudScope {
879
903
  /** Get a value; resolves to null when the key doesn't exist. */