@hyve-sdk/js 2.11.2 → 2.13.0-canary.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/dist/index.js CHANGED
@@ -198,17 +198,26 @@ var NativeMessageType = /* @__PURE__ */ ((NativeMessageType2) => {
198
198
  NativeMessageType2["REQUEST_NOTIFICATION_PERMISSION"] = "REQUEST_NOTIFICATION_PERMISSION";
199
199
  NativeMessageType2["GET_PRODUCTS"] = "GET_PRODUCTS";
200
200
  NativeMessageType2["PURCHASE"] = "PURCHASE";
201
+ NativeMessageType2["SHOW_REWARDED_AD"] = "SHOW_REWARDED_AD";
202
+ NativeMessageType2["SHOW_INTERSTITIAL_AD"] = "SHOW_INTERSTITIAL_AD";
201
203
  NativeMessageType2["IAP_AVAILABILITY_RESULT"] = "IAP_AVAILABILITY_RESULT";
202
204
  NativeMessageType2["PUSH_PERMISSION_GRANTED"] = "PUSH_PERMISSION_GRANTED";
203
205
  NativeMessageType2["PUSH_PERMISSION_DENIED"] = "PUSH_PERMISSION_DENIED";
204
206
  NativeMessageType2["PRODUCTS_RESULT"] = "PRODUCTS_RESULT";
205
207
  NativeMessageType2["PURCHASE_COMPLETE"] = "PURCHASE_COMPLETE";
206
208
  NativeMessageType2["PURCHASE_ERROR"] = "PURCHASE_ERROR";
209
+ NativeMessageType2["AD_RESULT"] = "AD_RESULT";
207
210
  return NativeMessageType2;
208
211
  })(NativeMessageType || {});
