@hyve-sdk/js 1.5.1 → 2.1.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/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
+ };