@usions/sdk 2.14.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 +2 -2
- package/src/browser.js +206 -1
- package/src/modules/core.js +46 -0
- package/src/modules/index.js +3 -0
- package/src/modules/permissions.js +157 -0
- package/types/index.d.ts +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usions/sdk",
|
|
3
|
-
"version": "2.
|
|
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",
|
|
@@ -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.
|
|
72
|
+
version: '2.17.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}
|
|
@@ -5044,6 +5090,163 @@ var Usion = (function () {
|
|
|
5044
5090
|
};
|
|
5045
5091
|
}
|
|
5046
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
|
+
|
|
5047
5250
|
/**
|
|
5048
5251
|
* Usion SDK — unified backend channel.
|
|
5049
5252
|
*
|
|
@@ -5182,6 +5385,8 @@ var Usion = (function () {
|
|
|
5182
5385
|
Usion.cloud = createCloudModule(Usion);
|
|
5183
5386
|
Usion.matchmaking = createMatchmakingModule(Usion);
|
|
5184
5387
|
Usion.notify = createNotifyModule(Usion);
|
|
5388
|
+
// Permission requests (host-mediated modal; embedded feature).
|
|
5389
|
+
Usion.permissions = createPermissionsModule(Usion);
|
|
5185
5390
|
|
|
5186
5391
|
// Netcode toolkit (transport-agnostic, zero-dependency).
|
|
5187
5392
|
Usion.netcode = netcode;
|
package/src/modules/core.js
CHANGED
|
@@ -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}
|
package/src/modules/index.js
CHANGED
|
@@ -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. */
|