@hyve-sdk/js 1.5.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -658
- package/dist/index.d.mts +164 -137
- package/dist/index.d.ts +164 -137
- package/dist/index.js +483 -538
- package/dist/index.mjs +478 -530
- package/dist/react.d.mts +453 -0
- package/dist/react.d.ts +453 -0
- package/dist/react.js +2173 -0
- package/dist/react.mjs +2151 -0
- package/package.json +30 -15
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,2151 @@
|
|
|
1
|
+
// src/react.tsx
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useRef
|
|
6
|
+
} from "react";
|
|
7
|
+
|
|
8
|
+
// src/utils/index.ts
|
|
9
|
+
import { v4 as uuidv4 } from "uuid";
|
|
10
|
+
|
|
11
|
+
// src/utils/logger.ts
|
|
12
|
+
var Logger = class _Logger {
|
|
13
|
+
config;
|
|
14
|
+
constructor() {
|
|
15
|
+
this.config = this.initializeConfig();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Initialize logger configuration based on NODE_ENV
|
|
19
|
+
*/
|
|
20
|
+
initializeConfig() {
|
|
21
|
+
const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
22
|
+
let enabled = false;
|
|
23
|
+
let levels = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
|
|
24
|
+
if (isNode) {
|
|
25
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
26
|
+
enabled = nodeEnv !== "production";
|
|
27
|
+
const logLevelEnv = process.env.HYVE_SDK_LOG_LEVEL;
|
|
28
|
+
if (logLevelEnv) {
|
|
29
|
+
const configuredLevels = logLevelEnv.split(",").map((l) => l.trim());
|
|
30
|
+
levels = new Set(configuredLevels);
|
|
31
|
+
}
|
|
32
|
+
} else if (typeof window !== "undefined") {
|
|
33
|
+
try {
|
|
34
|
+
enabled = process?.env.NODE_ENV !== "production";
|
|
35
|
+
} catch (e) {
|
|
36
|
+
enabled = true;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const localStorageLogLevel = localStorage.getItem("HYVE_SDK_LOG_LEVEL");
|
|
40
|
+
if (localStorageLogLevel) {
|
|
41
|
+
const configuredLevels = localStorageLogLevel.split(",").map((l) => l.trim());
|
|
42
|
+
levels = new Set(configuredLevels);
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
enabled,
|
|
49
|
+
prefix: "[Hyve SDK]",
|
|
50
|
+
levels
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Set which log levels to display
|
|
55
|
+
*/
|
|
56
|
+
setLevels(levels) {
|
|
57
|
+
this.config.levels = new Set(levels);
|
|
58
|
+
if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
|
|
59
|
+
try {
|
|
60
|
+
localStorage.setItem("HYVE_SDK_LOG_LEVEL", levels.join(","));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if logging is enabled
|
|
67
|
+
*/
|
|
68
|
+
isEnabled() {
|
|
69
|
+
return this.config.enabled;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Internal log method
|
|
73
|
+
*/
|
|
74
|
+
log(level, ...args) {
|
|
75
|
+
if (!this.config.enabled || !this.config.levels.has(level)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
79
|
+
const prefix = `${this.config.prefix} [${level.toUpperCase()}] [${timestamp}]`;
|
|
80
|
+
switch (level) {
|
|
81
|
+
case "debug":
|
|
82
|
+
console.debug(prefix, ...args);
|
|
83
|
+
break;
|
|
84
|
+
case "info":
|
|
85
|
+
console.info(prefix, ...args);
|
|
86
|
+
break;
|
|
87
|
+
case "warn":
|
|
88
|
+
console.warn(prefix, ...args);
|
|
89
|
+
break;
|
|
90
|
+
case "error":
|
|
91
|
+
console.error(prefix, ...args);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Log a debug message
|
|
97
|
+
*/
|
|
98
|
+
debug(...args) {
|
|
99
|
+
this.log("debug", ...args);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Log an info message
|
|
103
|
+
*/
|
|
104
|
+
info(...args) {
|
|
105
|
+
this.log("info", ...args);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Log a warning message
|
|
109
|
+
*/
|
|
110
|
+
warn(...args) {
|
|
111
|
+
this.log("warn", ...args);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Log an error message
|
|
115
|
+
*/
|
|
116
|
+
error(...args) {
|
|
117
|
+
this.log("error", ...args);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Create a child logger with a specific prefix
|
|
121
|
+
*/
|
|
122
|
+
child(prefix) {
|
|
123
|
+
const childLogger = new _Logger();
|
|
124
|
+
childLogger.config = {
|
|
125
|
+
...this.config,
|
|
126
|
+
prefix: `${this.config.prefix} [${prefix}]`
|
|
127
|
+
};
|
|
128
|
+
return childLogger;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
var logger = new Logger();
|
|
132
|
+
|
|
133
|
+
// src/utils/auth.ts
|
|
134
|
+
function parseUrlParams(searchParams) {
|
|
135
|
+
const params = typeof searchParams === "string" ? new URLSearchParams(searchParams) : searchParams;
|
|
136
|
+
return {
|
|
137
|
+
gameStartTab: params.get("game_start_tab") || "",
|
|
138
|
+
platform: params.get("platform") || "",
|
|
139
|
+
hyveAccess: params.get("hyve-access") || "",
|
|
140
|
+
gameId: params.get("game-id") || ""
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/utils/native-bridge.ts
|
|
145
|
+
var NativeBridge = class {
|
|
146
|
+
static handlers = /* @__PURE__ */ new Map();
|
|
147
|
+
static isInitialized = false;
|
|
148
|
+
/**
|
|
149
|
+
* Checks if the app is running inside a React Native WebView
|
|
150
|
+
*/
|
|
151
|
+
static isNativeContext() {
|
|
152
|
+
return typeof window !== "undefined" && "ReactNativeWebView" in window;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Initializes the native bridge message listener
|
|
156
|
+
* Call this once when your app starts
|
|
157
|
+
*/
|
|
158
|
+
static initialize() {
|
|
159
|
+
if (this.isInitialized) {
|
|
160
|
+
logger.debug("[NativeBridge] Already initialized");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (typeof window === "undefined") {
|
|
164
|
+
logger.warn("[NativeBridge] Window not available, skipping initialization");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const boundHandler = this.handleNativeMessage.bind(this);
|
|
168
|
+
window.addEventListener("message", boundHandler);
|
|
169
|
+
document.addEventListener("message", boundHandler);
|
|
170
|
+
this.isInitialized = true;
|
|
171
|
+
logger.info("[NativeBridge] Initialized and listening for native messages");
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Handles incoming messages from React Native
|
|
175
|
+
*/
|
|
176
|
+
static handleNativeMessage(event) {
|
|
177
|
+
try {
|
|
178
|
+
let data;
|
|
179
|
+
if (typeof event.data === "string") {
|
|
180
|
+
try {
|
|
181
|
+
data = JSON.parse(event.data);
|
|
182
|
+
} catch {
|
|
183
|
+
logger.debug("[NativeBridge] Ignoring non-JSON message");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
} else if (typeof event.data === "object" && event.data !== null) {
|
|
187
|
+
data = event.data;
|
|
188
|
+
} else {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!data?.type) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const nativeResponseTypes = [
|
|
195
|
+
"IAP_AVAILABILITY_RESULT" /* IAP_AVAILABILITY_RESULT */,
|
|
196
|
+
"PUSH_PERMISSION_GRANTED" /* PUSH_PERMISSION_GRANTED */,
|
|
197
|
+
"PUSH_PERMISSION_DENIED" /* PUSH_PERMISSION_DENIED */,
|
|
198
|
+
"PRODUCTS_RESULT" /* PRODUCTS_RESULT */,
|
|
199
|
+
"PURCHASE_COMPLETE" /* PURCHASE_COMPLETE */,
|
|
200
|
+
"PURCHASE_ERROR" /* PURCHASE_ERROR */
|
|
201
|
+
];
|
|
202
|
+
if (!nativeResponseTypes.includes(data.type)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const handler = this.handlers.get(data.type);
|
|
206
|
+
if (handler) {
|
|
207
|
+
logger.debug(`[NativeBridge] Handling message: ${data.type}`);
|
|
208
|
+
handler(data.payload);
|
|
209
|
+
} else {
|
|
210
|
+
logger.warn(`[NativeBridge] No handler registered for: ${data.type}`);
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
logger.error("[NativeBridge] Error handling native message:", error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Sends a message to React Native
|
|
218
|
+
* @param type Message type
|
|
219
|
+
* @param payload Optional payload data
|
|
220
|
+
*/
|
|
221
|
+
static send(type, payload) {
|
|
222
|
+
if (!this.isNativeContext()) {
|
|
223
|
+
logger.debug(`[NativeBridge] Not in native context, skipping message: ${type}`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const message = { type, payload, timestamp: Date.now() };
|
|
228
|
+
window.ReactNativeWebView.postMessage(JSON.stringify(message));
|
|
229
|
+
logger.debug(`[NativeBridge] Sent message to native: ${type}`, payload);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
logger.error(`[NativeBridge] Error sending message to native:`, error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Registers a handler for messages from React Native
|
|
236
|
+
* @param type Message type to listen for
|
|
237
|
+
* @param handler Function to call when message is received
|
|
238
|
+
*/
|
|
239
|
+
static on(type, handler) {
|
|
240
|
+
this.handlers.set(type, handler);
|
|
241
|
+
logger.debug(`[NativeBridge] Registered handler for: ${type}`);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Unregisters a handler for a specific message type
|
|
245
|
+
* @param type Message type to stop listening for
|
|
246
|
+
*/
|
|
247
|
+
static off(type) {
|
|
248
|
+
this.handlers.delete(type);
|
|
249
|
+
logger.debug(`[NativeBridge] Unregistered handler for: ${type}`);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Clears all registered handlers
|
|
253
|
+
*/
|
|
254
|
+
static clearHandlers() {
|
|
255
|
+
this.handlers.clear();
|
|
256
|
+
logger.debug("[NativeBridge] Cleared all handlers");
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Checks if In-App Purchases are available on the device
|
|
260
|
+
* The native app will respond with a message containing availability status
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* // Listen for the response
|
|
264
|
+
* NativeBridge.on("IAP_AVAILABILITY_RESULT", (payload) => {
|
|
265
|
+
* console.log("IAP available:", payload.available);
|
|
266
|
+
* });
|
|
267
|
+
*
|
|
268
|
+
* // Send the request
|
|
269
|
+
* NativeBridge.checkIAPAvailability();
|
|
270
|
+
*/
|
|
271
|
+
static checkIAPAvailability() {
|
|
272
|
+
this.send("CHECK_IAP_AVAILABILITY" /* CHECK_IAP_AVAILABILITY */);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Requests notification permission from the native app
|
|
276
|
+
* The native app will respond with PUSH_PERMISSION_GRANTED or PUSH_PERMISSION_DENIED
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* // Listen for the response
|
|
280
|
+
* NativeBridge.on("PUSH_PERMISSION_GRANTED", () => {
|
|
281
|
+
* console.log("Permission granted");
|
|
282
|
+
* });
|
|
283
|
+
*
|
|
284
|
+
* NativeBridge.on("PUSH_PERMISSION_DENIED", () => {
|
|
285
|
+
* console.log("Permission denied");
|
|
286
|
+
* });
|
|
287
|
+
*
|
|
288
|
+
* // Send the request
|
|
289
|
+
* NativeBridge.requestNotificationPermission();
|
|
290
|
+
*/
|
|
291
|
+
static requestNotificationPermission() {
|
|
292
|
+
this.send("REQUEST_NOTIFICATION_PERMISSION" /* REQUEST_NOTIFICATION_PERMISSION */);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Requests available products for a specific game
|
|
296
|
+
* The native app will respond with PRODUCTS_RESULT containing product details
|
|
297
|
+
*
|
|
298
|
+
* @param gameId - The game ID to fetch products for
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* // Listen for the response
|
|
302
|
+
* NativeBridge.on("PRODUCTS_RESULT", (payload) => {
|
|
303
|
+
* console.log("Products:", payload.products);
|
|
304
|
+
* });
|
|
305
|
+
*
|
|
306
|
+
* // Send the request
|
|
307
|
+
* NativeBridge.getProducts(123);
|
|
308
|
+
*/
|
|
309
|
+
static getProducts(gameId) {
|
|
310
|
+
this.send("GET_PRODUCTS" /* GET_PRODUCTS */, { gameId });
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Initiates a purchase for a specific product
|
|
314
|
+
* The native app will respond with PURCHASE_COMPLETE or PURCHASE_ERROR
|
|
315
|
+
*
|
|
316
|
+
* @param productId - The product ID to purchase
|
|
317
|
+
* @param userId - The user ID making the purchase
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* // Listen for responses
|
|
321
|
+
* NativeBridge.on("PURCHASE_COMPLETE", (payload) => {
|
|
322
|
+
* console.log("Purchase successful:", payload.productId);
|
|
323
|
+
* });
|
|
324
|
+
*
|
|
325
|
+
* NativeBridge.on("PURCHASE_ERROR", (payload) => {
|
|
326
|
+
* console.error("Purchase failed:", payload.error);
|
|
327
|
+
* });
|
|
328
|
+
*
|
|
329
|
+
* // Initiate purchase
|
|
330
|
+
* NativeBridge.purchase("product_123", "user_456");
|
|
331
|
+
*/
|
|
332
|
+
static purchase(productId, userId) {
|
|
333
|
+
this.send("PURCHASE" /* PURCHASE */, { productId, userId });
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// src/utils/jwt.ts
|
|
338
|
+
function decodeJwt(token) {
|
|
339
|
+
try {
|
|
340
|
+
const parts = token.split(".");
|
|
341
|
+
if (parts.length !== 3) {
|
|
342
|
+
logger.warn("Invalid JWT format - expected 3 parts");
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const payload = parts[1];
|
|
346
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
347
|
+
const jsonPayload = atob(base64);
|
|
348
|
+
const decoded = JSON.parse(jsonPayload);
|
|
349
|
+
logger.debug("JWT decoded successfully:", decoded);
|
|
350
|
+
return decoded;
|
|
351
|
+
} catch (error) {
|
|
352
|
+
logger.error("Failed to decode JWT:", error);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function extractUserIdFromJwt(token) {
|
|
357
|
+
const payload = decodeJwt(token);
|
|
358
|
+
if (!payload) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const userId = payload.sub || payload.user_id?.toString();
|
|
362
|
+
if (userId) {
|
|
363
|
+
logger.info("Extracted user ID from JWT:", userId);
|
|
364
|
+
return userId;
|
|
365
|
+
}
|
|
366
|
+
logger.warn("No user ID found in JWT payload");
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
function extractGameIdFromJwt(token) {
|
|
370
|
+
const payload = decodeJwt(token);
|
|
371
|
+
if (!payload) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
const gameId = payload.game_id;
|
|
375
|
+
if (typeof gameId === "number") {
|
|
376
|
+
logger.info("Extracted game ID from JWT:", gameId);
|
|
377
|
+
return gameId;
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/utils/index.ts
|
|
383
|
+
function generateUUID() {
|
|
384
|
+
return uuidv4();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/services/ads.ts
|
|
388
|
+
var AdsService = class {
|
|
389
|
+
config = {
|
|
390
|
+
sound: "on",
|
|
391
|
+
debug: false,
|
|
392
|
+
onBeforeAd: () => {
|
|
393
|
+
},
|
|
394
|
+
onAfterAd: () => {
|
|
395
|
+
},
|
|
396
|
+
onRewardEarned: () => {
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
// Cached init promise — ensures adConfig() is only called once
|
|
400
|
+
initPromise = null;
|
|
401
|
+
ready = false;
|
|
402
|
+
/**
|
|
403
|
+
* Optionally configure the ads service.
|
|
404
|
+
* Not required — ads work without calling this.
|
|
405
|
+
*/
|
|
406
|
+
configure(config) {
|
|
407
|
+
this.config = {
|
|
408
|
+
...this.config,
|
|
409
|
+
...config,
|
|
410
|
+
onBeforeAd: config.onBeforeAd ?? this.config.onBeforeAd,
|
|
411
|
+
onAfterAd: config.onAfterAd ?? this.config.onAfterAd,
|
|
412
|
+
onRewardEarned: config.onRewardEarned ?? this.config.onRewardEarned
|
|
413
|
+
};
|
|
414
|
+
if (this.config.debug) {
|
|
415
|
+
logger.debug("[AdsService] Configuration updated:", { sound: this.config.sound });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Initialize the Google H5 Ads system.
|
|
420
|
+
* Returns a cached promise — safe to call multiple times.
|
|
421
|
+
*/
|
|
422
|
+
initialize() {
|
|
423
|
+
if (this.initPromise) return this.initPromise;
|
|
424
|
+
this.initPromise = new Promise((resolve) => {
|
|
425
|
+
if (!window.adConfig || !window.adBreak) {
|
|
426
|
+
if (this.config.debug) {
|
|
427
|
+
logger.debug("[AdsService] Google Ads SDK not found \u2014 ads unavailable");
|
|
428
|
+
}
|
|
429
|
+
resolve(false);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (this.config.debug) {
|
|
433
|
+
logger.debug("[AdsService] Initializing ads system...");
|
|
434
|
+
}
|
|
435
|
+
window.adConfig({
|
|
436
|
+
sound: this.config.sound,
|
|
437
|
+
preloadAdBreaks: "on",
|
|
438
|
+
onReady: () => {
|
|
439
|
+
this.ready = true;
|
|
440
|
+
if (this.config.debug) {
|
|
441
|
+
logger.debug("[AdsService] Ads ready");
|
|
442
|
+
}
|
|
443
|
+
resolve(true);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
setTimeout(() => resolve(false), 5e3);
|
|
447
|
+
});
|
|
448
|
+
return this.initPromise;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Show an ad. Auto-initializes on the first call.
|
|
452
|
+
* Returns immediately with success: false if ads are disabled or unavailable.
|
|
453
|
+
*/
|
|
454
|
+
async show(type) {
|
|
455
|
+
const requestedAt = Date.now();
|
|
456
|
+
const ready = await this.initialize();
|
|
457
|
+
if (!ready || !window.adBreak) {
|
|
458
|
+
return {
|
|
459
|
+
success: false,
|
|
460
|
+
type,
|
|
461
|
+
error: new Error("Ads not available"),
|
|
462
|
+
requestedAt,
|
|
463
|
+
completedAt: Date.now()
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
return this.showAdBreak(type);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Show an ad break via the Google H5 API.
|
|
470
|
+
*/
|
|
471
|
+
async showAdBreak(type) {
|
|
472
|
+
const requestedAt = Date.now();
|
|
473
|
+
return new Promise((resolve) => {
|
|
474
|
+
const googleType = type === "rewarded" ? "reward" : type === "preroll" ? "start" : "next";
|
|
475
|
+
const adName = `${type}-ad-${Date.now()}`;
|
|
476
|
+
if (this.config.debug) {
|
|
477
|
+
logger.debug(`[AdsService] Showing ${type} ad`);
|
|
478
|
+
}
|
|
479
|
+
this.config.onBeforeAd(type);
|
|
480
|
+
const adBreakConfig = {
|
|
481
|
+
type: googleType,
|
|
482
|
+
name: adName,
|
|
483
|
+
afterAd: () => {
|
|
484
|
+
this.config.onAfterAd(type);
|
|
485
|
+
},
|
|
486
|
+
adBreakDone: (info) => {
|
|
487
|
+
const completedAt = Date.now();
|
|
488
|
+
const success = type === "rewarded" ? info?.breakStatus === "viewed" : info?.breakStatus !== "error";
|
|
489
|
+
const error = info?.breakStatus === "error" && info?.error ? new Error(info.error) : void 0;
|
|
490
|
+
if (this.config.debug) {
|
|
491
|
+
logger.debug("[AdsService] Ad break done:", { success, status: info?.breakStatus });
|
|
492
|
+
}
|
|
493
|
+
resolve({ success, type, error, requestedAt, completedAt });
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
if (type === "rewarded") {
|
|
497
|
+
adBreakConfig.beforeReward = (showAdFn) => showAdFn();
|
|
498
|
+
adBreakConfig.adViewed = () => this.config.onRewardEarned();
|
|
499
|
+
}
|
|
500
|
+
window.adBreak(adBreakConfig);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Returns the configured ad lifecycle callbacks.
|
|
505
|
+
* Used by platform-specific providers (e.g. Playgama) to fire the same hooks.
|
|
506
|
+
*/
|
|
507
|
+
getCallbacks() {
|
|
508
|
+
return {
|
|
509
|
+
onBeforeAd: this.config.onBeforeAd,
|
|
510
|
+
onAfterAd: this.config.onAfterAd,
|
|
511
|
+
onRewardEarned: this.config.onRewardEarned
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Check if ads have successfully initialized and are ready to show.
|
|
516
|
+
*/
|
|
517
|
+
isReady() {
|
|
518
|
+
return this.ready;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// src/services/playgama.ts
|
|
523
|
+
var PLAYGAMA_BRIDGE_CDN = "https://bridge.playgama.com/v1/stable/playgama-bridge.js";
|
|
524
|
+
var PlaygamaService = class {
|
|
525
|
+
initialized = false;
|
|
526
|
+
/**
|
|
527
|
+
* Detects if the game is running on the Playgama platform.
|
|
528
|
+
* Playgama injects a `platform_id=playgama` URL parameter into the game URL.
|
|
529
|
+
* Falls back to checking document.referrer for playgama.com.
|
|
530
|
+
*/
|
|
531
|
+
static isPlaygamaDomain() {
|
|
532
|
+
try {
|
|
533
|
+
const url = new URL(window.location.href);
|
|
534
|
+
if (url.searchParams.get("platform_id") === "playgama") {
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
if (document.referrer.includes("playgama.com")) {
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
if (window !== window.top) {
|
|
541
|
+
try {
|
|
542
|
+
if (window.parent.location.hostname.includes("playgama.com")) {
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return false;
|
|
549
|
+
} catch {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Loads the Playgama Bridge script from CDN and initializes it.
|
|
555
|
+
* Safe to call multiple times - resolves immediately if already initialized.
|
|
556
|
+
*/
|
|
557
|
+
async initialize() {
|
|
558
|
+
if (this.initialized) return true;
|
|
559
|
+
try {
|
|
560
|
+
await this.loadScript();
|
|
561
|
+
await window.bridge.initialize();
|
|
562
|
+
this.initialized = true;
|
|
563
|
+
return true;
|
|
564
|
+
} catch (error) {
|
|
565
|
+
logger.warn("[PlaygamaService] Failed to initialize:", error);
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
isInitialized() {
|
|
570
|
+
return this.initialized;
|
|
571
|
+
}
|
|
572
|
+
async showInterstitial(callbacks) {
|
|
573
|
+
const requestedAt = Date.now();
|
|
574
|
+
const bridge = window.bridge;
|
|
575
|
+
if (!this.initialized || !bridge) {
|
|
576
|
+
return {
|
|
577
|
+
success: false,
|
|
578
|
+
type: "interstitial",
|
|
579
|
+
error: new Error("Playgama not initialized"),
|
|
580
|
+
requestedAt,
|
|
581
|
+
completedAt: Date.now()
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
if (!bridge.advertisement.isInterstitialSupported) {
|
|
585
|
+
return {
|
|
586
|
+
success: false,
|
|
587
|
+
type: "interstitial",
|
|
588
|
+
error: new Error("Interstitial ads not supported on this platform"),
|
|
589
|
+
requestedAt,
|
|
590
|
+
completedAt: Date.now()
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
return new Promise((resolve) => {
|
|
594
|
+
const handler = (state) => {
|
|
595
|
+
if (state === "opened") {
|
|
596
|
+
callbacks?.onBeforeAd?.();
|
|
597
|
+
} else if (state === "closed") {
|
|
598
|
+
bridge.advertisement.off(bridge.EVENT_NAME.INTERSTITIAL_STATE_CHANGED, handler);
|
|
599
|
+
callbacks?.onAfterAd?.();
|
|
600
|
+
resolve({ success: true, type: "interstitial", requestedAt, completedAt: Date.now() });
|
|
601
|
+
} else if (state === "failed") {
|
|
602
|
+
bridge.advertisement.off(bridge.EVENT_NAME.INTERSTITIAL_STATE_CHANGED, handler);
|
|
603
|
+
callbacks?.onAfterAd?.();
|
|
604
|
+
resolve({
|
|
605
|
+
success: false,
|
|
606
|
+
type: "interstitial",
|
|
607
|
+
error: new Error("Interstitial ad failed"),
|
|
608
|
+
requestedAt,
|
|
609
|
+
completedAt: Date.now()
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
bridge.advertisement.on(bridge.EVENT_NAME.INTERSTITIAL_STATE_CHANGED, handler);
|
|
614
|
+
bridge.advertisement.showInterstitial();
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
async showRewarded(callbacks) {
|
|
618
|
+
const requestedAt = Date.now();
|
|
619
|
+
const bridge = window.bridge;
|
|
620
|
+
if (!this.initialized || !bridge) {
|
|
621
|
+
return {
|
|
622
|
+
success: false,
|
|
623
|
+
type: "rewarded",
|
|
624
|
+
error: new Error("Playgama not initialized"),
|
|
625
|
+
requestedAt,
|
|
626
|
+
completedAt: Date.now()
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
if (!bridge.advertisement.isRewardedSupported) {
|
|
630
|
+
return {
|
|
631
|
+
success: false,
|
|
632
|
+
type: "rewarded",
|
|
633
|
+
error: new Error("Rewarded ads not supported on this platform"),
|
|
634
|
+
requestedAt,
|
|
635
|
+
completedAt: Date.now()
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
return new Promise((resolve) => {
|
|
639
|
+
let rewarded = false;
|
|
640
|
+
const handler = (state) => {
|
|
641
|
+
if (state === "opened") {
|
|
642
|
+
callbacks?.onBeforeAd?.();
|
|
643
|
+
} else if (state === "rewarded") {
|
|
644
|
+
rewarded = true;
|
|
645
|
+
callbacks?.onRewardEarned?.();
|
|
646
|
+
} else if (state === "closed" || state === "failed") {
|
|
647
|
+
bridge.advertisement.off(bridge.EVENT_NAME.REWARDED_STATE_CHANGED, handler);
|
|
648
|
+
callbacks?.onAfterAd?.();
|
|
649
|
+
resolve({
|
|
650
|
+
success: rewarded,
|
|
651
|
+
type: "rewarded",
|
|
652
|
+
error: !rewarded ? new Error("Rewarded ad not completed") : void 0,
|
|
653
|
+
requestedAt,
|
|
654
|
+
completedAt: Date.now()
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
bridge.advertisement.on(bridge.EVENT_NAME.REWARDED_STATE_CHANGED, handler);
|
|
659
|
+
bridge.advertisement.showRewarded();
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
loadScript() {
|
|
663
|
+
return new Promise((resolve, reject) => {
|
|
664
|
+
if (window.bridge) {
|
|
665
|
+
resolve();
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const script = document.createElement("script");
|
|
669
|
+
script.src = PLAYGAMA_BRIDGE_CDN;
|
|
670
|
+
script.onload = () => resolve();
|
|
671
|
+
script.onerror = () => reject(new Error("Failed to load Playgama Bridge script"));
|
|
672
|
+
document.head.appendChild(script);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
// src/services/crazygames.ts
|
|
678
|
+
var CRAZYGAMES_SDK_CDN = "https://sdk.crazygames.com/crazygames-sdk-v2.js";
|
|
679
|
+
var CrazyGamesService = class {
|
|
680
|
+
initialized = false;
|
|
681
|
+
/**
|
|
682
|
+
* Detects if the game is running on the CrazyGames platform.
|
|
683
|
+
* Games on CrazyGames run inside an iframe, so we check document.referrer
|
|
684
|
+
* and attempt to read the parent frame location.
|
|
685
|
+
*/
|
|
686
|
+
static isCrazyGamesDomain() {
|
|
687
|
+
try {
|
|
688
|
+
if (document.referrer.includes("crazygames.com")) {
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
if (window !== window.top) {
|
|
692
|
+
try {
|
|
693
|
+
if (window.parent.location.hostname.includes("crazygames.com")) {
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
} catch {
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return false;
|
|
700
|
+
} catch {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Loads the CrazyGames SDK from CDN and confirms the environment is 'crazygames'.
|
|
706
|
+
* Safe to call multiple times — resolves immediately if already initialized.
|
|
707
|
+
*/
|
|
708
|
+
async initialize() {
|
|
709
|
+
if (this.initialized) return true;
|
|
710
|
+
try {
|
|
711
|
+
await this.loadScript();
|
|
712
|
+
const sdk = window.CrazyGames?.SDK;
|
|
713
|
+
if (!sdk) {
|
|
714
|
+
logger.warn("[CrazyGamesService] SDK not found after script load");
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
const env = await sdk.getEnvironment();
|
|
718
|
+
if (env !== "crazygames" && env !== "local") {
|
|
719
|
+
logger.warn("[CrazyGamesService] Unexpected environment:", env);
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
this.initialized = true;
|
|
723
|
+
return true;
|
|
724
|
+
} catch (error) {
|
|
725
|
+
logger.warn("[CrazyGamesService] Failed to initialize:", error);
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
isInitialized() {
|
|
730
|
+
return this.initialized;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Shows a midgame (interstitial) ad via the CrazyGames SDK.
|
|
734
|
+
*/
|
|
735
|
+
async showInterstitial(callbacks) {
|
|
736
|
+
const requestedAt = Date.now();
|
|
737
|
+
const sdk = window.CrazyGames?.SDK;
|
|
738
|
+
if (!this.initialized || !sdk) {
|
|
739
|
+
return {
|
|
740
|
+
success: false,
|
|
741
|
+
type: "interstitial",
|
|
742
|
+
error: new Error("CrazyGames SDK not initialized"),
|
|
743
|
+
requestedAt,
|
|
744
|
+
completedAt: Date.now()
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
return new Promise((resolve) => {
|
|
748
|
+
sdk.ad.requestAd("midgame", {
|
|
749
|
+
adStarted: () => {
|
|
750
|
+
callbacks?.onBeforeAd?.();
|
|
751
|
+
},
|
|
752
|
+
adFinished: () => {
|
|
753
|
+
callbacks?.onAfterAd?.();
|
|
754
|
+
resolve({ success: true, type: "interstitial", requestedAt, completedAt: Date.now() });
|
|
755
|
+
},
|
|
756
|
+
adError: (error) => {
|
|
757
|
+
callbacks?.onAfterAd?.();
|
|
758
|
+
resolve({
|
|
759
|
+
success: false,
|
|
760
|
+
type: "interstitial",
|
|
761
|
+
error: new Error(`CrazyGames interstitial ad error: ${error}`),
|
|
762
|
+
requestedAt,
|
|
763
|
+
completedAt: Date.now()
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Shows a rewarded ad via the CrazyGames SDK.
|
|
771
|
+
* Resolves with success: true only when adFinished fires (ad was fully watched).
|
|
772
|
+
*/
|
|
773
|
+
async showRewarded(callbacks) {
|
|
774
|
+
const requestedAt = Date.now();
|
|
775
|
+
const sdk = window.CrazyGames?.SDK;
|
|
776
|
+
if (!this.initialized || !sdk) {
|
|
777
|
+
return {
|
|
778
|
+
success: false,
|
|
779
|
+
type: "rewarded",
|
|
780
|
+
error: new Error("CrazyGames SDK not initialized"),
|
|
781
|
+
requestedAt,
|
|
782
|
+
completedAt: Date.now()
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
return new Promise((resolve) => {
|
|
786
|
+
sdk.ad.requestAd("rewarded", {
|
|
787
|
+
adStarted: () => {
|
|
788
|
+
callbacks?.onBeforeAd?.();
|
|
789
|
+
},
|
|
790
|
+
adFinished: () => {
|
|
791
|
+
callbacks?.onRewardEarned?.();
|
|
792
|
+
callbacks?.onAfterAd?.();
|
|
793
|
+
resolve({ success: true, type: "rewarded", requestedAt, completedAt: Date.now() });
|
|
794
|
+
},
|
|
795
|
+
adError: (error) => {
|
|
796
|
+
callbacks?.onAfterAd?.();
|
|
797
|
+
resolve({
|
|
798
|
+
success: false,
|
|
799
|
+
type: "rewarded",
|
|
800
|
+
error: new Error(`CrazyGames rewarded ad error: ${error}`),
|
|
801
|
+
requestedAt,
|
|
802
|
+
completedAt: Date.now()
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Notifies CrazyGames that gameplay has started.
|
|
810
|
+
* Call when the player actively begins or resumes playing.
|
|
811
|
+
*/
|
|
812
|
+
gameplayStart() {
|
|
813
|
+
if (!this.initialized) return;
|
|
814
|
+
window.CrazyGames?.SDK.game.gameplayStart();
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Notifies CrazyGames that gameplay has stopped.
|
|
818
|
+
* Call during menu access, level completion, or pausing.
|
|
819
|
+
*/
|
|
820
|
+
gameplayStop() {
|
|
821
|
+
if (!this.initialized) return;
|
|
822
|
+
window.CrazyGames?.SDK.game.gameplayStop();
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Triggers a celebration effect on the CrazyGames website.
|
|
826
|
+
* Use sparingly for significant achievements (boss defeat, personal record, etc.)
|
|
827
|
+
*/
|
|
828
|
+
happytime() {
|
|
829
|
+
if (!this.initialized) return;
|
|
830
|
+
window.CrazyGames?.SDK.game.happytime();
|
|
831
|
+
}
|
|
832
|
+
loadScript() {
|
|
833
|
+
return new Promise((resolve, reject) => {
|
|
834
|
+
if (window.CrazyGames?.SDK) {
|
|
835
|
+
resolve();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const script = document.createElement("script");
|
|
839
|
+
script.src = CRAZYGAMES_SDK_CDN;
|
|
840
|
+
script.onload = () => resolve();
|
|
841
|
+
script.onerror = () => reject(new Error("Failed to load CrazyGames SDK script"));
|
|
842
|
+
document.head.appendChild(script);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
// src/services/billing.ts
|
|
848
|
+
var BillingService = class {
|
|
849
|
+
config;
|
|
850
|
+
platform = "unknown" /* UNKNOWN */;
|
|
851
|
+
initPromise = null;
|
|
852
|
+
nativeAvailable = false;
|
|
853
|
+
// Stripe instance for web payments
|
|
854
|
+
stripe = null;
|
|
855
|
+
checkoutElement = null;
|
|
856
|
+
// Callbacks for purchase events
|
|
857
|
+
onPurchaseCompleteCallback;
|
|
858
|
+
onPurchaseErrorCallback;
|
|
859
|
+
constructor(config) {
|
|
860
|
+
this.config = config;
|
|
861
|
+
this.detectPlatform();
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Update billing configuration. Resets initialization so next call re-inits with new config.
|
|
865
|
+
*/
|
|
866
|
+
configure(config) {
|
|
867
|
+
this.config = {
|
|
868
|
+
...this.config,
|
|
869
|
+
...config
|
|
870
|
+
};
|
|
871
|
+
this.initPromise = null;
|
|
872
|
+
logger.info("[BillingService] Configuration updated");
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Detects if running on web or native platform
|
|
876
|
+
*/
|
|
877
|
+
detectPlatform() {
|
|
878
|
+
const isNative = NativeBridge.isNativeContext();
|
|
879
|
+
const hasWindow = typeof window !== "undefined";
|
|
880
|
+
if (isNative) {
|
|
881
|
+
this.platform = "native" /* NATIVE */;
|
|
882
|
+
logger.info("[BillingService] Platform: NATIVE");
|
|
883
|
+
} else if (hasWindow) {
|
|
884
|
+
this.platform = "web" /* WEB */;
|
|
885
|
+
logger.info("[BillingService] Platform: WEB");
|
|
886
|
+
} else {
|
|
887
|
+
this.platform = "unknown" /* UNKNOWN */;
|
|
888
|
+
logger.warn("[BillingService] Platform: UNKNOWN");
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Get the current platform
|
|
893
|
+
*/
|
|
894
|
+
getPlatform() {
|
|
895
|
+
return this.platform;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Initialize the billing service. Idempotent — returns cached promise on subsequent calls.
|
|
899
|
+
* Called automatically by getProducts() and purchase().
|
|
900
|
+
*/
|
|
901
|
+
initialize() {
|
|
902
|
+
if (this.initPromise) return this.initPromise;
|
|
903
|
+
logger.info(`[BillingService] Initializing for ${this.platform} platform...`);
|
|
904
|
+
this.initPromise = (async () => {
|
|
905
|
+
try {
|
|
906
|
+
if (this.platform === "native" /* NATIVE */) {
|
|
907
|
+
return await this.initializeNative();
|
|
908
|
+
} else if (this.platform === "web" /* WEB */) {
|
|
909
|
+
return await this.initializeWeb();
|
|
910
|
+
}
|
|
911
|
+
logger.error("[BillingService] Cannot initialize: unknown platform");
|
|
912
|
+
return false;
|
|
913
|
+
} catch (error) {
|
|
914
|
+
logger.error("[BillingService] Initialization failed:", error?.message);
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
})();
|
|
918
|
+
return this.initPromise;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Initialize native billing
|
|
922
|
+
*/
|
|
923
|
+
async initializeNative() {
|
|
924
|
+
return new Promise((resolve) => {
|
|
925
|
+
let resolved = false;
|
|
926
|
+
try {
|
|
927
|
+
NativeBridge.initialize();
|
|
928
|
+
NativeBridge.on(
|
|
929
|
+
"PURCHASE_COMPLETE" /* PURCHASE_COMPLETE */,
|
|
930
|
+
(payload) => {
|
|
931
|
+
logger.info("[BillingService] Purchase complete:", payload.productId);
|
|
932
|
+
const result = {
|
|
933
|
+
success: true,
|
|
934
|
+
productId: payload.productId,
|
|
935
|
+
transactionId: payload.transactionId,
|
|
936
|
+
transactionDate: payload.transactionDate
|
|
937
|
+
};
|
|
938
|
+
if (this.onPurchaseCompleteCallback) {
|
|
939
|
+
this.onPurchaseCompleteCallback(result);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
);
|
|
943
|
+
NativeBridge.on(
|
|
944
|
+
"PURCHASE_ERROR" /* PURCHASE_ERROR */,
|
|
945
|
+
(payload) => {
|
|
946
|
+
logger.error("[BillingService] Purchase error:", payload.message);
|
|
947
|
+
const result = {
|
|
948
|
+
success: false,
|
|
949
|
+
productId: payload.productId || "unknown",
|
|
950
|
+
error: {
|
|
951
|
+
code: payload.code,
|
|
952
|
+
message: payload.message
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
if (this.onPurchaseErrorCallback) {
|
|
956
|
+
this.onPurchaseErrorCallback(result);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
);
|
|
960
|
+
NativeBridge.on(
|
|
961
|
+
"IAP_AVAILABILITY_RESULT" /* IAP_AVAILABILITY_RESULT */,
|
|
962
|
+
(payload) => {
|
|
963
|
+
this.nativeAvailable = payload.available;
|
|
964
|
+
logger.info(`[BillingService] Native billing ${payload.available ? "available" : "unavailable"}`);
|
|
965
|
+
resolved = true;
|
|
966
|
+
resolve(payload.available);
|
|
967
|
+
}
|
|
968
|
+
);
|
|
969
|
+
setTimeout(() => {
|
|
970
|
+
NativeBridge.checkIAPAvailability();
|
|
971
|
+
}, 100);
|
|
972
|
+
setTimeout(() => {
|
|
973
|
+
if (!resolved) {
|
|
974
|
+
logger.warn("[BillingService] Native initialization timeout");
|
|
975
|
+
resolved = true;
|
|
976
|
+
resolve(false);
|
|
977
|
+
}
|
|
978
|
+
}, 5e3);
|
|
979
|
+
} catch (error) {
|
|
980
|
+
logger.error("[BillingService] Native initialization failed:", error?.message);
|
|
981
|
+
resolved = true;
|
|
982
|
+
resolve(false);
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Initialize web billing (Stripe)
|
|
988
|
+
*/
|
|
989
|
+
async initializeWeb() {
|
|
990
|
+
if (!this.config.stripePublishableKey) {
|
|
991
|
+
logger.error("[BillingService] Stripe publishable key not provided");
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
if (typeof window === "undefined") {
|
|
996
|
+
logger.error("[BillingService] Window is undefined (not in browser)");
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
if (!window.Stripe) {
|
|
1000
|
+
await this.loadStripeScript();
|
|
1001
|
+
}
|
|
1002
|
+
if (window.Stripe) {
|
|
1003
|
+
this.stripe = window.Stripe(this.config.stripePublishableKey);
|
|
1004
|
+
logger.info("[BillingService] Web billing initialized");
|
|
1005
|
+
return true;
|
|
1006
|
+
} else {
|
|
1007
|
+
logger.error("[BillingService] Stripe not available after loading");
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
logger.error("[BillingService] Stripe initialization failed:", error?.message);
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Load Stripe.js script dynamically
|
|
1017
|
+
*/
|
|
1018
|
+
loadStripeScript() {
|
|
1019
|
+
return new Promise((resolve, reject) => {
|
|
1020
|
+
if (typeof window === "undefined") {
|
|
1021
|
+
reject(new Error("Window is not defined"));
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (window.Stripe) {
|
|
1025
|
+
resolve();
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if (!document || !document.head) {
|
|
1029
|
+
reject(new Error("document is undefined"));
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const script = document.createElement("script");
|
|
1033
|
+
script.src = "https://js.stripe.com/v3/";
|
|
1034
|
+
script.async = true;
|
|
1035
|
+
script.onload = () => {
|
|
1036
|
+
logger.info("[BillingService] Stripe.js loaded");
|
|
1037
|
+
resolve();
|
|
1038
|
+
};
|
|
1039
|
+
script.onerror = () => {
|
|
1040
|
+
logger.error("[BillingService] Failed to load Stripe.js");
|
|
1041
|
+
reject(new Error("Failed to load Stripe.js"));
|
|
1042
|
+
};
|
|
1043
|
+
try {
|
|
1044
|
+
document.head.appendChild(script);
|
|
1045
|
+
} catch (appendError) {
|
|
1046
|
+
reject(appendError);
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Check if billing is available based on current config and platform state
|
|
1052
|
+
*/
|
|
1053
|
+
isAvailable() {
|
|
1054
|
+
if (this.platform === "native" /* NATIVE */) {
|
|
1055
|
+
return this.nativeAvailable;
|
|
1056
|
+
} else if (this.platform === "web" /* WEB */) {
|
|
1057
|
+
return !!this.config.stripePublishableKey;
|
|
1058
|
+
}
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Get available products. Auto-initializes on first call.
|
|
1063
|
+
*/
|
|
1064
|
+
async getProducts() {
|
|
1065
|
+
await this.initialize();
|
|
1066
|
+
if (this.platform === "native" /* NATIVE */) {
|
|
1067
|
+
return await this.getProductsNative();
|
|
1068
|
+
} else if (this.platform === "web" /* WEB */) {
|
|
1069
|
+
return await this.getProductsWeb();
|
|
1070
|
+
}
|
|
1071
|
+
throw new Error("Cannot get products: unknown platform");
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Get products from native IAP
|
|
1075
|
+
*/
|
|
1076
|
+
async getProductsNative() {
|
|
1077
|
+
if (!this.config.gameId || !this.config.checkoutUrl) {
|
|
1078
|
+
const error = new Error("gameId and checkoutUrl required for native purchases");
|
|
1079
|
+
logger.error("[BillingService]", error.message);
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
const response = await fetch(
|
|
1083
|
+
`${this.config.checkoutUrl}/get-native-packages?game_id=${this.config.gameId}`
|
|
1084
|
+
);
|
|
1085
|
+
if (!response.ok) {
|
|
1086
|
+
throw new Error(`Failed to fetch native products: ${response.status}`);
|
|
1087
|
+
}
|
|
1088
|
+
const data = await response.json();
|
|
1089
|
+
if (!data.packages || !Array.isArray(data.packages)) {
|
|
1090
|
+
throw new Error("Invalid response format: missing packages array");
|
|
1091
|
+
}
|
|
1092
|
+
const products = data.packages.map((pkg) => ({
|
|
1093
|
+
productId: pkg.productId,
|
|
1094
|
+
title: pkg.package_name,
|
|
1095
|
+
description: `${pkg.game_name} - ${pkg.package_name}`,
|
|
1096
|
+
price: pkg.price_cents / 100,
|
|
1097
|
+
localizedPrice: pkg.price_display,
|
|
1098
|
+
currency: "USD"
|
|
1099
|
+
}));
|
|
1100
|
+
logger.info(`[BillingService] Fetched ${products.length} native products`);
|
|
1101
|
+
return products;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Get products from web API (Stripe)
|
|
1105
|
+
*/
|
|
1106
|
+
async getProductsWeb() {
|
|
1107
|
+
if (!this.config.checkoutUrl || !this.config.gameId) {
|
|
1108
|
+
const error = new Error("checkoutUrl and gameId required for web purchases");
|
|
1109
|
+
logger.error("[BillingService]", error.message);
|
|
1110
|
+
throw error;
|
|
1111
|
+
}
|
|
1112
|
+
try {
|
|
1113
|
+
const url = `${this.config.checkoutUrl}/get-packages?game_id=${this.config.gameId}`;
|
|
1114
|
+
const response = await fetch(url);
|
|
1115
|
+
if (!response.ok) {
|
|
1116
|
+
throw new Error(`Failed to fetch web products: ${response.status}`);
|
|
1117
|
+
}
|
|
1118
|
+
const data = await response.json();
|
|
1119
|
+
if (!data.packages || !Array.isArray(data.packages)) {
|
|
1120
|
+
throw new Error("Invalid response format: missing packages array");
|
|
1121
|
+
}
|
|
1122
|
+
const products = data.packages.map((pkg) => ({
|
|
1123
|
+
productId: pkg.priceId || pkg.productId,
|
|
1124
|
+
// Prefer priceId for Stripe
|
|
1125
|
+
title: pkg.package_name,
|
|
1126
|
+
description: `${pkg.game_name} - ${pkg.package_name}`,
|
|
1127
|
+
price: pkg.price_cents / 100,
|
|
1128
|
+
localizedPrice: pkg.price_display,
|
|
1129
|
+
currency: "USD"
|
|
1130
|
+
}));
|
|
1131
|
+
logger.info(`[BillingService] Fetched ${products.length} web products`);
|
|
1132
|
+
return products;
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
logger.error("[BillingService] Failed to fetch web products:", error?.message);
|
|
1135
|
+
throw error;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Purchase a product. Auto-initializes on first call.
|
|
1140
|
+
* @param productId - The product ID (priceId for web/Stripe, productId for native)
|
|
1141
|
+
* @param options - Optional purchase options
|
|
1142
|
+
* @param options.elementId - For web: DOM element ID to mount Stripe checkout (default: 'stripe-checkout-element')
|
|
1143
|
+
*/
|
|
1144
|
+
async purchase(productId, options) {
|
|
1145
|
+
await this.initialize();
|
|
1146
|
+
if (!this.isAvailable()) {
|
|
1147
|
+
throw new Error("Billing is not available on this platform");
|
|
1148
|
+
}
|
|
1149
|
+
if (this.platform === "native" /* NATIVE */) {
|
|
1150
|
+
return await this.purchaseNative(productId);
|
|
1151
|
+
} else if (this.platform === "web" /* WEB */) {
|
|
1152
|
+
return await this.purchaseWeb(productId, options?.elementId);
|
|
1153
|
+
}
|
|
1154
|
+
throw new Error("Cannot purchase: unknown platform");
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Purchase via native IAP
|
|
1158
|
+
*/
|
|
1159
|
+
async purchaseNative(productId) {
|
|
1160
|
+
return new Promise((resolve, reject) => {
|
|
1161
|
+
if (!this.config.userId) {
|
|
1162
|
+
reject(new Error("userId is required for native purchases"));
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
logger.info(`[BillingService] Purchasing: ${productId}`);
|
|
1166
|
+
const previousCompleteCallback = this.onPurchaseCompleteCallback;
|
|
1167
|
+
const previousErrorCallback = this.onPurchaseErrorCallback;
|
|
1168
|
+
const cleanup = () => {
|
|
1169
|
+
this.onPurchaseCompleteCallback = previousCompleteCallback;
|
|
1170
|
+
this.onPurchaseErrorCallback = previousErrorCallback;
|
|
1171
|
+
};
|
|
1172
|
+
const completeHandler = (result) => {
|
|
1173
|
+
if (result.productId === productId) {
|
|
1174
|
+
cleanup();
|
|
1175
|
+
resolve(result);
|
|
1176
|
+
if (previousCompleteCallback) {
|
|
1177
|
+
previousCompleteCallback(result);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
const errorHandler = (result) => {
|
|
1182
|
+
if (result.productId === productId) {
|
|
1183
|
+
cleanup();
|
|
1184
|
+
reject(new Error(result.error?.message || "Purchase failed"));
|
|
1185
|
+
if (previousErrorCallback) {
|
|
1186
|
+
previousErrorCallback(result);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
this.onPurchaseCompleteCallback = completeHandler;
|
|
1191
|
+
this.onPurchaseErrorCallback = errorHandler;
|
|
1192
|
+
NativeBridge.purchase(productId, this.config.userId);
|
|
1193
|
+
setTimeout(() => {
|
|
1194
|
+
cleanup();
|
|
1195
|
+
reject(new Error("Purchase timeout"));
|
|
1196
|
+
}, 6e4);
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Purchase via web (Stripe)
|
|
1201
|
+
* @param productId - The priceId (Stripe price ID) for web purchases
|
|
1202
|
+
* @param elementId - Optional DOM element ID to mount the checkout form (default: 'stripe-checkout-element')
|
|
1203
|
+
*/
|
|
1204
|
+
async purchaseWeb(productId, elementId) {
|
|
1205
|
+
if (!this.config.userId || !this.config.checkoutUrl) {
|
|
1206
|
+
throw new Error("userId and checkoutUrl are required for web purchases");
|
|
1207
|
+
}
|
|
1208
|
+
if (!this.stripe) {
|
|
1209
|
+
throw new Error("Stripe not initialized. Call initialize() first.");
|
|
1210
|
+
}
|
|
1211
|
+
try {
|
|
1212
|
+
const returnUrl = typeof window !== "undefined" ? `${window.location.href}${window.location.href.includes("?") ? "&" : "?"}payment=complete` : void 0;
|
|
1213
|
+
const requestBody = {
|
|
1214
|
+
priceId: productId,
|
|
1215
|
+
// API expects priceId for Stripe
|
|
1216
|
+
userId: this.config.userId,
|
|
1217
|
+
gameId: this.config.gameId,
|
|
1218
|
+
embedded: true,
|
|
1219
|
+
// Enable embedded checkout
|
|
1220
|
+
return_url: returnUrl
|
|
1221
|
+
};
|
|
1222
|
+
const response = await fetch(`${this.config.checkoutUrl}/create-checkout-session`, {
|
|
1223
|
+
method: "POST",
|
|
1224
|
+
headers: {
|
|
1225
|
+
"Content-Type": "application/json"
|
|
1226
|
+
},
|
|
1227
|
+
body: JSON.stringify(requestBody)
|
|
1228
|
+
});
|
|
1229
|
+
if (!response.ok) {
|
|
1230
|
+
const errorText = await response.text();
|
|
1231
|
+
throw new Error(`Failed to create checkout session: ${response.status} - ${errorText}`);
|
|
1232
|
+
}
|
|
1233
|
+
const responseData = await response.json();
|
|
1234
|
+
const { client_secret, id } = responseData;
|
|
1235
|
+
if (!client_secret) {
|
|
1236
|
+
throw new Error("No client_secret returned from checkout session");
|
|
1237
|
+
}
|
|
1238
|
+
await this.mountCheckoutElement(client_secret, elementId || "stripe-checkout-element");
|
|
1239
|
+
logger.info(`[BillingService] Checkout session created: ${id}`);
|
|
1240
|
+
return {
|
|
1241
|
+
success: true,
|
|
1242
|
+
productId,
|
|
1243
|
+
transactionId: id
|
|
1244
|
+
};
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
logger.error("[BillingService] Web purchase failed:", error?.message);
|
|
1247
|
+
return {
|
|
1248
|
+
success: false,
|
|
1249
|
+
productId,
|
|
1250
|
+
error: {
|
|
1251
|
+
code: "WEB_PURCHASE_FAILED",
|
|
1252
|
+
message: error.message || "Purchase failed"
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Mount Stripe embedded checkout element to the DOM
|
|
1259
|
+
* @param clientSecret - The client secret from the checkout session
|
|
1260
|
+
* @param elementId - The ID of the DOM element to mount to
|
|
1261
|
+
*/
|
|
1262
|
+
async mountCheckoutElement(clientSecret, elementId) {
|
|
1263
|
+
if (!this.stripe) {
|
|
1264
|
+
throw new Error("Stripe not initialized");
|
|
1265
|
+
}
|
|
1266
|
+
try {
|
|
1267
|
+
if (this.checkoutElement) {
|
|
1268
|
+
logger.info("[BillingService] Unmounting existing checkout element");
|
|
1269
|
+
this.unmountCheckoutElement();
|
|
1270
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1271
|
+
}
|
|
1272
|
+
const container = document.getElementById(elementId);
|
|
1273
|
+
if (!container) {
|
|
1274
|
+
throw new Error(`Element with id "${elementId}" not found in the DOM`);
|
|
1275
|
+
}
|
|
1276
|
+
container.innerHTML = "";
|
|
1277
|
+
logger.info("[BillingService] Creating new checkout instance");
|
|
1278
|
+
this.checkoutElement = await this.stripe.initEmbeddedCheckout({
|
|
1279
|
+
clientSecret
|
|
1280
|
+
});
|
|
1281
|
+
logger.info("[BillingService] Mounting checkout element to DOM");
|
|
1282
|
+
this.checkoutElement.mount(`#${elementId}`);
|
|
1283
|
+
this.setupCheckoutEventListeners();
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
logger.error("[BillingService] Failed to mount checkout:", error?.message);
|
|
1286
|
+
throw error;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Set up event listeners for checkout completion
|
|
1291
|
+
*/
|
|
1292
|
+
setupCheckoutEventListeners() {
|
|
1293
|
+
if (typeof window !== "undefined") {
|
|
1294
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
1295
|
+
const paymentStatus = urlParams.get("payment");
|
|
1296
|
+
if (paymentStatus === "complete") {
|
|
1297
|
+
const sessionId = urlParams.get("session_id");
|
|
1298
|
+
if (this.onPurchaseCompleteCallback) {
|
|
1299
|
+
this.onPurchaseCompleteCallback({
|
|
1300
|
+
success: true,
|
|
1301
|
+
productId: "",
|
|
1302
|
+
// Would need to be tracked separately
|
|
1303
|
+
transactionId: sessionId || void 0
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
logger.info("[BillingService] Payment completed");
|
|
1307
|
+
urlParams.delete("payment");
|
|
1308
|
+
urlParams.delete("session_id");
|
|
1309
|
+
const newUrl = `${window.location.pathname}${urlParams.toString() ? "?" + urlParams.toString() : ""}`;
|
|
1310
|
+
window.history.replaceState({}, "", newUrl);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Unmount and destroy the checkout element
|
|
1316
|
+
*/
|
|
1317
|
+
unmountCheckoutElement() {
|
|
1318
|
+
if (this.checkoutElement) {
|
|
1319
|
+
try {
|
|
1320
|
+
this.checkoutElement.unmount();
|
|
1321
|
+
if (typeof this.checkoutElement.destroy === "function") {
|
|
1322
|
+
this.checkoutElement.destroy();
|
|
1323
|
+
}
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
logger.warn("[BillingService] Error unmounting checkout element:", error?.message);
|
|
1326
|
+
}
|
|
1327
|
+
this.checkoutElement = null;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Set callback for successful purchases
|
|
1332
|
+
*/
|
|
1333
|
+
onPurchaseComplete(callback) {
|
|
1334
|
+
this.onPurchaseCompleteCallback = callback;
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Set callback for failed purchases
|
|
1338
|
+
*/
|
|
1339
|
+
onPurchaseError(callback) {
|
|
1340
|
+
this.onPurchaseErrorCallback = callback;
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Clean up resources
|
|
1344
|
+
*/
|
|
1345
|
+
dispose() {
|
|
1346
|
+
if (this.platform === "native" /* NATIVE */) {
|
|
1347
|
+
NativeBridge.off("IAP_AVAILABILITY_RESULT" /* IAP_AVAILABILITY_RESULT */);
|
|
1348
|
+
NativeBridge.off("PRODUCTS_RESULT" /* PRODUCTS_RESULT */);
|
|
1349
|
+
NativeBridge.off("PURCHASE_COMPLETE" /* PURCHASE_COMPLETE */);
|
|
1350
|
+
NativeBridge.off("PURCHASE_ERROR" /* PURCHASE_ERROR */);
|
|
1351
|
+
}
|
|
1352
|
+
this.unmountCheckoutElement();
|
|
1353
|
+
this.initPromise = null;
|
|
1354
|
+
this.nativeAvailable = false;
|
|
1355
|
+
this.stripe = null;
|
|
1356
|
+
this.onPurchaseCompleteCallback = void 0;
|
|
1357
|
+
this.onPurchaseErrorCallback = void 0;
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
// src/services/storage.ts
|
|
1362
|
+
var CloudStorageAdapter = class {
|
|
1363
|
+
constructor(callApi) {
|
|
1364
|
+
this.callApi = callApi;
|
|
1365
|
+
}
|
|
1366
|
+
async saveGameData(gameId, key, value) {
|
|
1367
|
+
return this.callApi("/api/v1/persistent-game-data", {
|
|
1368
|
+
method: "POST",
|
|
1369
|
+
headers: {
|
|
1370
|
+
"Content-Type": "application/json"
|
|
1371
|
+
},
|
|
1372
|
+
body: JSON.stringify({ game_id: gameId, key, value })
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
async batchSaveGameData(gameId, items) {
|
|
1376
|
+
return this.callApi(
|
|
1377
|
+
"/api/v1/persistent-game-data/batch",
|
|
1378
|
+
{
|
|
1379
|
+
method: "POST",
|
|
1380
|
+
headers: {
|
|
1381
|
+
"Content-Type": "application/json"
|
|
1382
|
+
},
|
|
1383
|
+
body: JSON.stringify({ game_id: gameId, items })
|
|
1384
|
+
}
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
async getGameData(gameId, key) {
|
|
1388
|
+
try {
|
|
1389
|
+
const params = new URLSearchParams({ game_id: gameId, key });
|
|
1390
|
+
const response = await this.callApi(
|
|
1391
|
+
`/api/v1/persistent-game-data?${params}`
|
|
1392
|
+
);
|
|
1393
|
+
return response.data;
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
if (error instanceof Error && error.message.includes("404")) {
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
throw error;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
async getMultipleGameData(gameId, keys) {
|
|
1402
|
+
const params = new URLSearchParams({ game_id: gameId });
|
|
1403
|
+
keys.forEach((key) => params.append("keys", key));
|
|
1404
|
+
const response = await this.callApi(
|
|
1405
|
+
`/api/v1/persistent-game-data?${params}`
|
|
1406
|
+
);
|
|
1407
|
+
return response.data;
|
|
1408
|
+
}
|
|
1409
|
+
async deleteGameData(gameId, key) {
|
|
1410
|
+
try {
|
|
1411
|
+
const params = new URLSearchParams({ game_id: gameId, key });
|
|
1412
|
+
const response = await this.callApi(
|
|
1413
|
+
`/api/v1/persistent-game-data?${params}`,
|
|
1414
|
+
{ method: "DELETE" }
|
|
1415
|
+
);
|
|
1416
|
+
return response.success && response.deleted_count > 0;
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
if (error instanceof Error && error.message.includes("404")) {
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
throw error;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
async deleteMultipleGameData(gameId, keys) {
|
|
1425
|
+
const params = new URLSearchParams({ game_id: gameId });
|
|
1426
|
+
keys.forEach((key) => params.append("keys", key));
|
|
1427
|
+
const response = await this.callApi(
|
|
1428
|
+
`/api/v1/persistent-game-data?${params}`,
|
|
1429
|
+
{ method: "DELETE" }
|
|
1430
|
+
);
|
|
1431
|
+
return response.deleted_count;
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
var LocalStorageAdapter = class {
|
|
1435
|
+
constructor(getUserId) {
|
|
1436
|
+
this.getUserId = getUserId;
|
|
1437
|
+
}
|
|
1438
|
+
storagePrefix = "hyve_game_data";
|
|
1439
|
+
getStorageKey(gameId, key) {
|
|
1440
|
+
return `${this.storagePrefix}:${gameId}:${key}`;
|
|
1441
|
+
}
|
|
1442
|
+
async saveGameData(gameId, key, value) {
|
|
1443
|
+
try {
|
|
1444
|
+
const storageKey = this.getStorageKey(gameId, key);
|
|
1445
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1446
|
+
const existing = localStorage.getItem(storageKey);
|
|
1447
|
+
const createdAt = existing ? JSON.parse(existing).created_at || now : now;
|
|
1448
|
+
const item = { key, value, created_at: createdAt, updated_at: now };
|
|
1449
|
+
localStorage.setItem(storageKey, JSON.stringify(item));
|
|
1450
|
+
return { success: true, message: "Data saved successfully" };
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
throw new Error(`Failed to save: ${error instanceof Error ? error.message : "Unknown"}`);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async batchSaveGameData(gameId, items) {
|
|
1456
|
+
for (const item of items) {
|
|
1457
|
+
await this.saveGameData(gameId, item.key, item.value);
|
|
1458
|
+
}
|
|
1459
|
+
return { success: true, message: `${items.length} items saved` };
|
|
1460
|
+
}
|
|
1461
|
+
async getGameData(gameId, key) {
|
|
1462
|
+
const data = localStorage.getItem(this.getStorageKey(gameId, key));
|
|
1463
|
+
return data ? JSON.parse(data) : null;
|
|
1464
|
+
}
|
|
1465
|
+
async getMultipleGameData(gameId, keys) {
|
|
1466
|
+
const items = [];
|
|
1467
|
+
for (const key of keys) {
|
|
1468
|
+
const item = await this.getGameData(gameId, key);
|
|
1469
|
+
if (item) items.push(item);
|
|
1470
|
+
}
|
|
1471
|
+
return items;
|
|
1472
|
+
}
|
|
1473
|
+
async deleteGameData(gameId, key) {
|
|
1474
|
+
const storageKey = this.getStorageKey(gameId, key);
|
|
1475
|
+
const exists = localStorage.getItem(storageKey) !== null;
|
|
1476
|
+
if (exists) localStorage.removeItem(storageKey);
|
|
1477
|
+
return exists;
|
|
1478
|
+
}
|
|
1479
|
+
async deleteMultipleGameData(gameId, keys) {
|
|
1480
|
+
let count = 0;
|
|
1481
|
+
for (const key of keys) {
|
|
1482
|
+
if (await this.deleteGameData(gameId, key)) count++;
|
|
1483
|
+
}
|
|
1484
|
+
return count;
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
// src/core/client.ts
|
|
1489
|
+
function determineEnvironmentFromParentUrl() {
|
|
1490
|
+
try {
|
|
1491
|
+
let parentUrl = "";
|
|
1492
|
+
if (window !== window.top) {
|
|
1493
|
+
try {
|
|
1494
|
+
parentUrl = window.parent.location.href;
|
|
1495
|
+
} catch (e) {
|
|
1496
|
+
parentUrl = document.referrer;
|
|
1497
|
+
}
|
|
1498
|
+
} else {
|
|
1499
|
+
parentUrl = window.location.href;
|
|
1500
|
+
}
|
|
1501
|
+
logger.debug("Detected parent URL:", parentUrl);
|
|
1502
|
+
if (parentUrl.includes("marvin.dev.hyve.gg") || parentUrl.includes("dev.hyve.gg")) {
|
|
1503
|
+
logger.info("Environment detected: dev (from parent URL)");
|
|
1504
|
+
return true;
|
|
1505
|
+
}
|
|
1506
|
+
if (parentUrl.includes("marvin.hyve.gg") || parentUrl.includes("hyve.gg")) {
|
|
1507
|
+
logger.info("Environment detected: prod (from parent URL)");
|
|
1508
|
+
return false;
|
|
1509
|
+
}
|
|
1510
|
+
logger.info("Environment unknown, defaulting to dev");
|
|
1511
|
+
return true;
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
logger.warn("Failed to determine environment from parent URL:", error);
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
var HyveClient = class {
|
|
1518
|
+
telemetryConfig;
|
|
1519
|
+
apiBaseUrl;
|
|
1520
|
+
sessionId;
|
|
1521
|
+
userId = null;
|
|
1522
|
+
jwtToken = null;
|
|
1523
|
+
gameId = null;
|
|
1524
|
+
adsService;
|
|
1525
|
+
playgamaService = null;
|
|
1526
|
+
playgamaInitPromise = null;
|
|
1527
|
+
crazyGamesService = null;
|
|
1528
|
+
crazyGamesInitPromise = null;
|
|
1529
|
+
billingService;
|
|
1530
|
+
storageMode;
|
|
1531
|
+
cloudStorageAdapter;
|
|
1532
|
+
localStorageAdapter;
|
|
1533
|
+
/**
|
|
1534
|
+
* Creates a new HyveClient instance
|
|
1535
|
+
* @param config Optional configuration including telemetry and ads
|
|
1536
|
+
*/
|
|
1537
|
+
constructor(config) {
|
|
1538
|
+
const isDev = config?.isDev !== void 0 ? config.isDev : determineEnvironmentFromParentUrl();
|
|
1539
|
+
this.telemetryConfig = {
|
|
1540
|
+
isDev,
|
|
1541
|
+
...config
|
|
1542
|
+
};
|
|
1543
|
+
if (this.telemetryConfig.apiBaseUrl) {
|
|
1544
|
+
this.apiBaseUrl = this.telemetryConfig.apiBaseUrl;
|
|
1545
|
+
} else {
|
|
1546
|
+
this.apiBaseUrl = this.telemetryConfig.isDev ? "https://product-api.dev.hyve.gg" : "https://product-api.prod.hyve.gg";
|
|
1547
|
+
}
|
|
1548
|
+
this.sessionId = generateUUID();
|
|
1549
|
+
this.adsService = new AdsService();
|
|
1550
|
+
if (config?.ads) {
|
|
1551
|
+
this.adsService.configure(config.ads);
|
|
1552
|
+
}
|
|
1553
|
+
if (typeof window !== "undefined" && PlaygamaService.isPlaygamaDomain()) {
|
|
1554
|
+
this.playgamaService = new PlaygamaService();
|
|
1555
|
+
this.playgamaInitPromise = this.playgamaService.initialize().then((success) => {
|
|
1556
|
+
logger.info("Playgama Bridge initialized:", success);
|
|
1557
|
+
return success;
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
if (typeof window !== "undefined" && CrazyGamesService.isCrazyGamesDomain()) {
|
|
1561
|
+
this.crazyGamesService = new CrazyGamesService();
|
|
1562
|
+
this.crazyGamesInitPromise = this.crazyGamesService.initialize().then((success) => {
|
|
1563
|
+
logger.info("CrazyGames SDK initialized:", success);
|
|
1564
|
+
return success;
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
this.storageMode = config?.storageMode || "cloud";
|
|
1568
|
+
this.cloudStorageAdapter = new CloudStorageAdapter(
|
|
1569
|
+
(endpoint, options) => this.callApi(endpoint, options)
|
|
1570
|
+
);
|
|
1571
|
+
this.localStorageAdapter = new LocalStorageAdapter(() => this.getUserId());
|
|
1572
|
+
if (typeof window !== "undefined") {
|
|
1573
|
+
this._parseUrlAuth();
|
|
1574
|
+
}
|
|
1575
|
+
const billingConfig = {
|
|
1576
|
+
checkoutUrl: this.apiBaseUrl,
|
|
1577
|
+
userId: this.userId ?? void 0,
|
|
1578
|
+
gameId: this.gameId ? Number(this.gameId) : void 0,
|
|
1579
|
+
...config?.billing
|
|
1580
|
+
};
|
|
1581
|
+
this.billingService = new BillingService(billingConfig);
|
|
1582
|
+
const envSource = config?.isDev !== void 0 ? "explicit config" : "auto-detected from parent URL";
|
|
1583
|
+
logger.info("==========================================");
|
|
1584
|
+
logger.info("HyveClient Initialized");
|
|
1585
|
+
logger.info("==========================================");
|
|
1586
|
+
logger.info("Session ID:", this.sessionId);
|
|
1587
|
+
logger.info(
|
|
1588
|
+
"Environment:",
|
|
1589
|
+
this.telemetryConfig.isDev ? "DEVELOPMENT" : "PRODUCTION",
|
|
1590
|
+
`(${envSource})`
|
|
1591
|
+
);
|
|
1592
|
+
logger.info("API Base URL:", this.apiBaseUrl);
|
|
1593
|
+
logger.info("Playgama platform:", this.playgamaService !== null);
|
|
1594
|
+
logger.info("CrazyGames platform:", this.crazyGamesService !== null);
|
|
1595
|
+
logger.info(
|
|
1596
|
+
"Billing configured:",
|
|
1597
|
+
!!config?.billing && Object.keys(config.billing).length > 0
|
|
1598
|
+
);
|
|
1599
|
+
logger.info("Storage mode:", this.storageMode);
|
|
1600
|
+
logger.info("Authenticated:", this.jwtToken !== null);
|
|
1601
|
+
logger.debug("Config:", {
|
|
1602
|
+
isDev: this.telemetryConfig.isDev,
|
|
1603
|
+
hasCustomApiUrl: !!config?.apiBaseUrl,
|
|
1604
|
+
billingConfigured: !!config?.billing && Object.keys(config.billing).length > 0,
|
|
1605
|
+
storageMode: this.storageMode
|
|
1606
|
+
});
|
|
1607
|
+
logger.info("==========================================");
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Parses JWT and game ID from the current window URL and stores them on the client.
|
|
1611
|
+
* Called automatically during construction.
|
|
1612
|
+
*/
|
|
1613
|
+
_parseUrlAuth(urlParams) {
|
|
1614
|
+
try {
|
|
1615
|
+
const params = urlParams ? parseUrlParams(urlParams) : parseUrlParams(window.location.search);
|
|
1616
|
+
if (params.hyveAccess) {
|
|
1617
|
+
this.jwtToken = params.hyveAccess;
|
|
1618
|
+
logger.info("JWT token extracted from hyve-access parameter");
|
|
1619
|
+
const userId = extractUserIdFromJwt(this.jwtToken);
|
|
1620
|
+
if (userId) {
|
|
1621
|
+
this.userId = userId;
|
|
1622
|
+
logger.info("User ID extracted from JWT:", userId);
|
|
1623
|
+
}
|
|
1624
|
+
if (!this.gameId) {
|
|
1625
|
+
const gameIdFromJwt = extractGameIdFromJwt(this.jwtToken);
|
|
1626
|
+
if (gameIdFromJwt !== null) {
|
|
1627
|
+
this.gameId = gameIdFromJwt.toString();
|
|
1628
|
+
logger.info("Game ID extracted from JWT:", gameIdFromJwt);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
if (params.gameId) {
|
|
1633
|
+
this.gameId = params.gameId;
|
|
1634
|
+
logger.info("Game ID extracted from game-id parameter:", this.gameId);
|
|
1635
|
+
}
|
|
1636
|
+
if (this.jwtToken) {
|
|
1637
|
+
logger.info("Authentication successful via JWT");
|
|
1638
|
+
} else {
|
|
1639
|
+
logger.info("No hyve-access JWT token in URL \u2014 unauthenticated");
|
|
1640
|
+
}
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
logger.error("Error parsing URL auth:", error);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Sends a user-level telemetry event using JWT authentication
|
|
1647
|
+
* Requires JWT token, authenticated user, and game ID from URL parameters
|
|
1648
|
+
* @param eventLocation Location where the event occurred
|
|
1649
|
+
* @param eventCategory Main category of the event
|
|
1650
|
+
* @param eventAction Primary action taken
|
|
1651
|
+
* @param eventSubCategory Optional sub-category
|
|
1652
|
+
* @param eventSubAction Optional sub-action
|
|
1653
|
+
* @param eventDetails Optional event details (object or JSON string)
|
|
1654
|
+
* @param platformId Optional platform identifier
|
|
1655
|
+
* @returns Promise resolving to boolean indicating success
|
|
1656
|
+
*/
|
|
1657
|
+
async sendTelemetry(eventLocation, eventCategory, eventAction, eventSubCategory, eventSubAction, eventDetails, platformId) {
|
|
1658
|
+
if (!this.jwtToken) {
|
|
1659
|
+
logger.error("JWT token required. Ensure hyve-access and game-id are present in the URL.");
|
|
1660
|
+
return false;
|
|
1661
|
+
}
|
|
1662
|
+
if (!this.gameId) {
|
|
1663
|
+
logger.error("Game ID required. Ensure game-id URL parameter is set.");
|
|
1664
|
+
return false;
|
|
1665
|
+
}
|
|
1666
|
+
try {
|
|
1667
|
+
if (eventDetails) {
|
|
1668
|
+
try {
|
|
1669
|
+
if (typeof eventDetails === "string") {
|
|
1670
|
+
JSON.parse(eventDetails);
|
|
1671
|
+
} else if (typeof eventDetails === "object") {
|
|
1672
|
+
JSON.stringify(eventDetails);
|
|
1673
|
+
}
|
|
1674
|
+
} catch (validationError) {
|
|
1675
|
+
logger.error("Invalid JSON in eventDetails:", validationError);
|
|
1676
|
+
logger.error("eventDetails value:", eventDetails);
|
|
1677
|
+
return false;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
const toJsonString = (data) => {
|
|
1681
|
+
if (!data) return null;
|
|
1682
|
+
return typeof data === "string" ? data : JSON.stringify(data);
|
|
1683
|
+
};
|
|
1684
|
+
const telemetryEvent = {
|
|
1685
|
+
game_id: this.gameId,
|
|
1686
|
+
session_id: this.sessionId,
|
|
1687
|
+
platform_id: platformId || null,
|
|
1688
|
+
event_location: eventLocation,
|
|
1689
|
+
event_category: eventCategory,
|
|
1690
|
+
event_action: eventAction,
|
|
1691
|
+
event_sub_category: eventSubCategory || null,
|
|
1692
|
+
event_sub_action: eventSubAction || null,
|
|
1693
|
+
event_details: toJsonString(eventDetails)
|
|
1694
|
+
};
|
|
1695
|
+
logger.debug("Sending telemetry event:", telemetryEvent);
|
|
1696
|
+
const telemetryUrl = `${this.apiBaseUrl}/api/v1/analytics/send`;
|
|
1697
|
+
const response = await fetch(telemetryUrl, {
|
|
1698
|
+
method: "POST",
|
|
1699
|
+
headers: {
|
|
1700
|
+
"Content-Type": "application/json",
|
|
1701
|
+
Authorization: `Bearer ${this.jwtToken}`
|
|
1702
|
+
},
|
|
1703
|
+
body: JSON.stringify(telemetryEvent)
|
|
1704
|
+
});
|
|
1705
|
+
if (response.ok) {
|
|
1706
|
+
logger.info("Telemetry event sent successfully:", response.status);
|
|
1707
|
+
return true;
|
|
1708
|
+
} else {
|
|
1709
|
+
const errorText = await response.text();
|
|
1710
|
+
logger.error(
|
|
1711
|
+
"Failed to send telemetry event:",
|
|
1712
|
+
response.status,
|
|
1713
|
+
errorText
|
|
1714
|
+
);
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
logger.error("Error sending telemetry event:", error);
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Makes an authenticated API call using the JWT token
|
|
1724
|
+
* @param endpoint API endpoint path (will be appended to base URL)
|
|
1725
|
+
* @param options Fetch options (method, body, etc.)
|
|
1726
|
+
* @returns Promise resolving to the API response
|
|
1727
|
+
*/
|
|
1728
|
+
async callApi(endpoint, options = {}) {
|
|
1729
|
+
if (!this.jwtToken) {
|
|
1730
|
+
throw new Error(
|
|
1731
|
+
"No JWT token available. Ensure hyve-access and game-id are present in the URL."
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
try {
|
|
1735
|
+
const url = `${this.apiBaseUrl}${endpoint.startsWith("/") ? endpoint : `/${endpoint}`}`;
|
|
1736
|
+
logger.debug("Making API call to:", url);
|
|
1737
|
+
const response = await fetch(url, {
|
|
1738
|
+
...options,
|
|
1739
|
+
headers: {
|
|
1740
|
+
"Content-Type": "application/json",
|
|
1741
|
+
Authorization: `Bearer ${this.jwtToken}`,
|
|
1742
|
+
...options.headers
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
if (!response.ok) {
|
|
1746
|
+
const errorText = await response.text();
|
|
1747
|
+
throw new Error(`API request failed: ${response.status} ${errorText}`);
|
|
1748
|
+
}
|
|
1749
|
+
return await response.json();
|
|
1750
|
+
} catch (error) {
|
|
1751
|
+
logger.error("API call failed:", error);
|
|
1752
|
+
throw error;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Gets the user's inventory
|
|
1757
|
+
* @returns Promise resolving to the user's inventory
|
|
1758
|
+
*/
|
|
1759
|
+
async getInventory() {
|
|
1760
|
+
return this.callApi("/api/v1/inventory");
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Gets a specific inventory item by ID
|
|
1764
|
+
* @param itemId The inventory item ID
|
|
1765
|
+
* @returns Promise resolving to the inventory item details
|
|
1766
|
+
*/
|
|
1767
|
+
async getInventoryItem(itemId) {
|
|
1768
|
+
return this.callApi(`/api/v1/inventory/${itemId}`);
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Updates the telemetry configuration
|
|
1772
|
+
* @param config New telemetry configuration
|
|
1773
|
+
*/
|
|
1774
|
+
updateTelemetryConfig(config) {
|
|
1775
|
+
this.telemetryConfig = {
|
|
1776
|
+
...this.telemetryConfig,
|
|
1777
|
+
...config
|
|
1778
|
+
};
|
|
1779
|
+
if (config.apiBaseUrl !== void 0) {
|
|
1780
|
+
this.apiBaseUrl = config.apiBaseUrl;
|
|
1781
|
+
} else if (config.isDev !== void 0) {
|
|
1782
|
+
this.apiBaseUrl = config.isDev ? "https://product-api.dev.hyve.gg" : "https://product-api.prod.hyve.gg";
|
|
1783
|
+
}
|
|
1784
|
+
logger.info("Config updated");
|
|
1785
|
+
logger.info("API Base URL:", this.apiBaseUrl);
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Gets the current user ID
|
|
1789
|
+
* @returns Current user ID or null if not authenticated
|
|
1790
|
+
*/
|
|
1791
|
+
getUserId() {
|
|
1792
|
+
return this.userId;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Gets the current session ID
|
|
1796
|
+
* @returns Current session ID
|
|
1797
|
+
*/
|
|
1798
|
+
getSessionId() {
|
|
1799
|
+
return this.sessionId;
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Gets the current JWT token
|
|
1803
|
+
* @returns Current JWT token or null if not available
|
|
1804
|
+
*/
|
|
1805
|
+
getJwtToken() {
|
|
1806
|
+
return this.jwtToken;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Gets the current game ID
|
|
1810
|
+
* @returns Current game ID or null if not available
|
|
1811
|
+
*/
|
|
1812
|
+
getGameId() {
|
|
1813
|
+
return this.gameId;
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Gets the API base URL
|
|
1817
|
+
* @returns API base URL
|
|
1818
|
+
*/
|
|
1819
|
+
getApiBaseUrl() {
|
|
1820
|
+
return this.apiBaseUrl;
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Checks if user is authenticated
|
|
1824
|
+
* @returns Boolean indicating authentication status
|
|
1825
|
+
*/
|
|
1826
|
+
isUserAuthenticated() {
|
|
1827
|
+
return this.userId !== null;
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Checks if JWT token is available
|
|
1831
|
+
* @returns Boolean indicating if JWT token is present
|
|
1832
|
+
*/
|
|
1833
|
+
hasJwtToken() {
|
|
1834
|
+
return this.jwtToken !== null;
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Logs out the current user
|
|
1838
|
+
*/
|
|
1839
|
+
logout() {
|
|
1840
|
+
this.userId = null;
|
|
1841
|
+
this.jwtToken = null;
|
|
1842
|
+
this.gameId = null;
|
|
1843
|
+
logger.info("User logged out");
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Resets the client state
|
|
1847
|
+
*/
|
|
1848
|
+
reset() {
|
|
1849
|
+
this.logout();
|
|
1850
|
+
this.sessionId = generateUUID();
|
|
1851
|
+
logger.info("Client reset with new sessionId:", this.sessionId);
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Get the storage adapter based on mode
|
|
1855
|
+
* @param mode Storage mode override (cloud or local)
|
|
1856
|
+
*/
|
|
1857
|
+
getStorageAdapter(mode) {
|
|
1858
|
+
const storageMode = mode || this.storageMode;
|
|
1859
|
+
return storageMode === "local" ? this.localStorageAdapter : this.cloudStorageAdapter;
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Returns the current game ID or throws if not available.
|
|
1863
|
+
*/
|
|
1864
|
+
requireGameId() {
|
|
1865
|
+
const gameId = this.requireGameId();
|
|
1866
|
+
return gameId;
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Save persistent game data
|
|
1870
|
+
* @param key Data key
|
|
1871
|
+
* @param value Data value (any JSON-serializable value)
|
|
1872
|
+
* @param storage Storage mode override ('cloud' or 'local')
|
|
1873
|
+
* @returns Promise resolving to save response
|
|
1874
|
+
*/
|
|
1875
|
+
async saveGameData(key, value, storage) {
|
|
1876
|
+
const gameId = this.requireGameId();
|
|
1877
|
+
const storageMode = storage || this.storageMode;
|
|
1878
|
+
logger.debug(`Saving game data to ${storageMode}: ${gameId}/${key}`);
|
|
1879
|
+
const adapter = this.getStorageAdapter(storage);
|
|
1880
|
+
const response = await adapter.saveGameData(gameId, key, value);
|
|
1881
|
+
logger.info(`Game data saved successfully to ${storageMode}: ${gameId}/${key}`);
|
|
1882
|
+
return response;
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Batch save persistent game data
|
|
1886
|
+
* @param items Array of key-value pairs to save
|
|
1887
|
+
* @param storage Storage mode override ('cloud' or 'local')
|
|
1888
|
+
* @returns Promise resolving to save response
|
|
1889
|
+
*/
|
|
1890
|
+
async batchSaveGameData(items, storage) {
|
|
1891
|
+
const gameId = this.requireGameId();
|
|
1892
|
+
const storageMode = storage || this.storageMode;
|
|
1893
|
+
logger.debug(`Batch saving ${items.length} game data entries to ${storageMode} for game: ${gameId}`);
|
|
1894
|
+
const adapter = this.getStorageAdapter(storage);
|
|
1895
|
+
const response = await adapter.batchSaveGameData(gameId, items);
|
|
1896
|
+
logger.info(`Batch saved ${items.length} game data entries successfully to ${storageMode}`);
|
|
1897
|
+
return response;
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Get persistent game data
|
|
1901
|
+
* @param key Data key
|
|
1902
|
+
* @param storage Storage mode override ('cloud' or 'local')
|
|
1903
|
+
* @returns Promise resolving to game data item or null if not found
|
|
1904
|
+
*/
|
|
1905
|
+
async getGameData(key, storage) {
|
|
1906
|
+
const gameId = this.requireGameId();
|
|
1907
|
+
const storageMode = storage || this.storageMode;
|
|
1908
|
+
logger.debug(`Getting game data from ${storageMode}: ${gameId}/${key}`);
|
|
1909
|
+
const adapter = this.getStorageAdapter(storage);
|
|
1910
|
+
const data = await adapter.getGameData(gameId, key);
|
|
1911
|
+
if (data) {
|
|
1912
|
+
logger.info(`Game data retrieved successfully from ${storageMode}: ${gameId}/${key}`);
|
|
1913
|
+
} else {
|
|
1914
|
+
logger.debug(`Game data not found in ${storageMode}: ${key}`);
|
|
1915
|
+
}
|
|
1916
|
+
return data;
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Get multiple persistent game data entries
|
|
1920
|
+
* @param keys Array of data keys
|
|
1921
|
+
* @param storage Storage mode override ('cloud' or 'local')
|
|
1922
|
+
* @returns Promise resolving to array of game data items
|
|
1923
|
+
*/
|
|
1924
|
+
async getMultipleGameData(keys, storage) {
|
|
1925
|
+
const gameId = this.requireGameId();
|
|
1926
|
+
const storageMode = storage || this.storageMode;
|
|
1927
|
+
logger.debug(`Getting ${keys.length} game data entries from ${storageMode} for game: ${gameId}`);
|
|
1928
|
+
const adapter = this.getStorageAdapter(storage);
|
|
1929
|
+
const data = await adapter.getMultipleGameData(gameId, keys);
|
|
1930
|
+
logger.info(`Retrieved ${data.length} game data entries from ${storageMode}`);
|
|
1931
|
+
return data;
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Delete persistent game data
|
|
1935
|
+
* @param key Data key
|
|
1936
|
+
* @param storage Storage mode override ('cloud' or 'local')
|
|
1937
|
+
* @returns Promise resolving to boolean indicating if data was deleted
|
|
1938
|
+
*/
|
|
1939
|
+
async deleteGameData(key, storage) {
|
|
1940
|
+
const gameId = this.requireGameId();
|
|
1941
|
+
const storageMode = storage || this.storageMode;
|
|
1942
|
+
logger.debug(`Deleting game data from ${storageMode}: ${gameId}/${key}`);
|
|
1943
|
+
const adapter = this.getStorageAdapter(storage);
|
|
1944
|
+
const deleted = await adapter.deleteGameData(gameId, key);
|
|
1945
|
+
if (deleted) {
|
|
1946
|
+
logger.info(`Game data deleted successfully from ${storageMode}: ${gameId}/${key}`);
|
|
1947
|
+
} else {
|
|
1948
|
+
logger.debug(`Game data not found in ${storageMode}: ${key}`);
|
|
1949
|
+
}
|
|
1950
|
+
return deleted;
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Delete multiple persistent game data entries
|
|
1954
|
+
* @param keys Array of data keys
|
|
1955
|
+
* @param storage Storage mode override ('cloud' or 'local')
|
|
1956
|
+
* @returns Promise resolving to number of entries deleted
|
|
1957
|
+
*/
|
|
1958
|
+
async deleteMultipleGameData(keys, storage) {
|
|
1959
|
+
const gameId = this.requireGameId();
|
|
1960
|
+
const storageMode = storage || this.storageMode;
|
|
1961
|
+
logger.debug(`Deleting ${keys.length} game data entries from ${storageMode} for game: ${gameId}`);
|
|
1962
|
+
const adapter = this.getStorageAdapter(storage);
|
|
1963
|
+
const deletedCount = await adapter.deleteMultipleGameData(gameId, keys);
|
|
1964
|
+
logger.info(`Deleted ${deletedCount} game data entries from ${storageMode}`);
|
|
1965
|
+
return deletedCount;
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Configure storage mode
|
|
1969
|
+
* @param mode Storage mode ('cloud' or 'local')
|
|
1970
|
+
*/
|
|
1971
|
+
configureStorage(mode) {
|
|
1972
|
+
this.storageMode = mode;
|
|
1973
|
+
logger.info("Storage mode updated to:", mode);
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Get current storage mode
|
|
1977
|
+
* @returns Current storage mode
|
|
1978
|
+
*/
|
|
1979
|
+
getStorageMode() {
|
|
1980
|
+
return this.storageMode;
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Configure ads service
|
|
1984
|
+
* @param config Ads configuration
|
|
1985
|
+
*/
|
|
1986
|
+
configureAds(config) {
|
|
1987
|
+
this.adsService.configure(config);
|
|
1988
|
+
logger.info("Ads configuration updated");
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Show an ad
|
|
1992
|
+
* @param type Type of ad to show ('rewarded', 'interstitial', or 'preroll')
|
|
1993
|
+
* @returns Promise resolving to ad result
|
|
1994
|
+
*/
|
|
1995
|
+
async showAd(type) {
|
|
1996
|
+
if (this.crazyGamesService) {
|
|
1997
|
+
if (this.crazyGamesInitPromise) {
|
|
1998
|
+
await this.crazyGamesInitPromise;
|
|
1999
|
+
}
|
|
2000
|
+
if (this.crazyGamesService.isInitialized()) {
|
|
2001
|
+
const { onBeforeAd, onAfterAd, onRewardEarned } = this.adsService.getCallbacks();
|
|
2002
|
+
if (type === "rewarded") {
|
|
2003
|
+
return this.crazyGamesService.showRewarded({
|
|
2004
|
+
onBeforeAd: () => onBeforeAd("rewarded"),
|
|
2005
|
+
onAfterAd: () => onAfterAd("rewarded"),
|
|
2006
|
+
onRewardEarned
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
return this.crazyGamesService.showInterstitial({
|
|
2010
|
+
onBeforeAd: () => onBeforeAd(type),
|
|
2011
|
+
onAfterAd: () => onAfterAd(type)
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
if (this.playgamaService) {
|
|
2016
|
+
if (this.playgamaInitPromise) {
|
|
2017
|
+
await this.playgamaInitPromise;
|
|
2018
|
+
}
|
|
2019
|
+
if (this.playgamaService.isInitialized()) {
|
|
2020
|
+
const { onBeforeAd, onAfterAd, onRewardEarned } = this.adsService.getCallbacks();
|
|
2021
|
+
if (type === "rewarded") {
|
|
2022
|
+
return this.playgamaService.showRewarded({
|
|
2023
|
+
onBeforeAd: () => onBeforeAd("rewarded"),
|
|
2024
|
+
onAfterAd: () => onAfterAd("rewarded"),
|
|
2025
|
+
onRewardEarned
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
return this.playgamaService.showInterstitial({
|
|
2029
|
+
onBeforeAd: () => onBeforeAd(type),
|
|
2030
|
+
onAfterAd: () => onAfterAd(type)
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
return this.adsService.show(type);
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Notifies CrazyGames that gameplay has started.
|
|
2038
|
+
* No-op on other platforms.
|
|
2039
|
+
*/
|
|
2040
|
+
async gameplayStart() {
|
|
2041
|
+
if (this.crazyGamesService) {
|
|
2042
|
+
if (this.crazyGamesInitPromise) await this.crazyGamesInitPromise;
|
|
2043
|
+
this.crazyGamesService.gameplayStart();
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Notifies CrazyGames that gameplay has stopped.
|
|
2048
|
+
* No-op on other platforms.
|
|
2049
|
+
*/
|
|
2050
|
+
async gameplayStop() {
|
|
2051
|
+
if (this.crazyGamesService) {
|
|
2052
|
+
if (this.crazyGamesInitPromise) await this.crazyGamesInitPromise;
|
|
2053
|
+
this.crazyGamesService.gameplayStop();
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Triggers a celebration effect on the CrazyGames website for significant achievements.
|
|
2058
|
+
* No-op on other platforms.
|
|
2059
|
+
*/
|
|
2060
|
+
async happytime() {
|
|
2061
|
+
if (this.crazyGamesService) {
|
|
2062
|
+
if (this.crazyGamesInitPromise) await this.crazyGamesInitPromise;
|
|
2063
|
+
this.crazyGamesService.happytime();
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Check if ads are ready to show
|
|
2068
|
+
* @returns Boolean indicating if ads have initialized successfully
|
|
2069
|
+
*/
|
|
2070
|
+
areAdsReady() {
|
|
2071
|
+
return this.adsService.isReady();
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Get the billing platform
|
|
2075
|
+
* @returns Current billing platform
|
|
2076
|
+
*/
|
|
2077
|
+
getBillingPlatform() {
|
|
2078
|
+
return this.billingService.getPlatform();
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* Check if billing is available
|
|
2082
|
+
* @returns Boolean indicating if billing is available
|
|
2083
|
+
*/
|
|
2084
|
+
isBillingAvailable() {
|
|
2085
|
+
return this.billingService.isAvailable();
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Get available billing products
|
|
2089
|
+
* @returns Promise resolving to array of products
|
|
2090
|
+
*/
|
|
2091
|
+
async getBillingProducts() {
|
|
2092
|
+
return await this.billingService.getProducts();
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Purchase a product
|
|
2096
|
+
* @param productId Product ID to purchase
|
|
2097
|
+
* @param options Optional purchase options (e.g., elementId for web)
|
|
2098
|
+
* @returns Promise resolving to purchase result
|
|
2099
|
+
*/
|
|
2100
|
+
async purchaseProduct(productId, options) {
|
|
2101
|
+
return await this.billingService.purchase(productId, options);
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Set callback for successful purchases
|
|
2105
|
+
* @param callback Function to call on purchase completion
|
|
2106
|
+
*/
|
|
2107
|
+
onPurchaseComplete(callback) {
|
|
2108
|
+
this.billingService.onPurchaseComplete(callback);
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Set callback for failed purchases
|
|
2112
|
+
* @param callback Function to call on purchase error
|
|
2113
|
+
*/
|
|
2114
|
+
onPurchaseError(callback) {
|
|
2115
|
+
this.billingService.onPurchaseError(callback);
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Unmount Stripe checkout element
|
|
2119
|
+
*/
|
|
2120
|
+
unmountBillingCheckout() {
|
|
2121
|
+
this.billingService.unmountCheckoutElement();
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
|
|
2125
|
+
// src/react.tsx
|
|
2126
|
+
import { jsx } from "react/jsx-runtime";
|
|
2127
|
+
var HyveSdkContext = createContext(null);
|
|
2128
|
+
function HyveSdkProvider({
|
|
2129
|
+
client,
|
|
2130
|
+
config,
|
|
2131
|
+
children
|
|
2132
|
+
}) {
|
|
2133
|
+
const instanceRef = useRef(null);
|
|
2134
|
+
if (client) {
|
|
2135
|
+
instanceRef.current = client;
|
|
2136
|
+
} else if (!instanceRef.current) {
|
|
2137
|
+
instanceRef.current = new HyveClient(config);
|
|
2138
|
+
}
|
|
2139
|
+
return /* @__PURE__ */ jsx(HyveSdkContext.Provider, { value: instanceRef.current, children });
|
|
2140
|
+
}
|
|
2141
|
+
function useHyveSdk() {
|
|
2142
|
+
const client = useContext(HyveSdkContext);
|
|
2143
|
+
if (!client) {
|
|
2144
|
+
throw new Error("useHyveSdk must be used within a HyveSdkProvider");
|
|
2145
|
+
}
|
|
2146
|
+
return client;
|
|
2147
|
+
}
|
|
2148
|
+
export {
|
|
2149
|
+
HyveSdkProvider,
|
|
2150
|
+
useHyveSdk
|
|
2151
|
+
};
|