@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/react.js CHANGED
@@ -170,6 +170,12 @@ function parseUrlParams(searchParams) {
170
170
  var NativeBridge = class {
171
171
  static handlers = /* @__PURE__ */ new Map();
172
172
  static isInitialized = false;
173
+ /**
174
+ * Pending `sendAdRequest()` promises, keyed by the ad type native echoes
175
+ * back in `AD_RESULT.type` ("rewarded" | "interstitial"). One in-flight
176
+ * request per type — ads are shown sequentially.
177
+ */
178
+ static adResultResolvers = /* @__PURE__ */ new Map();
173
179
  /**
174
180
  * Checks if the app is running inside a React Native WebView
175
181
  */
@@ -222,16 +228,20 @@ var NativeBridge = class {
222
228
  "PUSH_PERMISSION_DENIED" /* PUSH_PERMISSION_DENIED */,
223
229
  "PRODUCTS_RESULT" /* PRODUCTS_RESULT */,
224
230
  "PURCHASE_COMPLETE" /* PURCHASE_COMPLETE */,
225
- "PURCHASE_ERROR" /* PURCHASE_ERROR */
231
+ "PURCHASE_ERROR" /* PURCHASE_ERROR */,
232
+ "AD_RESULT" /* AD_RESULT */
226
233
  ];
227
234
  if (!nativeResponseTypes.includes(data.type)) {
228
235
  return;
229
236
  }
237
+ if (data.type === "AD_RESULT" /* AD_RESULT */) {
238
+ this.resolveAdResult(data.payload);
239
+ }
230
240
  const handler = this.handlers.get(data.type);
231
241
  if (handler) {
232
242
  logger.debug(`[NativeBridge] Handling message: ${data.type}`);
233
243
  handler(data.payload);
234
- } else {
244
+ } else if (data.type !== "AD_RESULT" /* AD_RESULT */) {
235
245
  logger.warn(`[NativeBridge] No handler registered for: ${data.type}`);
236
246
  }
237
247
  } catch (error) {
@@ -357,6 +367,81 @@ var NativeBridge = class {
357
367
  static purchase(productId, userId) {
358
368
  this.send("PURCHASE" /* PURCHASE */, { productId, userId });
359
369
  }
370
+ /**
371
+ * Sends a native ad request and resolves with the `AD_RESULT` native sends
372
+ * back. Never rejects — a missing native context, timeout, or superseded
373
+ * request resolves to `{ success: false }` with an error code so callers can
374
+ * decide whether to fall back to the web ad path.
375
+ *
376
+ * @param messageType - `SHOW_REWARDED_AD` or `SHOW_INTERSTITIAL_AD`
377
+ * @param payload - `{ gameId, format, placement? }`
378
+ * @param timeoutMs - How long to wait for `AD_RESULT` before giving up
379
+ *
380
+ * @example
381
+ * const result = await NativeBridge.sendAdRequest(
382
+ * NativeMessageType.SHOW_REWARDED_AD,
383
+ * { gameId: "123", format: "rewarded", placement: "level_end" },
384
+ * );
385
+ * if (result.success) grantReward();
386
+ */
387
+ static sendAdRequest(messageType, payload, timeoutMs = 12e4) {
388
+ const expectedType = messageType === "SHOW_REWARDED_AD" /* SHOW_REWARDED_AD */ ? "rewarded" : "interstitial";
389
+ return new Promise((resolve) => {
390
+ if (!this.isNativeContext()) {
391
+ resolve({ type: expectedType, success: false, error: "no_native_context" });
392
+ return;
393
+ }
394
+ this.initialize();
395
+ const existing = this.adResultResolvers.get(expectedType);
396
+ if (existing) {
397
+ logger.warn(`[NativeBridge] Superseding in-flight ${expectedType} ad request`);
398
+ existing({ type: expectedType, success: false, error: "superseded" });
399
+ }
400
+ let settled = false;
401
+ const timer = setTimeout(() => {
402
+ if (settled) return;
403
+ settled = true;
404
+ this.adResultResolvers.delete(expectedType);
405
+ logger.warn(`[NativeBridge] Ad request timed out: ${expectedType}`);
406
+ resolve({ type: expectedType, success: false, error: "timeout" });
407
+ }, timeoutMs);
408
+ this.adResultResolvers.set(expectedType, (result) => {
409
+ if (settled) return;
410
+ settled = true;
411
+ clearTimeout(timer);
412
+ this.adResultResolvers.delete(expectedType);
413
+ resolve(result);
414
+ });
415
+ this.send(messageType, payload);
416
+ });
417
+ }
418
+ /**
419
+ * Registers a handler invoked for every `AD_RESULT` message from native,
420
+ * in addition to resolving any pending {@link sendAdRequest} promise.
421
+ * Useful for diagnostics or availability tracking.
422
+ *
423
+ * @param handler - Called with the raw `AD_RESULT` payload
424
+ */
425
+ static onAdResult(handler) {
426
+ this.on("AD_RESULT" /* AD_RESULT */, handler);
427
+ }
428
+ /**
429
+ * Resolves the pending {@link sendAdRequest} promise that matches an
430
+ * incoming `AD_RESULT` payload. No-op when nothing is waiting (e.g. a late
431
+ * result after a timeout).
432
+ */
433
+ static resolveAdResult(payload) {
434
+ if (!payload?.type) {
435
+ logger.warn("[NativeBridge] AD_RESULT received without a type");
436
+ return;
437
+ }
438
+ const resolver = this.adResultResolvers.get(payload.type);
439
+ if (resolver) {
440
+ resolver(payload);
441
+ } else {
442
+ logger.debug(`[NativeBridge] No pending ad request for: ${payload.type}`);
443
+ }
444
+ }
360
445
  };
361
446
 
362
447
  // src/utils/jwt.ts
@@ -713,10 +798,12 @@ function getAttributionData() {
713
798
  }
714
799
 
715
800
  // src/services/ads.ts
801
+ var FALLBACK_ERROR_CODES = ["not_configured", "config_fetch_failed"];
716
802
  var AdsService = class {
717
803
  config = {
718
804
  sound: "on",
719
805
  debug: false,
806
+ useNativeAds: false,
720
807
  onBeforeAd: () => {
721
808
  },
722
809
  onAfterAd: () => {
@@ -727,6 +814,16 @@ var AdsService = class {
727
814
  // Cached init promise — ensures adConfig() is only called once
728
815
  initPromise = null;
729
816
  ready = false;
817
+ // Game this SDK instance serves — sent to native so it can resolve unit IDs.
818
+ // The SDK never stores or resolves unit IDs itself (Decision 7).
819
+ gameId = null;
820
+ /**
821
+ * Set the game ID used in native ad requests. Called by the client once the
822
+ * game ID is known (from URL params or JWT). No-op for the web ad path.
823
+ */
824
+ setGameId(gameId) {
825
+ this.gameId = gameId;
826
+ }
730
827
  /**
731
828
  * Optionally configure the ads service.
732
829
  * Not required — ads work without calling this.
@@ -740,7 +837,10 @@ var AdsService = class {
740
837
  onRewardEarned: config.onRewardEarned ?? this.config.onRewardEarned
741
838
  };
742
839
  if (this.config.debug) {
743
- logger.debug("[AdsService] Configuration updated:", { sound: this.config.sound });
840
+ logger.debug("[AdsService] Configuration updated:", {
841
+ sound: this.config.sound,
842
+ useNativeAds: this.config.useNativeAds
843
+ });
744
844
  }
745
845
  }
746
846
  /**
@@ -777,10 +877,93 @@ var AdsService = class {
777
877
  }
778
878
  /**
779
879
  * Show an ad. Auto-initializes on the first call.
780
- * Returns immediately with success: false if ads are disabled or unavailable.
880
+ *
881
+ * Inside the Hyve mobile shell (`window.ReactNativeWebView` present) with
882
+ * `useNativeAds` enabled, the request is routed to native AdMob via the
883
+ * bridge. Otherwise — or when native reports the slot is `not_configured` /
884
+ * `config_fetch_failed` — it falls back to the Google H5 web path.
885
+ *
886
+ * Returns `success: false` if ads are disabled or unavailable.
887
+ *
888
+ * @param type - Ad type to show
889
+ * @param placement - Optional placement key, forwarded to native; ignored on
890
+ * the web path (H5 has no placement concept).
781
891
  */
782
- async show(type) {
892
+ async show(type, placement) {
783
893
  const requestedAt = Date.now();
894
+ if (this.isNativeAdsContext()) {
895
+ const nativeResult = await this.showNative(type, placement, requestedAt);
896
+ if (nativeResult) return nativeResult;
897
+ }
898
+ return this.showWeb(type, requestedAt, true);
899
+ }
900
+ /**
901
+ * True when running inside the Hyve mobile shell with native ads enabled.
902
+ */
903
+ isNativeAdsContext() {
904
+ return NativeBridge.isNativeContext() && this.config.useNativeAds === true;
905
+ }
906
+ /**
907
+ * Route an ad request to native AdMob via the bridge (Decision 7 — native
908
+ * owns unit-ID resolution; the SDK only sends `{ gameId, format, placement }`).
909
+ *
910
+ * Returns the resolved `AdResult`, or `null` to indicate the caller should
911
+ * fall back to the web ad path for this single call.
912
+ */
913
+ async showNative(type, placement, requestedAt) {
914
+ if (!this.gameId) {
915
+ if (this.config.debug) {
916
+ logger.debug("[AdsService] Native ads enabled but no gameId set \u2014 using web path");
917
+ }
918
+ return null;
919
+ }
920
+ const format = type === "rewarded" ? "rewarded" : "interstitial";
921
+ const messageType = type === "rewarded" ? "SHOW_REWARDED_AD" /* SHOW_REWARDED_AD */ : "SHOW_INTERSTITIAL_AD" /* SHOW_INTERSTITIAL_AD */;
922
+ if (this.config.debug) {
923
+ logger.debug(`[AdsService] Requesting native ${type} ad (format: ${format})`, {
924
+ gameId: this.gameId,
925
+ placement
926
+ });
927
+ }
928
+ this.config.onBeforeAd(type);
929
+ const result = await NativeBridge.sendAdRequest(messageType, {
930
+ gameId: this.gameId,
931
+ format,
932
+ placement
933
+ });
934
+ const completedAt = Date.now();
935
+ if (result.error && FALLBACK_ERROR_CODES.includes(result.error)) {
936
+ if (this.config.debug) {
937
+ logger.debug(`[AdsService] Native ad ${result.error} \u2014 falling back to web path`);
938
+ }
939
+ return this.showWeb(type, requestedAt, false);
940
+ }
941
+ this.config.onAfterAd(type);
942
+ if (type === "rewarded" && result.success) {
943
+ this.config.onRewardEarned();
944
+ }
945
+ if (this.config.debug) {
946
+ logger.debug("[AdsService] Native ad result:", {
947
+ type,
948
+ success: result.success,
949
+ error: result.error
950
+ });
951
+ }
952
+ return {
953
+ success: result.success,
954
+ type,
955
+ error: result.error ? new Error(result.error) : void 0,
956
+ requestedAt,
957
+ completedAt
958
+ };
959
+ }
960
+ /**
961
+ * Show an ad via the Google H5 web path. Auto-initializes on first call.
962
+ *
963
+ * @param fireBeforeAd - When false, `onBeforeAd` is not fired (the native
964
+ * fallback path already fired it before delegating here).
965
+ */
966
+ async showWeb(type, requestedAt, fireBeforeAd) {
784
967
  const ready = await this.initialize();
785
968
  if (!ready || !window.adBreak) {
786
969
  return {
@@ -791,12 +974,15 @@ var AdsService = class {
791
974
  completedAt: Date.now()
792
975
  };
793
976
  }
794
- return this.showAdBreak(type);
977
+ return this.showAdBreak(type, fireBeforeAd);
795
978
  }
796
979
  /**
797
980
  * Show an ad break via the Google H5 API.
981
+ *
982
+ * @param fireBeforeAd - When false, skips `onBeforeAd` (already fired by the
983
+ * native fallback path that delegated here).
798
984
  */
799
- async showAdBreak(type) {
985
+ async showAdBreak(type, fireBeforeAd = true) {
800
986
  const requestedAt = Date.now();
801
987
  return new Promise((resolve) => {
802
988
  const googleType = type === "rewarded" ? "reward" : type === "preroll" ? "start" : "next";
@@ -804,7 +990,9 @@ var AdsService = class {
804
990
  if (this.config.debug) {
805
991
  logger.debug(`[AdsService] Showing ${type} ad`);
806
992
  }
807
- this.config.onBeforeAd(type);
993
+ if (fireBeforeAd) {
994
+ this.config.onBeforeAd(type);
995
+ }
808
996
  const adBreakConfig = {
809
997
  type: googleType,
810
998
  name: adName,
@@ -1972,6 +2160,7 @@ var HyveClient = class {
1972
2160
  this.gameId = params.gameId;
1973
2161
  logger.info("Game ID extracted from game-id parameter:", this.gameId);
1974
2162
  }
2163
+ this.adsService.setGameId(this.gameId);
1975
2164
  if (this.jwtToken) {
1976
2165
  logger.info("Authentication successful via JWT");
1977
2166
  } else {
@@ -2077,6 +2266,93 @@ var HyveClient = class {
2077
2266
  return false;
2078
2267
  }
2079
2268
  }
2269
+ /**
2270
+ * Required lifecycle telemetry — Session start.
2271
+ * See https://docs.hyve.gg/docs/telemetry#required-lifecycle-events
2272
+ */
2273
+ async sessionStart() {
2274
+ return this.sendTelemetry("game", "session", "start");
2275
+ }
2276
+ /**
2277
+ * Required lifecycle telemetry — Session end.
2278
+ */
2279
+ async sessionEnd() {
2280
+ return this.sendTelemetry("game", "session", "end");
2281
+ }
2282
+ /**
2283
+ * Required lifecycle telemetry — Lobby loading start.
2284
+ */
2285
+ async lobbyLoadingStart() {
2286
+ return this.sendTelemetry("game", "loading", "start");
2287
+ }
2288
+ /**
2289
+ * Required lifecycle telemetry — Lobby loading end.
2290
+ */
2291
+ async lobbyLoadingEnd() {
2292
+ return this.sendTelemetry("game", "loading", "end");
2293
+ }
2294
+ /**
2295
+ * Required lifecycle telemetry — Game loading start.
2296
+ */
2297
+ async gameLoadingStart() {
2298
+ return this.sendTelemetry("game", "game_loading", "start");
2299
+ }
2300
+ /**
2301
+ * Required lifecycle telemetry — Game loading end.
2302
+ */
2303
+ async gameLoadingEnd() {
2304
+ return this.sendTelemetry("game", "game_loading", "end");
2305
+ }
2306
+ /**
2307
+ * Required lifecycle telemetry — Lobby initialization.
2308
+ */
2309
+ async lobbyInit() {
2310
+ return this.sendTelemetry("game", "lobby", "init");
2311
+ }
2312
+ /**
2313
+ * Required lifecycle telemetry — Gameplay end.
2314
+ * Also notifies CrazyGames that gameplay has stopped.
2315
+ */
2316
+ async gameplayEnd() {
2317
+ if (this.crazyGamesService) {
2318
+ if (this.crazyGamesInitPromise) await this.crazyGamesInitPromise;
2319
+ this.crazyGamesService.gameplayStop();
2320
+ }
2321
+ return this.sendTelemetry("game", "gameplay", "end");
2322
+ }
2323
+ /**
2324
+ * Required lifecycle telemetry — Store opened.
2325
+ */
2326
+ async storeOpen() {
2327
+ return this.sendTelemetry("game", "store", "start");
2328
+ }
2329
+ /**
2330
+ * Required lifecycle telemetry — Purchase complete.
2331
+ * @param itemName Name of the purchased item (required by the platform)
2332
+ */
2333
+ async purchaseComplete(itemName) {
2334
+ return this.sendTelemetry("game", "purchase", "complete", null, null, {
2335
+ item_name: itemName
2336
+ });
2337
+ }
2338
+ /**
2339
+ * Required lifecycle telemetry — Purchase failed.
2340
+ * @param itemName Name of the item the purchase was for
2341
+ */
2342
+ async purchaseFail(itemName) {
2343
+ return this.sendTelemetry("game", "purchase", "fail", null, null, {
2344
+ item_name: itemName
2345
+ });
2346
+ }
2347
+ /**
2348
+ * Required lifecycle telemetry — Purchase cancelled.
2349
+ * @param itemName Name of the item the purchase was for
2350
+ */
2351
+ async purchaseCancel(itemName) {
2352
+ return this.sendTelemetry("game", "purchase", "cancel", null, null, {
2353
+ item_name: itemName
2354
+ });
2355
+ }
2080
2356
  /**
2081
2357
  * Makes an authenticated API call using the JWT token
2082
2358
  * @param endpoint API endpoint path (will be appended to base URL)
@@ -2198,6 +2474,7 @@ var HyveClient = class {
2198
2474
  this.userId = null;
2199
2475
  this.jwtToken = null;
2200
2476
  this.gameId = null;
2477
+ this.adsService.setGameId(null);
2201
2478
  logger.info("User logged out");
2202
2479
  }
2203
2480
  /**
@@ -2372,9 +2649,11 @@ var HyveClient = class {
2372
2649
  /**
2373
2650
  * Show an ad
2374
2651
  * @param type Type of ad to show ('rewarded', 'interstitial', or 'preroll')
2652
+ * @param placement Optional placement key, forwarded to the native AdMob path
2653
+ * to resolve a per-placement unit ID. Ignored on the web (H5/Playgama) path.
2375
2654
  * @returns Promise resolving to ad result
2376
2655
  */
2377
- async showAd(type) {
2656
+ async showAd(type, placement) {
2378
2657
  if (this.crazyGamesService) {
2379
2658
  if (this.crazyGamesInitPromise) {
2380
2659
  await this.crazyGamesInitPromise;
@@ -2413,17 +2692,18 @@ var HyveClient = class {
2413
2692
  });
2414
2693
  }
2415
2694
  }
2416
- return this.adsService.show(type);
2695
+ return this.adsService.show(type, placement);
2417
2696
  }
2418
2697
  /**
2419
- * Notifies CrazyGames that gameplay has started.
2420
- * No-op on other platforms.
2698
+ * Required lifecycle telemetry Gameplay start.
2699
+ * Also notifies CrazyGames that gameplay has started.
2421
2700
  */
2422
2701
  async gameplayStart() {
2423
2702
  if (this.crazyGamesService) {
2424
2703
  if (this.crazyGamesInitPromise) await this.crazyGamesInitPromise;
2425
2704
  this.crazyGamesService.gameplayStart();
2426
2705
  }
2706
+ return this.sendTelemetry("game", "gameplay", "start");
2427
2707
  }
2428
2708
  /**
2429
2709
  * Notifies CrazyGames that gameplay has stopped.