@oyna360/game-sdk 0.6.3 → 0.7.1
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/README.md +210 -203
- package/dist/index.d.ts +19 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +189 -107
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +57 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/docs/07-postmessage-protocol.md +53 -45
- package/docs/08-wallet.md +65 -40
- package/docs/12-commerce-release.md +12 -0
- package/package.json +57 -56
package/dist/index.js
CHANGED
|
@@ -12,14 +12,21 @@ exports.unlockAchievement = unlockAchievement;
|
|
|
12
12
|
exports.getAchievements = getAchievements;
|
|
13
13
|
exports.getWallet = getWallet;
|
|
14
14
|
exports.convertGems = convertGems;
|
|
15
|
+
exports.getProducts = getProducts;
|
|
16
|
+
exports.purchaseProduct = purchaseProduct;
|
|
17
|
+
exports.getPurchaseStatus = getPurchaseStatus;
|
|
18
|
+
exports.getPurchaseReceipts = getPurchaseReceipts;
|
|
19
|
+
exports.getInventory = getInventory;
|
|
20
|
+
exports.consumeItem = consumeItem;
|
|
21
|
+
exports.getExchangeStatus = getExchangeStatus;
|
|
15
22
|
exports.endSession = endSession;
|
|
16
23
|
const types_1 = require("./types");
|
|
17
24
|
Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return types_1.SDK_VERSION; } });
|
|
18
25
|
const DEFAULT_INIT_TIMEOUT_MS = 10_000;
|
|
19
26
|
const IFRAME_PROBE_MS = 400;
|
|
20
27
|
const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
|
|
21
|
-
const DEV_AUTH_MESSAGE =
|
|
22
|
-
const DEV_CODE_QUERY =
|
|
28
|
+
const DEV_AUTH_MESSAGE = "oyna360:dev-auth";
|
|
29
|
+
const DEV_CODE_QUERY = "oyna_dev_code";
|
|
23
30
|
let initPayload = null;
|
|
24
31
|
/** Origin of the platform parent after the first trusted iframe message. */
|
|
25
32
|
let trustedPlatformOrigin = null;
|
|
@@ -28,14 +35,14 @@ let directPlatformUrl = null;
|
|
|
28
35
|
const initWaiters = [];
|
|
29
36
|
function isEmbeddedInPlatform() {
|
|
30
37
|
try {
|
|
31
|
-
return typeof window !==
|
|
38
|
+
return typeof window !== "undefined" && window.parent !== window;
|
|
32
39
|
}
|
|
33
40
|
catch {
|
|
34
41
|
return true;
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
function rememberDirectPlatformUrl(url) {
|
|
38
|
-
const trimmed = url.replace(/\/$/,
|
|
45
|
+
const trimmed = url.replace(/\/$/, "");
|
|
39
46
|
if (trimmed)
|
|
40
47
|
directPlatformUrl = trimmed;
|
|
41
48
|
}
|
|
@@ -50,17 +57,17 @@ function asInitPayload(message) {
|
|
|
50
57
|
};
|
|
51
58
|
}
|
|
52
59
|
function isCompleteInit(data) {
|
|
53
|
-
if (!data || typeof data !==
|
|
60
|
+
if (!data || typeof data !== "object")
|
|
54
61
|
return false;
|
|
55
62
|
const m = data;
|
|
56
|
-
return (m.type ===
|
|
63
|
+
return (m.type === "platform:init" &&
|
|
57
64
|
!!m.session?.token &&
|
|
58
65
|
!!m.user?.id &&
|
|
59
66
|
!!m.game?.slug &&
|
|
60
67
|
!!m.avatar?.presetKey);
|
|
61
68
|
}
|
|
62
69
|
function rememberTrustedOrigin(origin) {
|
|
63
|
-
if (!trustedPlatformOrigin && origin && origin !==
|
|
70
|
+
if (!trustedPlatformOrigin && origin && origin !== "null") {
|
|
64
71
|
trustedPlatformOrigin = origin;
|
|
65
72
|
}
|
|
66
73
|
}
|
|
@@ -70,7 +77,7 @@ function rememberTrustedOrigin(origin) {
|
|
|
70
77
|
function getParentMessageTarget() {
|
|
71
78
|
if (trustedPlatformOrigin)
|
|
72
79
|
return trustedPlatformOrigin;
|
|
73
|
-
if (typeof window !==
|
|
80
|
+
if (typeof window !== "undefined") {
|
|
74
81
|
const raw = window.__OYNA360_DEV__?.platformWebUrl;
|
|
75
82
|
if (raw) {
|
|
76
83
|
try {
|
|
@@ -81,7 +88,7 @@ function getParentMessageTarget() {
|
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
try {
|
|
84
|
-
if (typeof document !==
|
|
91
|
+
if (typeof document !== "undefined" && document.referrer) {
|
|
85
92
|
return new URL(document.referrer).origin;
|
|
86
93
|
}
|
|
87
94
|
}
|
|
@@ -89,7 +96,7 @@ function getParentMessageTarget() {
|
|
|
89
96
|
/* ignore */
|
|
90
97
|
}
|
|
91
98
|
}
|
|
92
|
-
return
|
|
99
|
+
return "*";
|
|
93
100
|
}
|
|
94
101
|
/**
|
|
95
102
|
* Only accept postMessage traffic from the real platform parent.
|
|
@@ -97,7 +104,7 @@ function getParentMessageTarget() {
|
|
|
97
104
|
* here so settleInit → publish → onMessage cannot recurse.
|
|
98
105
|
*/
|
|
99
106
|
function isTrustedPlatformMessage(event) {
|
|
100
|
-
if (typeof window ===
|
|
107
|
+
if (typeof window === "undefined")
|
|
101
108
|
return false;
|
|
102
109
|
if (!isEmbeddedInPlatform())
|
|
103
110
|
return false;
|
|
@@ -109,16 +116,16 @@ function isTrustedPlatformMessage(event) {
|
|
|
109
116
|
}
|
|
110
117
|
/** Publish standard platform:init for lobby-sdk / game listeners (same-window). */
|
|
111
118
|
function publishPlatformInit(payload) {
|
|
112
|
-
if (typeof window ===
|
|
119
|
+
if (typeof window === "undefined")
|
|
113
120
|
return;
|
|
114
121
|
const message = {
|
|
115
|
-
type:
|
|
122
|
+
type: "platform:init",
|
|
116
123
|
version: types_1.SDK_VERSION,
|
|
117
124
|
...payload,
|
|
118
125
|
};
|
|
119
126
|
window.__OYNA360_PLATFORM_INIT__ = message;
|
|
120
127
|
window.__OYNA360_PLATFORM_INIT_OK__ = true;
|
|
121
|
-
window.dispatchEvent(new MessageEvent(
|
|
128
|
+
window.dispatchEvent(new MessageEvent("message", {
|
|
122
129
|
data: message,
|
|
123
130
|
origin: window.location.origin,
|
|
124
131
|
}));
|
|
@@ -142,16 +149,18 @@ function onMessage(event) {
|
|
|
142
149
|
const data = event.data;
|
|
143
150
|
if (!isCompleteInit(data)) {
|
|
144
151
|
// Production parent may send init; accept even if avatar missing for backward compat
|
|
145
|
-
if (!data ||
|
|
152
|
+
if (!data ||
|
|
153
|
+
typeof data !== "object" ||
|
|
154
|
+
data.type !== "platform:init") {
|
|
146
155
|
return;
|
|
147
156
|
}
|
|
148
157
|
const message = data;
|
|
149
158
|
if (!message.session?.token || !message.user?.id || !message.game?.slug)
|
|
150
159
|
return;
|
|
151
160
|
const avatar = message.avatar ?? {
|
|
152
|
-
presetId:
|
|
153
|
-
presetKey:
|
|
154
|
-
presetKind:
|
|
161
|
+
presetId: "default",
|
|
162
|
+
presetKey: "default-1",
|
|
163
|
+
presetKind: "procedural",
|
|
155
164
|
customConfig: {},
|
|
156
165
|
};
|
|
157
166
|
rememberTrustedOrigin(event.origin);
|
|
@@ -165,14 +174,14 @@ function onMessage(event) {
|
|
|
165
174
|
settleInit(asInitPayload(data));
|
|
166
175
|
}
|
|
167
176
|
function signalReady() {
|
|
168
|
-
if (typeof window ===
|
|
177
|
+
if (typeof window === "undefined")
|
|
169
178
|
return;
|
|
170
179
|
if (!isEmbeddedInPlatform())
|
|
171
180
|
return;
|
|
172
|
-
window.parent.postMessage({ type:
|
|
181
|
+
window.parent.postMessage({ type: "platform:ready" }, getParentMessageTarget());
|
|
173
182
|
}
|
|
174
|
-
if (typeof window !==
|
|
175
|
-
window.addEventListener(
|
|
183
|
+
if (typeof window !== "undefined") {
|
|
184
|
+
window.addEventListener("message", onMessage);
|
|
176
185
|
signalReady();
|
|
177
186
|
}
|
|
178
187
|
function postToPlatform(outboundType, payload, resultType, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, stableRequestId) {
|
|
@@ -181,23 +190,24 @@ function postToPlatform(outboundType, payload, resultType, timeoutMs = DEFAULT_R
|
|
|
181
190
|
reject(new Error(`${outboundType} requires the platform iframe parent`));
|
|
182
191
|
return;
|
|
183
192
|
}
|
|
184
|
-
const requestId = stableRequestId ??
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
const requestId = stableRequestId ??
|
|
194
|
+
(typeof crypto !== "undefined" && crypto.randomUUID
|
|
195
|
+
? crypto.randomUUID()
|
|
196
|
+
: `${Date.now()}-${Math.random()}`);
|
|
187
197
|
const timer = setTimeout(() => {
|
|
188
|
-
window.removeEventListener(
|
|
189
|
-
reject(new Error(
|
|
198
|
+
window.removeEventListener("message", onResult);
|
|
199
|
+
reject(new Error("Platform SDK request timeout"));
|
|
190
200
|
}, timeoutMs);
|
|
191
201
|
function onResult(event) {
|
|
192
202
|
if (!isTrustedPlatformMessage(event))
|
|
193
203
|
return;
|
|
194
204
|
const data = event.data;
|
|
195
|
-
if (!data || typeof data !==
|
|
205
|
+
if (!data || typeof data !== "object" || data.type !== resultType)
|
|
196
206
|
return;
|
|
197
207
|
if (data.requestId !== requestId)
|
|
198
208
|
return;
|
|
199
209
|
clearTimeout(timer);
|
|
200
|
-
window.removeEventListener(
|
|
210
|
+
window.removeEventListener("message", onResult);
|
|
201
211
|
rememberTrustedOrigin(event.origin);
|
|
202
212
|
const error = data.error;
|
|
203
213
|
if (error) {
|
|
@@ -206,41 +216,41 @@ function postToPlatform(outboundType, payload, resultType, timeoutMs = DEFAULT_R
|
|
|
206
216
|
}
|
|
207
217
|
resolve(data.result);
|
|
208
218
|
}
|
|
209
|
-
window.addEventListener(
|
|
219
|
+
window.addEventListener("message", onResult);
|
|
210
220
|
window.parent.postMessage({ type: outboundType, requestId, ...payload }, getParentMessageTarget());
|
|
211
221
|
});
|
|
212
222
|
}
|
|
213
223
|
function nestErrorMessage(data, fallback) {
|
|
214
|
-
if (!data || typeof data !==
|
|
224
|
+
if (!data || typeof data !== "object")
|
|
215
225
|
return fallback;
|
|
216
226
|
const message = data.message;
|
|
217
|
-
if (typeof message ===
|
|
227
|
+
if (typeof message === "string")
|
|
218
228
|
return message;
|
|
219
229
|
if (Array.isArray(message))
|
|
220
|
-
return message.join(
|
|
230
|
+
return message.join(", ");
|
|
221
231
|
return fallback;
|
|
222
232
|
}
|
|
223
233
|
/** Direct Development / top-level: call platform REST with the game session token. */
|
|
224
234
|
async function apiRequest(path, options = {}) {
|
|
225
235
|
const base = directPlatformUrl;
|
|
226
236
|
if (!base) {
|
|
227
|
-
throw new Error(
|
|
237
|
+
throw new Error("Direct API calls require platformUrl. Pass PlatformSDK.init({ platformUrl, gameSlug }) or window.__OYNA360_DEV__.");
|
|
228
238
|
}
|
|
229
239
|
const headers = {
|
|
230
|
-
Accept:
|
|
240
|
+
Accept: "application/json",
|
|
231
241
|
};
|
|
232
242
|
if (options.body !== undefined) {
|
|
233
|
-
headers[
|
|
243
|
+
headers["Content-Type"] = "application/json";
|
|
234
244
|
}
|
|
235
245
|
if (options.auth !== false) {
|
|
236
246
|
const token = initPayload?.session?.token;
|
|
237
247
|
if (!token) {
|
|
238
|
-
throw new Error(
|
|
248
|
+
throw new Error("PlatformSDK.init() must complete before calling this API");
|
|
239
249
|
}
|
|
240
250
|
headers.Authorization = `Bearer ${token}`;
|
|
241
251
|
}
|
|
242
252
|
const response = await fetch(`${base}${path}`, {
|
|
243
|
-
method: options.method ?? (options.body !== undefined ?
|
|
253
|
+
method: options.method ?? (options.body !== undefined ? "POST" : "GET"),
|
|
244
254
|
headers,
|
|
245
255
|
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
246
256
|
});
|
|
@@ -260,19 +270,27 @@ async function markDirectSessionReady() {
|
|
|
260
270
|
if (!directPlatformUrl || !initPayload?.session?.token)
|
|
261
271
|
return;
|
|
262
272
|
try {
|
|
263
|
-
await apiRequest(
|
|
273
|
+
await apiRequest("/sessions/ready", { method: "POST" });
|
|
264
274
|
}
|
|
265
275
|
catch {
|
|
266
276
|
// Non-fatal: presence analytics only
|
|
267
277
|
}
|
|
268
278
|
}
|
|
269
279
|
function resolveDevConfig(options) {
|
|
270
|
-
const fromWindow = typeof window !==
|
|
271
|
-
const platformUrl = (options?.platformUrl ||
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
280
|
+
const fromWindow = typeof window !== "undefined" ? window.__OYNA360_DEV__ : undefined;
|
|
281
|
+
const platformUrl = (options?.platformUrl ||
|
|
282
|
+
fromWindow?.platformUrl ||
|
|
283
|
+
"").replace(/\/$/, "");
|
|
284
|
+
const gameSlug = (options?.gameSlug || fromWindow?.gameSlug || "").trim();
|
|
285
|
+
const clientId = (options?.dev?.clientId ||
|
|
286
|
+
fromWindow?.clientId ||
|
|
287
|
+
"").trim();
|
|
288
|
+
const credential = (options?.dev?.credential ||
|
|
289
|
+
fromWindow?.credential ||
|
|
290
|
+
"").trim();
|
|
291
|
+
let platformWebUrl = (options?.platformWebUrl ||
|
|
292
|
+
fromWindow?.platformWebUrl ||
|
|
293
|
+
"").replace(/\/$/, "");
|
|
276
294
|
if (!platformWebUrl && platformUrl) {
|
|
277
295
|
try {
|
|
278
296
|
// Same host as API by default (production reverse-proxy). Locally set platformWebUrl explicitly
|
|
@@ -280,7 +298,7 @@ function resolveDevConfig(options) {
|
|
|
280
298
|
platformWebUrl = new URL(platformUrl).origin;
|
|
281
299
|
}
|
|
282
300
|
catch {
|
|
283
|
-
platformWebUrl =
|
|
301
|
+
platformWebUrl = "";
|
|
284
302
|
}
|
|
285
303
|
}
|
|
286
304
|
return { platformUrl, platformWebUrl, gameSlug, clientId, credential };
|
|
@@ -288,41 +306,41 @@ function resolveDevConfig(options) {
|
|
|
288
306
|
async function runCredentialGatewayBootstrap(options) {
|
|
289
307
|
const { platformUrl, clientId, credential } = resolveDevConfig(options);
|
|
290
308
|
if (!platformUrl || !clientId || !credential) {
|
|
291
|
-
throw new Error(
|
|
292
|
-
|
|
309
|
+
throw new Error("Developer Environment requires platformUrl + dev.clientId + dev.credential. " +
|
|
310
|
+
"Create them at /developer and pass PlatformSDK.init({ platformUrl, dev: { clientId, credential } }).");
|
|
293
311
|
}
|
|
294
312
|
rememberDirectPlatformUrl(platformUrl);
|
|
295
313
|
const origin = window.location.origin;
|
|
296
314
|
const response = await fetch(`${platformUrl}/dev/gateway/session`, {
|
|
297
|
-
method:
|
|
298
|
-
headers: {
|
|
315
|
+
method: "POST",
|
|
316
|
+
headers: { "Content-Type": "application/json" },
|
|
299
317
|
body: JSON.stringify({ clientId, credential, origin }),
|
|
300
318
|
});
|
|
301
319
|
const data = await response.json().catch(() => ({}));
|
|
302
320
|
if (!response.ok) {
|
|
303
|
-
const msg = typeof data.message ===
|
|
321
|
+
const msg = typeof data.message === "string"
|
|
304
322
|
? data.message
|
|
305
323
|
: Array.isArray(data.message)
|
|
306
|
-
? data.message.join(
|
|
307
|
-
:
|
|
324
|
+
? data.message.join(", ")
|
|
325
|
+
: "Dev gateway session failed";
|
|
308
326
|
throw new Error(msg);
|
|
309
327
|
}
|
|
310
328
|
const payload = data;
|
|
311
329
|
if (!payload?.session?.token || !payload.avatar || !payload.lobby?.wsUrl) {
|
|
312
|
-
throw new Error(
|
|
330
|
+
throw new Error("Invalid SdkInitPayload from dev gateway");
|
|
313
331
|
}
|
|
314
332
|
return payload;
|
|
315
333
|
}
|
|
316
334
|
function readCodeFromUrl() {
|
|
317
|
-
if (typeof window ===
|
|
335
|
+
if (typeof window === "undefined")
|
|
318
336
|
return null;
|
|
319
337
|
try {
|
|
320
338
|
const url = new URL(window.location.href);
|
|
321
339
|
const code = url.searchParams.get(DEV_CODE_QUERY);
|
|
322
340
|
if (code) {
|
|
323
341
|
url.searchParams.delete(DEV_CODE_QUERY);
|
|
324
|
-
url.searchParams.delete(
|
|
325
|
-
window.history.replaceState({},
|
|
342
|
+
url.searchParams.delete("oyna_dev_slug");
|
|
343
|
+
window.history.replaceState({}, "", url.toString());
|
|
326
344
|
}
|
|
327
345
|
return code;
|
|
328
346
|
}
|
|
@@ -332,22 +350,22 @@ function readCodeFromUrl() {
|
|
|
332
350
|
}
|
|
333
351
|
async function exchangeDevCode(platformUrl, code, gameOrigin) {
|
|
334
352
|
const response = await fetch(`${platformUrl}/dev/game-auth/exchange`, {
|
|
335
|
-
method:
|
|
336
|
-
headers: {
|
|
353
|
+
method: "POST",
|
|
354
|
+
headers: { "Content-Type": "application/json" },
|
|
337
355
|
body: JSON.stringify({ code, gameOrigin }),
|
|
338
356
|
});
|
|
339
357
|
const data = await response.json().catch(() => ({}));
|
|
340
358
|
if (!response.ok) {
|
|
341
|
-
const msg = typeof data.message ===
|
|
359
|
+
const msg = typeof data.message === "string"
|
|
342
360
|
? data.message
|
|
343
361
|
: Array.isArray(data.message)
|
|
344
|
-
? data.message.join(
|
|
345
|
-
:
|
|
362
|
+
? data.message.join(", ")
|
|
363
|
+
: "Development authorization exchange failed";
|
|
346
364
|
throw new Error(msg);
|
|
347
365
|
}
|
|
348
366
|
const payload = data;
|
|
349
367
|
if (!payload?.session?.token || !payload.avatar || !payload.lobby?.wsUrl) {
|
|
350
|
-
throw new Error(
|
|
368
|
+
throw new Error("Invalid SdkInitPayload from development exchange");
|
|
351
369
|
}
|
|
352
370
|
return payload;
|
|
353
371
|
}
|
|
@@ -356,13 +374,13 @@ function openDevAuthorizePopup(authUrl) {
|
|
|
356
374
|
const height = 720;
|
|
357
375
|
const left = Math.max(0, Math.floor(window.screenX + (window.outerWidth - width) / 2));
|
|
358
376
|
const top = Math.max(0, Math.floor(window.screenY + (window.outerHeight - height) / 2));
|
|
359
|
-
return window.open(authUrl,
|
|
377
|
+
return window.open(authUrl, "oyna360-dev-auth", `popup=yes,width=${width},height=${height},left=${left},top=${top}`);
|
|
360
378
|
}
|
|
361
379
|
async function runDirectDevelopmentBootstrap(options) {
|
|
362
380
|
const { platformUrl, platformWebUrl, gameSlug } = resolveDevConfig(options);
|
|
363
381
|
if (!platformUrl || !gameSlug || !platformWebUrl) {
|
|
364
|
-
throw new Error(
|
|
365
|
-
|
|
382
|
+
throw new Error("Direct Development Mode requires platformUrl + gameSlug (and platformWebUrl). " +
|
|
383
|
+
"Set PlatformSDK.init({ platformUrl, gameSlug }) or window.__OYNA360_DEV__.");
|
|
366
384
|
}
|
|
367
385
|
rememberDirectPlatformUrl(platformUrl);
|
|
368
386
|
const gameOrigin = window.location.origin;
|
|
@@ -370,17 +388,18 @@ async function runDirectDevelopmentBootstrap(options) {
|
|
|
370
388
|
if (existingCode) {
|
|
371
389
|
return exchangeDevCode(platformUrl, existingCode, gameOrigin);
|
|
372
390
|
}
|
|
373
|
-
const returnPath = `${window.location.pathname}${window.location.search}${window.location.hash}` ||
|
|
374
|
-
|
|
375
|
-
authUrl
|
|
376
|
-
authUrl.searchParams.set(
|
|
377
|
-
authUrl.searchParams.set(
|
|
391
|
+
const returnPath = `${window.location.pathname}${window.location.search}${window.location.hash}` ||
|
|
392
|
+
"/";
|
|
393
|
+
const authUrl = new URL("/dev/game-auth", platformWebUrl);
|
|
394
|
+
authUrl.searchParams.set("slug", gameSlug);
|
|
395
|
+
authUrl.searchParams.set("origin", gameOrigin);
|
|
396
|
+
authUrl.searchParams.set("return", returnPath.startsWith("/") ? returnPath : "/");
|
|
378
397
|
return new Promise((resolve, reject) => {
|
|
379
398
|
const timeoutMs = options?.timeout ?? 120_000;
|
|
380
399
|
let settled = false;
|
|
381
400
|
const cleanup = () => {
|
|
382
401
|
window.clearTimeout(timer);
|
|
383
|
-
window.removeEventListener(
|
|
402
|
+
window.removeEventListener("message", onAuthMessage);
|
|
384
403
|
};
|
|
385
404
|
const finish = (payload) => {
|
|
386
405
|
if (settled)
|
|
@@ -397,15 +416,15 @@ async function runDirectDevelopmentBootstrap(options) {
|
|
|
397
416
|
reject(err);
|
|
398
417
|
};
|
|
399
418
|
const timer = window.setTimeout(() => {
|
|
400
|
-
fail(new Error(
|
|
419
|
+
fail(new Error("Direct Development authorization timed out. Log in on the Oyna360 authorize page and allow popups."));
|
|
401
420
|
}, timeoutMs);
|
|
402
421
|
const onAuthMessage = (event) => {
|
|
403
422
|
const data = event.data;
|
|
404
|
-
if (!data || typeof data !==
|
|
423
|
+
if (!data || typeof data !== "object")
|
|
405
424
|
return;
|
|
406
425
|
if (data.type !== DEV_AUTH_MESSAGE)
|
|
407
426
|
return;
|
|
408
|
-
let expectedOrigin =
|
|
427
|
+
let expectedOrigin = "";
|
|
409
428
|
try {
|
|
410
429
|
expectedOrigin = new URL(platformWebUrl).origin;
|
|
411
430
|
}
|
|
@@ -419,7 +438,7 @@ async function runDirectDevelopmentBootstrap(options) {
|
|
|
419
438
|
return;
|
|
420
439
|
void exchangeDevCode(platformUrl, code, gameOrigin).then(finish, fail);
|
|
421
440
|
};
|
|
422
|
-
window.addEventListener(
|
|
441
|
+
window.addEventListener("message", onAuthMessage);
|
|
423
442
|
const popup = openDevAuthorizePopup(authUrl.toString());
|
|
424
443
|
if (!popup) {
|
|
425
444
|
// Popup blocked → full redirect; after login, game reloads with ?oyna_dev_code=
|
|
@@ -438,7 +457,7 @@ function waitForIframeInit(timeoutMs) {
|
|
|
438
457
|
const index = initWaiters.findIndex((w) => w.timer === timer);
|
|
439
458
|
if (index >= 0)
|
|
440
459
|
initWaiters.splice(index, 1);
|
|
441
|
-
reject(new Error(
|
|
460
|
+
reject(new Error("Platform SDK init timeout — is the game running inside the platform?"));
|
|
442
461
|
}, timeoutMs);
|
|
443
462
|
initWaiters.push({ resolve, reject, timer });
|
|
444
463
|
signalReady();
|
|
@@ -454,8 +473,8 @@ async function init(options) {
|
|
|
454
473
|
if (initPayload) {
|
|
455
474
|
return initPayload;
|
|
456
475
|
}
|
|
457
|
-
if (typeof window ===
|
|
458
|
-
throw new Error(
|
|
476
|
+
if (typeof window === "undefined") {
|
|
477
|
+
throw new Error("PlatformSDK.init() requires a browser environment");
|
|
459
478
|
}
|
|
460
479
|
// Pin API base early so Direct Dev feature calls work after init.
|
|
461
480
|
const earlyDev = resolveDevConfig(options);
|
|
@@ -472,13 +491,14 @@ async function init(options) {
|
|
|
472
491
|
}
|
|
473
492
|
}
|
|
474
493
|
// Already delivered by a trusted writer (this SDK or lobby-sdk after origin check)
|
|
475
|
-
if (window.__OYNA360_PLATFORM_INIT_OK__ &&
|
|
494
|
+
if (window.__OYNA360_PLATFORM_INIT_OK__ &&
|
|
495
|
+
window.__OYNA360_PLATFORM_INIT__?.session?.token) {
|
|
476
496
|
const cached = asInitPayload(window.__OYNA360_PLATFORM_INIT__);
|
|
477
497
|
if (!cached.avatar) {
|
|
478
498
|
cached.avatar = {
|
|
479
|
-
presetId:
|
|
480
|
-
presetKey:
|
|
481
|
-
presetKind:
|
|
499
|
+
presetId: "default",
|
|
500
|
+
presetKey: "default-1",
|
|
501
|
+
presetKind: "procedural",
|
|
482
502
|
customConfig: {},
|
|
483
503
|
};
|
|
484
504
|
}
|
|
@@ -524,11 +544,11 @@ function isReady() {
|
|
|
524
544
|
async function submitScore(score) {
|
|
525
545
|
const payload = await init();
|
|
526
546
|
if (score < 0 || !Number.isFinite(score)) {
|
|
527
|
-
throw new Error(
|
|
547
|
+
throw new Error("Score must be a non-negative number");
|
|
528
548
|
}
|
|
529
549
|
const floorScore = Math.floor(score);
|
|
530
|
-
return callPlatformFeature(
|
|
531
|
-
method:
|
|
550
|
+
return callPlatformFeature("platform:score:submit", { sessionToken: payload.session.token, score: floorScore }, "platform:score:result", () => apiRequest(`/games/${payload.game.slug}/scores`, {
|
|
551
|
+
method: "POST",
|
|
532
552
|
body: { score: floorScore },
|
|
533
553
|
}));
|
|
534
554
|
}
|
|
@@ -536,63 +556,118 @@ async function submitScore(score) {
|
|
|
536
556
|
async function getLeaderboard(limit = 20) {
|
|
537
557
|
const payload = await init();
|
|
538
558
|
const safeLimit = Math.min(Math.max(Math.floor(limit) || 20, 1), 100);
|
|
539
|
-
return callPlatformFeature(
|
|
559
|
+
return callPlatformFeature("platform:leaderboard:get", { limit: safeLimit }, "platform:leaderboard:result", () => apiRequest(`/games/${payload.game.slug}/leaderboard?limit=${safeLimit}`, { auth: false }));
|
|
540
560
|
}
|
|
541
561
|
/** Unlock an achievement for the current player. Idempotent. */
|
|
542
562
|
async function unlockAchievement(key) {
|
|
543
563
|
const payload = await init();
|
|
544
564
|
if (!key.trim()) {
|
|
545
|
-
throw new Error(
|
|
565
|
+
throw new Error("Achievement key is required");
|
|
546
566
|
}
|
|
547
567
|
const achievementKey = key.trim();
|
|
548
|
-
return callPlatformFeature(
|
|
549
|
-
method:
|
|
568
|
+
return callPlatformFeature("platform:achievement:unlock", { sessionToken: payload.session.token, key: achievementKey }, "platform:achievement:unlock:result", () => apiRequest(`/games/${payload.game.slug}/achievements/unlock`, {
|
|
569
|
+
method: "POST",
|
|
550
570
|
body: { key: achievementKey },
|
|
551
571
|
}));
|
|
552
572
|
}
|
|
553
573
|
/** List achievements with unlock status for the current player. */
|
|
554
574
|
async function getAchievements() {
|
|
555
575
|
const payload = await init();
|
|
556
|
-
return callPlatformFeature(
|
|
576
|
+
return callPlatformFeature("platform:achievements:get", {}, "platform:achievements:result", () => apiRequest(`/games/${payload.game.slug}/achievements/me`));
|
|
557
577
|
}
|
|
558
578
|
/** Platform gem balance + coins-per-gem rate for this game. */
|
|
559
579
|
async function getWallet() {
|
|
560
580
|
const payload = await init();
|
|
561
|
-
return callPlatformFeature(
|
|
581
|
+
return callPlatformFeature("platform:wallet:get", {}, "platform:wallet:result", () => apiRequest(`/games/${payload.game.slug}/wallet`));
|
|
562
582
|
}
|
|
563
583
|
/**
|
|
564
584
|
* Convert platform gems into in-game coins.
|
|
565
585
|
* Platform deducts gems; the game must credit `coinsReceived` to its own economy.
|
|
566
586
|
*/
|
|
567
|
-
|
|
587
|
+
function operationId(value) {
|
|
588
|
+
const id = value ??
|
|
589
|
+
(typeof crypto !== "undefined" && crypto.randomUUID
|
|
590
|
+
? crypto.randomUUID()
|
|
591
|
+
: Date.now().toString(36) + "-" + Math.random().toString(36).slice(2));
|
|
592
|
+
if (!/^[A-Za-z0-9_-]{8,128}$/.test(id))
|
|
593
|
+
throw new Error("requestId must contain 8-128 ASCII letters, digits, underscores or hyphens");
|
|
594
|
+
return id;
|
|
595
|
+
}
|
|
596
|
+
function requiredOperationId(value) {
|
|
597
|
+
if (typeof value !== "string")
|
|
598
|
+
throw new Error("requestId is required");
|
|
599
|
+
return operationId(value);
|
|
600
|
+
}
|
|
601
|
+
function itemKey(value) {
|
|
602
|
+
if (!/^[A-Za-z0-9:_-]{1,100}$/.test(value))
|
|
603
|
+
throw new Error("Invalid item ID");
|
|
604
|
+
return value;
|
|
605
|
+
}
|
|
606
|
+
async function commerce(operation, args = {}) {
|
|
568
607
|
const payload = await init();
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (
|
|
573
|
-
throw new Error(
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
608
|
+
return callPlatformFeature("platform:commerce:request", { operation, args }, "platform:commerce:result", () => apiRequest(`/games/${encodeURIComponent(payload.game.slug)}/commerce`, { method: "POST", body: { operation, args } }));
|
|
609
|
+
}
|
|
610
|
+
async function convertGems(gems, requestId) {
|
|
611
|
+
if (!Number.isSafeInteger(gems) || gems < 1)
|
|
612
|
+
throw new Error("gems must be a positive safe integer");
|
|
613
|
+
return commerce("exchange", {
|
|
614
|
+
gems,
|
|
615
|
+
requestId: operationId(requestId),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function getProducts() {
|
|
619
|
+
return commerce("products");
|
|
620
|
+
}
|
|
621
|
+
/** Persist requestId before sending and reuse it when recovering/retrying the same purchase. */
|
|
622
|
+
function purchaseProduct(itemId, requestId) {
|
|
623
|
+
return commerce("purchase", {
|
|
624
|
+
itemId: itemKey(itemId),
|
|
625
|
+
requestId: operationId(requestId),
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
function getPurchaseStatus(requestId) {
|
|
629
|
+
return commerce("purchaseStatus", {
|
|
630
|
+
requestId: requiredOperationId(requestId),
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
function getPurchaseReceipts(cursor) {
|
|
634
|
+
if (cursor !== undefined && !/^[A-Za-z0-9-]{1,100}$/.test(cursor))
|
|
635
|
+
throw new Error("Invalid receipt cursor");
|
|
636
|
+
return commerce("receipts", cursor ? { cursor } : {});
|
|
637
|
+
}
|
|
638
|
+
function getInventory() {
|
|
639
|
+
return commerce("inventory");
|
|
640
|
+
}
|
|
641
|
+
function consumeItem(itemId, quantity, requestId) {
|
|
642
|
+
if (!Number.isSafeInteger(quantity) || quantity < 1)
|
|
643
|
+
throw new Error("quantity must be a positive safe integer");
|
|
644
|
+
return commerce("consume", {
|
|
645
|
+
itemId: itemKey(itemId),
|
|
646
|
+
quantity,
|
|
647
|
+
requestId: operationId(requestId),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
function getExchangeStatus(requestId) {
|
|
651
|
+
return commerce("exchangeStatus", {
|
|
652
|
+
requestId: requiredOperationId(requestId),
|
|
653
|
+
});
|
|
579
654
|
}
|
|
580
655
|
async function endSession() {
|
|
581
656
|
const token = initPayload?.session.token;
|
|
582
|
-
if (!token || typeof window ===
|
|
657
|
+
if (!token || typeof window === "undefined")
|
|
583
658
|
return;
|
|
584
659
|
if (isEmbeddedInPlatform()) {
|
|
585
660
|
const target = getParentMessageTarget();
|
|
586
661
|
window.parent.postMessage({
|
|
587
|
-
type:
|
|
662
|
+
type: "platform:session:end",
|
|
588
663
|
sessionToken: token,
|
|
589
664
|
}, target);
|
|
590
665
|
return;
|
|
591
666
|
}
|
|
592
667
|
if (directPlatformUrl) {
|
|
593
668
|
try {
|
|
594
|
-
await apiRequest(
|
|
595
|
-
method:
|
|
669
|
+
await apiRequest("/sessions/end", {
|
|
670
|
+
method: "POST",
|
|
596
671
|
body: { token },
|
|
597
672
|
auth: false,
|
|
598
673
|
});
|
|
@@ -615,6 +690,13 @@ exports.PlatformSDK = {
|
|
|
615
690
|
getAchievements,
|
|
616
691
|
getWallet,
|
|
617
692
|
convertGems,
|
|
693
|
+
getProducts,
|
|
694
|
+
purchaseProduct,
|
|
695
|
+
getPurchaseStatus,
|
|
696
|
+
getPurchaseReceipts,
|
|
697
|
+
getInventory,
|
|
698
|
+
consumeItem,
|
|
699
|
+
getExchangeStatus,
|
|
618
700
|
endSession,
|
|
619
701
|
};
|
|
620
702
|
exports.default = exports.PlatformSDK;
|