209
212
  var NativeBridge = class {
210
213
  static handlers = /* @__PURE__ */ new Map();
211
214
  static isInitialized = false;
215
+ /**
216
+ * Pending `sendAdRequest()` promises, keyed by the ad type native echoes
217
+ * back in `AD_RESULT.type` ("rewarded" | "interstitial"). One in-flight
218
+ * request per type — ads are shown sequentially.
219
+ */
220
+ static adResultResolvers = /* @__PURE__ */ new Map();
212
221
  /**
213
222
  * Checks if the app is running inside a React Native WebView
214
223
  */
@@ -261,16 +270,20 @@ var NativeBridge = class {
261
270
  "PUSH_PERMISSION_DENIED" /* PUSH_PERMISSION_DENIED */,
262
271
  "PRODUCTS_RESULT" /* PRODUCTS_RESULT */,
263
272
  "PURCHASE_COMPLETE" /* PURCHASE_COMPLETE */,
264
- "PURCHASE_ERROR" /* PURCHASE_ERROR */
273
+ "PURCHASE_ERROR" /* PURCHASE_ERROR */,
274
+ "AD_RESULT" /* AD_RESULT */
265
275
  ];
266
276
  if (!nativeResponseTypes.includes(data.type)) {
267
277
  return;
268
278
  }
279
+ if (data.type === "AD_RESULT" /* AD_RESULT */) {
280
+ this.resolveAdResult(data.payload);
281
+ }
269
282
  const handler = this.handlers.get(data.type);
270
283
  if (handler) {
271
284
  logger.debug(`[NativeBridge] Handling message: ${data.type}`);
272
285
  handler(data.payload);
273
- } else {
286
+ } else if (data.type !== "AD_RESULT" /* AD_RESULT */) {
274
287
  logger.warn(`[NativeBridge] No handler registered for: ${data.type}`);
275
288
  }
276
289
  } catch (error) {
@@ -396,6 +409,81 @@ var NativeBridge = class {
396
409
  static purchase(productId, userId) {
397
410
  this.send("PURCHASE" /* PURCHASE */, { productId, userId });
398
411
  }
412
+ /**
413
+ * Sends a native ad request and resolves with the `AD_RESULT` native sends
414
+ * back. Never rejects — a missing native context, timeout, or superseded
415
+ * request resolves to `{ success: false }` with an error code so callers can
416
+ * decide whether to fall back to the web ad path.
417
+ *
418
+ * @param messageType - `SHOW_REWARDED_AD` or `SHOW_INTERSTITIAL_AD`
419
+ * @param payload - `{ gameId, format, placement? }`
420
+ * @param timeoutMs - How long to wait for `AD_RESULT` before giving up
421
+ *
422
+ * @example
423
+ * const result = await NativeBridge.sendAdRequest(
424
+ * NativeMessageType.SHOW_REWARDED_AD,
425
+ * { gameId: "123", format: "rewarded", placement: "level_end" },
426
+ * );
427
+ * if (result.success) grantReward();
428
+ */
429
+ static sendAdRequest(messageType, payload, timeoutMs = 12e4) {
430
+ const expectedType = messageType === "SHOW_REWARDED_AD" /* SHOW_REWARDED_AD */ ? "rewarded" : "interstitial";
431
+ return new Promise((resolve) => {
432
+ if (!this.isNativeContext()) {
433
+ resolve({ type: expectedType, success: false, error: "no_native_context" });
434
+ return;
435
+ }
436
+ this.initialize();
437
+ const existing = this.adResultResolvers.get(expectedType);
438
+ if (existing) {
439
+ logger.warn(`[NativeBridge] Superseding in-flight ${expectedType} ad request`);
440
+ existing({ type: expectedType, success: false, error: "superseded" });
441
+ }
442
+ let settled = false;
443
+ const timer = setTimeout(() => {
444
+ if (settled) return;
445
+ settled = true;
446
+ this.adResultResolvers.delete(expectedType);
447
+ logger.warn(`[NativeBridge] Ad request timed out: ${expectedType}`);
448
+ resolve({ type: expectedType, success: false, error: "timeout" });
449
+ }, timeoutMs);
450
+ this.adResultResolvers.set(expectedType, (result) => {
451
+ if (settled) return;
452
+ settled = true;
453
+ clearTimeout(timer);
454
+ this.adResultResolvers.delete(expectedType);
455
+ resolve(result);
456
+ });
457
+ this.send(messageType, payload);
458
+ });
459
+ }
460
+ /**
461
+ * Registers a handler invoked for every `AD_RESULT` message from native,
462
+ * in addition to resolving any pending {@link sendAdRequest} promise.
463
+ * Useful for diagnostics or availability tracking.
464
+ *
465
+ * @param handler - Called with the raw `AD_RESULT` payload
466
+ */
467
+ static onAdResult(handler) {
468
+ this.on("AD_RESULT" /* AD_RESULT */, handler);
469
+ }
470
+ /**
471
+ * Resolves the pending {@link sendAdRequest} promise that matches an
472
+ * incoming `AD_RESULT` payload. No-op when nothing is waiting (e.g. a late
473
+ * result after a timeout).
474
+ */
475
+ static resolveAdResult(payload) {
476
+ if (!payload?.type) {
477
+ logger.warn("[NativeBridge] AD_RESULT received without a type");
478
+ return;
479
+ }
480
+ const resolver = this.adResultResolvers.get(payload.type);
481
+ if (resolver) {
482
+ resolver(payload);
483
+ } else {
484
+ logger.debug(`[NativeBridge] No pending ad request for: ${payload.type}`);
485
+ }
486
+ }
399
487
  };
400
488
 
401
489
  // src/utils/jwt.ts
@@ -752,10 +840,12 @@ function getAttributionData() {
752
840
  }
753
841
 
754
842
  // src/services/ads.ts
843
+ var FALLBACK_ERROR_CODES = ["not_configured", "config_fetch_failed"];
755
844
  var AdsService = class {
756
845
  config = {
757
846
  sound: "on",
758
847
  debug: false,
848
+ useNativeAds: false,
759
849
  onBeforeAd: () => {
760
850
  },
761
851
  onAfterAd: () => {
@@ -766,6 +856,16 @@ var AdsService = class {
766
856
  // Cached init promise — ensures adConfig() is only called once
767
857
  initPromise = null;
768
858
  ready = false;
859
+ // Game this SDK instance serves — sent to native so it can resolve unit IDs.
860
+ // The SDK never stores or resolves unit IDs itself (Decision 7).
861
+ gameId = null;
862
+ /**
863
+ * Set the game ID used in native ad requests. Called by the client once the
864
+ * game ID is known (from URL params or JWT). No-op for the web ad path.
865
+ */
866
+ setGameId(gameId) {
867
+ this.gameId = gameId;
868
+ }
769
869
  /**
770
870
  * Optionally configure the ads service.
771
871
  * Not required — ads work without calling this.
@@ -779,7 +879,10 @@ var AdsService = class {
779
879
  onRewardEarned: config.onRewardEarned ?? this.config.onRewardEarned
780
880
  };
781
881
  if (this.config.debug) {
782
- logger.debug("[AdsService] Configuration updated:", { sound: this.config.sound });
882
+ logger.debug("[AdsService] Configuration updated:", {
883
+ sound: this.config.sound,
884
+ useNativeAds: this.config.useNativeAds
885
+ });
783
886
  }
784
887
  }
785
888
  /**
@@ -816,10 +919,93 @@ var AdsService = class {
816
919
  }
817
920
  /**
818
921
  * Show an ad. Auto-initializes on the first call.
819
- * Returns immediately with success: false if ads are disabled or unavailable.
922
+ *
923
+ * Inside the Hyve mobile shell (`window.ReactNativeWebView` present) with
924
+ * `useNativeAds` enabled, the request is routed to native AdMob via the
925
+ * bridge. Otherwise — or when native reports the slot is `not_configured` /
926
+ * `config_fetch_failed` — it falls back to the Google H5 web path.
927
+ *
928
+ * Returns `success: false` if ads are disabled or unavailable.
929
+ *
930
+ * @param type - Ad type to show
931
+ * @param placement - Optional placement key, forwarded to native; ignored on
932
+ * the web path (H5 has no placement concept).
820
933
  */
821
- async show(type) {
934
+ async show(type, placement) {
822
935
  const requestedAt = Date.now();
936
+ if (this.isNativeAdsContext()) {
937
+ const nativeResult = await this.showNative(type, placement, requestedAt);
938
+ if (nativeResult) return nativeResult;
939
+ }
940
+ return this.showWeb(type, requestedAt, true);
941
+ }
942
+ /**
943
+ * True when running inside the Hyve mobile shell with native ads enabled.
944
+ */
945
+ isNativeAdsContext() {
946
+ return NativeBridge.isNativeContext() && this.config.useNativeAds === true;
947
+ }
948
+ /**
949
+ * Route an ad request to native AdMob via the bridge (Decision 7 — native
950
+ * owns unit-ID resolution; the SDK only sends `{ gameId, format, placement }`).
951
+ *
952
+ * Returns the resolved `AdResult`, or `null` to indicate the caller should
953
+ * fall back to the web ad path for this single call.
954
+ */
955
+ async showNative(type, placement, requestedAt) {
956
+ if (!this.gameId) {
957
+ if (this.config.debug) {
958
+ logger.debug("[AdsService] Native ads enabled but no gameId set \u2014 using web path");
959
+ }
960
+ return null;
961
+ }
962
+ const format = type === "rewarded" ? "rewarded" : "interstitial";
963
+ const messageType = type === "rewarded" ? "SHOW_REWARDED_AD" /* SHOW_REWARDED_AD */ : "SHOW_INTERSTITIAL_AD" /* SHOW_INTERSTITIAL_AD */;
964
+ if (this.config.debug) {
965
+ logger.debug(`[AdsService] Requesting native ${type} ad (format: ${format})`, {
966
+ gameId: this.gameId,
967
+ placement
968
+ });
969
+ }
970
+ this.config.onBeforeAd(type);
971
+ const result = await NativeBridge.sendAdRequest(messageType, {
972
+ gameId: this.gameId,
973
+ format,
974
+ placement
975
+ });
976
+ const completedAt = Date.now();
977
+ if (result.error && FALLBACK_ERROR_CODES.includes(result.error)) {
978
+ if (this.config.debug) {
979
+ logger.debug(`[AdsService] Native ad ${result.error} \u2014 falling back to web path`);
980
+ }
981
+ return this.showWeb(type, requestedAt, false);
982
+ }
983
+ this.config.onAfterAd(type);
984
+ if (type === "rewarded" && result.success) {
985
+ this.config.onRewardEarned();
986
+ }
987
+ if (this.config.debug) {
988
+ logger.debug("[AdsService] Native ad result:", {
989
+ type,
990
+ success: result.success,
991
+ error: result.error
992
+ });
993
+ }
994
+ return {
995
+ success: result.success,
996
+ type,
997
+ error: result.error ? new Error(result.error) : void 0,
998
+ requestedAt,
999
+ completedAt
1000
+ };
1001
+ }
1002
+ /**
1003
+ * Show an ad via the Google H5 web path. Auto-initializes on first call.
1004
+ *
1005
+ * @param fireBeforeAd - When false, `onBeforeAd` is not fired (the native
1006
+ * fallback path already fired it before delegating here).
1007
+ */
1008
+ async showWeb(type, requestedAt, fireBeforeAd) {
823
1009
  const ready = await this.initialize();
824
1010
  if (!ready || !window.adBreak) {
825
1011
  return {
@@ -830,12 +1016,15 @@ var AdsService = class {
830
1016
  completedAt: Date.now()
831
1017
  };
832
1018
  }
833
- return this.showAdBreak(type);
1019
+ return this.showAdBreak(type, fireBeforeAd);
834
1020
  }
835
1021
  /**
836
1022
  * Show an ad break via the Google H5 API.
1023
+ *
1024
+ * @param fireBeforeAd - When false, skips `onBeforeAd` (already fired by the
1025
+ * native fallback path that delegated here).
837
1026
  */
838
- async showAdBreak(type) {
1027
+ async showAdBreak(type, fireBeforeAd = true) {
839
1028
  const requestedAt = Date.now();
840
1029
  return new Promise((resolve) => {
841
1030
  const googleType = type === "rewarded" ? "reward" : type === "preroll" ? "start" : "next";
@@ -843,7 +1032,9 @@ var AdsService = class {
843
1032
  if (this.config.debug) {
844
1033
  logger.debug(`[AdsService] Showing ${type} ad`);
845
1034
  }
846
- this.config.onBeforeAd(type);
1035
+ if (fireBeforeAd) {
1036
+ this.config.onBeforeAd(type);
1037
+ }
847
1038
  const adBreakConfig = {
848
1039
  type: googleType,
849
1040
  name: adName,
@@ -2017,6 +2208,7 @@ var HyveClient = class {
2017
2208
  this.gameId = params.gameId;
2018
2209
  logger.info("Game ID extracted from game-id parameter:", this.gameId);
2019
2210
  }
2211
+ this.adsService.setGameId(this.gameId);
2020
2212
  if (this.jwtToken) {
2021
2213
  logger.info("Authentication successful via JWT");
2022
2214
  } else {
@@ -2122,6 +2314,93 @@ var HyveClient = class {
2122
2314
  return false;
2123
2315
  }
2124
2316
  }
2317
+ /**
2318
+ * Required lifecycle telemetry — Session start.
2319
+ * See https://docs.hyve.gg/docs/telemetry#required-lifecycle-events
2320
+ */
2321
+ async sessionStart() {
2322
+ return this.sendTelemetry("game", "session", "start");
2323
+ }
2324
+ /**
2325
+ * Required lifecycle telemetry — Session end.
2326
+ */
2327
+ async sessionEnd() {
2328
+ return this.sendTelemetry("game", "session", "end");
2329
+ }
2330
+ /**
2331
+ * Required lifecycle telemetry — Lobby loading start.
2332
+ */
2333
+ async lobbyLoadingStart() {
2334
+ return this.sendTelemetry("game", "loading", "start");
2335
+ }
2336
+ /**
2337
+ * Required lifecycle telemetry — Lobby loading end.
2338
+ */
2339
+ async lobbyLoadingEnd() {
2340
+ return this.sendTelemetry("game", "loading", "end");
2341
+ }
2342
+ /**
2343
+ * Required lifecycle telemetry — Game loading start.
2344
+ */
2345
+ async gameLoadingStart() {
2346
+ return this.sendTelemetry("game", "game_loading", "start");
2347
+ }
2348
+ /**
2349
+ * Required lifecycle telemetry — Game loading end.
2350
+ */
2351
+ async gameLoadingEnd() {
2352
+ return this.sendTelemetry("game", "game_loading", "end");
2353
+ }
2354
+ /**
2355
+ * Required lifecycle telemetry — Lobby initialization.
2356
+ */
2357
+ async lobbyInit() {
2358
+ return this.sendTelemetry("game", "lobby", "init");
2359
+ }
2360
+ /**
2361
+ * Required lifecycle telemetry — Gameplay end.
2362
+ * Also notifies CrazyGames that gameplay has stopped.
2363
+ */
2364
+ async gameplayEnd() {
2365
+ if (this.crazyGamesService) {
2366
+ if (this.crazyGamesInitPromise) await this.crazyGamesInitPromise;
2367
+ this.crazyGamesService.gameplayStop();
2368
+ }
2369
+ return this.sendTelemetry("game", "gameplay", "end");
2370
+ }
2371
+ /**
2372
+ * Required lifecycle telemetry — Store opened.
2373
+ */
2374
+ async storeOpen() {
2375
+ return this.sendTelemetry("game", "store", "start");
2376
+ }
2377
+ /**
2378
+ * Required lifecycle telemetry — Purchase complete.
2379
+ * @param itemName Name of the purchased item (required by the platform)
2380
+ */
2381
+ async purchaseComplete(itemName) {
2382
+ return this.sendTelemetry("game", "purchase", "complete", null, null, {
2383
+ item_name: itemName
2384
+ });
2385
+ }
2386
+ /**
2387
+ * Required lifecycle telemetry — Purchase failed.
2388
+ * @param itemName Name of the item the purchase was for
2389
+ */
2390
+ async purchaseFail(itemName) {
2391
+ return this.sendTelemetry("game", "purchase", "fail", null, null, {
2392
+ item_name: itemName
2393
+ });
2394
+ }
2395
+ /**
2396
+ * Required lifecycle telemetry — Purchase cancelled.
2397
+ * @param itemName Name of the item the purchase was for
2398
+ */
2399
+ async purchaseCancel(itemName) {
2400
+ return this.sendTelemetry("game", "purchase", "cancel", null, null, {
2401
+ item_name: itemName
2402
+ });
2403
+ }
2125
2404
  /**
2126
2405
  * Makes an authenticated API call using the JWT token
2127
2406
  * @param endpoint API endpoint path (will be appended to base URL)
@@ -2243,6 +2522,7 @@ var HyveClient = class {
2243
2522
  this.userId = null;
2244
2523
  this.jwtToken = null;
2245
2524
  this.gameId = null;
2525
+ this.adsService.setGameId(null);
2246
2526
  logger.info("User logged out");
2247
2527
  }
2248
2528
  /**
@@ -2417,9 +2697,11 @@ var HyveClient = class {
2417
2697
  /**
2418
2698
  * Show an ad
2419
2699
  * @param type Type of ad to show ('rewarded', 'interstitial', or 'preroll')
2700
+ * @param placement Optional placement key, forwarded to the native AdMob path
2701
+ * to resolve a per-placement unit ID. Ignored on the web (H5/Playgama) path.
2420
2702
  * @returns Promise resolving to ad result
2421
2703
  */
2422
- async showAd(type) {
2704
+ async showAd(type, placement) {
2423
2705
  if (this.crazyGamesService) {
2424
2706
  if (this.crazyGamesInitPromise) {
2425
2707
  await this.crazyGamesInitPromise;
@@ -2458,17 +2740,18 @@ var HyveClient = class {
2458
2740
  });
2459
2741
  }
2460
2742
  }
2461
- return this.adsService.show(type);
2743
+ return this.adsService.show(type, placement);
2462
2744
  }
2463
2745
  /**
2464
- * Notifies CrazyGames that gameplay has started.
2465
- * No-op on other platforms.
2746
+ * Required lifecycle telemetry Gameplay start.
2747
+ * Also notifies CrazyGames that gameplay has started.
2466
2748
  */
2467
2749
  async gameplayStart() {
2468
2750
  if (this.crazyGamesService) {
2469
2751
  if (this.crazyGamesInitPromise) await this.crazyGamesInitPromise;
2470
2752
  this.crazyGamesService.gameplayStart();
2471
2753
  }
2754
+ return this.sendTelemetry("game", "gameplay", "start");
2472
2755
  }
2473
2756
  /**
2474
2757
  * Notifies CrazyGames that gameplay has stopped.