@hyve-sdk/js 2.12.0 → 2.13.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 {
@@ -2330,6 +2522,7 @@ var HyveClient = class {
2330
2522
  this.userId = null;
2331
2523
  this.jwtToken = null;
2332
2524
  this.gameId = null;
2525
+ this.adsService.setGameId(null);
2333
2526
  logger.info("User logged out");
2334
2527
  }
2335
2528
  /**
@@ -2504,9 +2697,11 @@ var HyveClient = class {
2504
2697
  /**
2505
2698
  * Show an ad
2506
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.
2507
2702
  * @returns Promise resolving to ad result
2508
2703
  */
2509
- async showAd(type) {
2704
+ async showAd(type, placement) {
2510
2705
  if (this.crazyGamesService) {
2511
2706
  if (this.crazyGamesInitPromise) {
2512
2707
  await this.crazyGamesInitPromise;
@@ -2545,7 +2740,7 @@ var HyveClient = class {
2545
2740
  });
2546
2741
  }
2547
2742
  }
2548
- return this.adsService.show(type);
2743
+ return this.adsService.show(type, placement);
2549
2744
  }
2550
2745
  /**
2551
2746
  * Required lifecycle telemetry — Gameplay start.
package/dist/index.mjs CHANGED
@@ -158,17 +158,26 @@ var NativeMessageType = /* @__PURE__ */ ((NativeMessageType2) => {
158
158
  NativeMessageType2["REQUEST_NOTIFICATION_PERMISSION"] = "REQUEST_NOTIFICATION_PERMISSION";
159
159
  NativeMessageType2["GET_PRODUCTS"] = "GET_PRODUCTS";
160
160
  NativeMessageType2["PURCHASE"] = "PURCHASE";
161
+ NativeMessageType2["SHOW_REWARDED_AD"] = "SHOW_REWARDED_AD";
162
+ NativeMessageType2["SHOW_INTERSTITIAL_AD"] = "SHOW_INTERSTITIAL_AD";
161
163
  NativeMessageType2["IAP_AVAILABILITY_RESULT"] = "IAP_AVAILABILITY_RESULT";
162
164
  NativeMessageType2["PUSH_PERMISSION_GRANTED"] = "PUSH_PERMISSION_GRANTED";
163
165
  NativeMessageType2["PUSH_PERMISSION_DENIED"] = "PUSH_PERMISSION_DENIED";
164
166
  NativeMessageType2["PRODUCTS_RESULT"] = "PRODUCTS_RESULT";
165
167
  NativeMessageType2["PURCHASE_COMPLETE"] = "PURCHASE_COMPLETE";
166
168
  NativeMessageType2["PURCHASE_ERROR"] = "PURCHASE_ERROR";
169
+ NativeMessageType2["AD_RESULT"] = "AD_RESULT";
167
170
  return NativeMessageType2;
168
171
  })(NativeMessageType || {});
169
172
  var NativeBridge = class {
170
173
  static handlers = /* @__PURE__ */ new Map();
171
174
  static isInitialized = false;
175
+ /**
176
+ * Pending `sendAdRequest()` promises, keyed by the ad type native echoes
177
+ * back in `AD_RESULT.type` ("rewarded" | "interstitial"). One in-flight
178
+ * request per type — ads are shown sequentially.
179
+ */
180
+ static adResultResolvers = /* @__PURE__ */ new Map();
172
181
  /**
173
182
  * Checks if the app is running inside a React Native WebView
174
183
  */
@@ -221,16 +230,20 @@ var NativeBridge = class {
221
230
  "PUSH_PERMISSION_DENIED" /* PUSH_PERMISSION_DENIED */,
222
231
  "PRODUCTS_RESULT" /* PRODUCTS_RESULT */,
223
232
  "PURCHASE_COMPLETE" /* PURCHASE_COMPLETE */,
224
- "PURCHASE_ERROR" /* PURCHASE_ERROR */
233
+ "PURCHASE_ERROR" /* PURCHASE_ERROR */,
234
+ "AD_RESULT" /* AD_RESULT */
225
235
  ];
226
236
  if (!nativeResponseTypes.includes(data.type)) {
227
237
  return;
228
238
  }
239
+ if (data.type === "AD_RESULT" /* AD_RESULT */) {
240
+ this.resolveAdResult(data.payload);
241
+ }
229
242
  const handler = this.handlers.get(data.type);
230
243
  if (handler) {
231
244
  logger.debug(`[NativeBridge] Handling message: ${data.type}`);
232
245
  handler(data.payload);
233
- } else {
246
+ } else if (data.type !== "AD_RESULT" /* AD_RESULT */) {
234
247
  logger.warn(`[NativeBridge] No handler registered for: ${data.type}`);
235
248
  }
236
249
  } catch (error) {
@@ -356,6 +369,81 @@ var NativeBridge = class {
356
369
  static purchase(productId, userId) {
357
370
  this.send("PURCHASE" /* PURCHASE */, { productId, userId });
358
371
  }
372
+ /**
373
+ * Sends a native ad request and resolves with the `AD_RESULT` native sends
374
+ * back. Never rejects — a missing native context, timeout, or superseded
375
+ * request resolves to `{ success: false }` with an error code so callers can
376
+ * decide whether to fall back to the web ad path.
377
+ *
378
+ * @param messageType - `SHOW_REWARDED_AD` or `SHOW_INTERSTITIAL_AD`
379
+ * @param payload - `{ gameId, format, placement? }`
380
+ * @param timeoutMs - How long to wait for `AD_RESULT` before giving up
381
+ *
382
+ * @example
383
+ * const result = await NativeBridge.sendAdRequest(
384
+ * NativeMessageType.SHOW_REWARDED_AD,
385
+ * { gameId: "123", format: "rewarded", placement: "level_end" },
386
+ * );
387
+ * if (result.success) grantReward();
388
+ */
389
+ static sendAdRequest(messageType, payload, timeoutMs = 12e4) {
390
+ const expectedType = messageType === "SHOW_REWARDED_AD" /* SHOW_REWARDED_AD */ ? "rewarded" : "interstitial";
391
+ return new Promise((resolve) => {
392
+ if (!this.isNativeContext()) {
393
+ resolve({ type: expectedType, success: false, error: "no_native_context" });
394
+ return;
395
+ }
396
+ this.initialize();
397
+ const existing = this.adResultResolvers.get(expectedType);
398
+ if (existing) {
399
+ logger.warn(`[NativeBridge] Superseding in-flight ${expectedType} ad request`);
400
+ existing({ type: expectedType, success: false, error: "superseded" });
401
+ }
402
+ let settled = false;
403
+ const timer = setTimeout(() => {
404
+ if (settled) return;
405
+ settled = true;
406
+ this.adResultResolvers.delete(expectedType);
407
+ logger.warn(`[NativeBridge] Ad request timed out: ${expectedType}`);
408
+ resolve({ type: expectedType, success: false, error: "timeout" });
409
+ }, timeoutMs);
410
+ this.adResultResolvers.set(expectedType, (result) => {
411
+ if (settled) return;
412
+ settled = true;
413
+ clearTimeout(timer);
414
+ this.adResultResolvers.delete(expectedType);
415
+ resolve(result);
416
+ });
417
+ this.send(messageType, payload);
418
+ });
419
+ }
420
+ /**
421
+ * Registers a handler invoked for every `AD_RESULT` message from native,
422
+ * in addition to resolving any pending {@link sendAdRequest} promise.
423
+ * Useful for diagnostics or availability tracking.
424
+ *
425
+ * @param handler - Called with the raw `AD_RESULT` payload
426
+ */
427
+ static onAdResult(handler) {
428
+ this.on("AD_RESULT" /* AD_RESULT */, handler);
429
+ }
430
+ /**
431
+ * Resolves the pending {@link sendAdRequest} promise that matches an
432
+ * incoming `AD_RESULT` payload. No-op when nothing is waiting (e.g. a late
433
+ * result after a timeout).
434
+ */
435
+ static resolveAdResult(payload) {
436
+ if (!payload?.type) {
437
+ logger.warn("[NativeBridge] AD_RESULT received without a type");
438
+ return;
439
+ }
440
+ const resolver = this.adResultResolvers.get(payload.type);
441
+ if (resolver) {
442
+ resolver(payload);
443
+ } else {
444
+ logger.debug(`[NativeBridge] No pending ad request for: ${payload.type}`);
445
+ }
446
+ }
359
447
  };
360
448
 
361
449
  // src/utils/jwt.ts
@@ -712,10 +800,12 @@ function getAttributionData() {
712
800
  }
713
801
 
714
802
  // src/services/ads.ts
803
+ var FALLBACK_ERROR_CODES = ["not_configured", "config_fetch_failed"];
715
804
  var AdsService = class {
716
805
  config = {
717
806
  sound: "on",
718
807
  debug: false,
808
+ useNativeAds: false,
719
809
  onBeforeAd: () => {
720
810
  },
721
811
  onAfterAd: () => {
@@ -726,6 +816,16 @@ var AdsService = class {
726
816
  // Cached init promise — ensures adConfig() is only called once
727
817
  initPromise = null;
728
818
  ready = false;
819
+ // Game this SDK instance serves — sent to native so it can resolve unit IDs.
820
+ // The SDK never stores or resolves unit IDs itself (Decision 7).
821
+ gameId = null;
822
+ /**
823
+ * Set the game ID used in native ad requests. Called by the client once the
824
+ * game ID is known (from URL params or JWT). No-op for the web ad path.
825
+ */
826
+ setGameId(gameId) {
827
+ this.gameId = gameId;
828
+ }
729
829
  /**
730
830
  * Optionally configure the ads service.
731
831
  * Not required — ads work without calling this.
@@ -739,7 +839,10 @@ var AdsService = class {
739
839
  onRewardEarned: config.onRewardEarned ?? this.config.onRewardEarned
740
840
  };
741
841
  if (this.config.debug) {
742
- logger.debug("[AdsService] Configuration updated:", { sound: this.config.sound });
842
+ logger.debug("[AdsService] Configuration updated:", {
843
+ sound: this.config.sound,
844
+ useNativeAds: this.config.useNativeAds
845
+ });
743
846
  }
744
847
  }
745
848
  /**
@@ -776,10 +879,93 @@ var AdsService = class {
776
879
  }
777
880
  /**
778
881
  * Show an ad. Auto-initializes on the first call.
779
- * Returns immediately with success: false if ads are disabled or unavailable.
882
+ *
883
+ * Inside the Hyve mobile shell (`window.ReactNativeWebView` present) with
884
+ * `useNativeAds` enabled, the request is routed to native AdMob via the
885
+ * bridge. Otherwise — or when native reports the slot is `not_configured` /
886
+ * `config_fetch_failed` — it falls back to the Google H5 web path.
887
+ *
888
+ * Returns `success: false` if ads are disabled or unavailable.
889
+ *
890
+ * @param type - Ad type to show
891
+ * @param placement - Optional placement key, forwarded to native; ignored on
892
+ * the web path (H5 has no placement concept).
780
893
  */
781
- async show(type) {
894
+ async show(type, placement) {
782
895
  const requestedAt = Date.now();
896
+ if (this.isNativeAdsContext()) {
897
+ const nativeResult = await this.showNative(type, placement, requestedAt);
898
+ if (nativeResult) return nativeResult;
899
+ }
900
+ return this.showWeb(type, requestedAt, true);
901
+ }
902
+ /**
903
+ * True when running inside the Hyve mobile shell with native ads enabled.
904
+ */
905
+ isNativeAdsContext() {
906
+ return NativeBridge.isNativeContext() && this.config.useNativeAds === true;
907
+ }
908
+ /**
909
+ * Route an ad request to native AdMob via the bridge (Decision 7 — native
910
+ * owns unit-ID resolution; the SDK only sends `{ gameId, format, placement }`).
911
+ *
912
+ * Returns the resolved `AdResult`, or `null` to indicate the caller should
913
+ * fall back to the web ad path for this single call.
914
+ */
915
+ async showNative(type, placement, requestedAt) {
916
+ if (!this.gameId) {
917
+ if (this.config.debug) {
918
+ logger.debug("[AdsService] Native ads enabled but no gameId set \u2014 using web path");
919
+ }
920
+ return null;
921
+ }
922
+ const format = type === "rewarded" ? "rewarded" : "interstitial";
923
+ const messageType = type === "rewarded" ? "SHOW_REWARDED_AD" /* SHOW_REWARDED_AD */ : "SHOW_INTERSTITIAL_AD" /* SHOW_INTERSTITIAL_AD */;
924
+ if (this.config.debug) {
925
+ logger.debug(`[AdsService] Requesting native ${type} ad (format: ${format})`, {
926
+ gameId: this.gameId,
927
+ placement
928
+ });
929
+ }
930
+ this.config.onBeforeAd(type);
931
+ const result = await NativeBridge.sendAdRequest(messageType, {
932
+ gameId: this.gameId,
933
+ format,
934
+ placement
935
+ });
936
+ const completedAt = Date.now();
937
+ if (result.error && FALLBACK_ERROR_CODES.includes(result.error)) {
938
+ if (this.config.debug) {
939
+ logger.debug(`[AdsService] Native ad ${result.error} \u2014 falling back to web path`);
940
+ }
941
+ return this.showWeb(type, requestedAt, false);
942
+ }
943
+ this.config.onAfterAd(type);
944
+ if (type === "rewarded" && result.success) {
945
+ this.config.onRewardEarned();
946
+ }
947
+ if (this.config.debug) {
948
+ logger.debug("[AdsService] Native ad result:", {
949
+ type,
950
+ success: result.success,
951
+ error: result.error
952
+ });
953
+ }
954
+ return {
955
+ success: result.success,
956
+ type,
957
+ error: result.error ? new Error(result.error) : void 0,
958
+ requestedAt,
959
+ completedAt
960
+ };
961
+ }
962
+ /**
963
+ * Show an ad via the Google H5 web path. Auto-initializes on first call.
964
+ *
965
+ * @param fireBeforeAd - When false, `onBeforeAd` is not fired (the native
966
+ * fallback path already fired it before delegating here).
967
+ */
968
+ async showWeb(type, requestedAt, fireBeforeAd) {
783
969
  const ready = await this.initialize();
784
970
  if (!ready || !window.adBreak) {
785
971
  return {
@@ -790,12 +976,15 @@ var AdsService = class {
790
976
  completedAt: Date.now()
791
977
  };
792
978
  }
793
- return this.showAdBreak(type);
979
+ return this.showAdBreak(type, fireBeforeAd);
794
980
  }
795
981
  /**
796
982
  * Show an ad break via the Google H5 API.
983
+ *
984
+ * @param fireBeforeAd - When false, skips `onBeforeAd` (already fired by the
985
+ * native fallback path that delegated here).
797
986
  */
798
- async showAdBreak(type) {
987
+ async showAdBreak(type, fireBeforeAd = true) {
799
988
  const requestedAt = Date.now();
800
989
  return new Promise((resolve) => {
801
990
  const googleType = type === "rewarded" ? "reward" : type === "preroll" ? "start" : "next";
@@ -803,7 +992,9 @@ var AdsService = class {
803
992
  if (this.config.debug) {
804
993
  logger.debug(`[AdsService] Showing ${type} ad`);
805
994
  }
806
- this.config.onBeforeAd(type);
995
+ if (fireBeforeAd) {
996
+ this.config.onBeforeAd(type);
997
+ }
807
998
  const adBreakConfig = {
808
999
  type: googleType,
809
1000
  name: adName,
@@ -1977,6 +2168,7 @@ var HyveClient = class {
1977
2168
  this.gameId = params.gameId;
1978
2169
  logger.info("Game ID extracted from game-id parameter:", this.gameId);
1979
2170
  }
2171
+ this.adsService.setGameId(this.gameId);
1980
2172
  if (this.jwtToken) {
1981
2173
  logger.info("Authentication successful via JWT");
1982
2174
  } else {
@@ -2290,6 +2482,7 @@ var HyveClient = class {
2290
2482
  this.userId = null;
2291
2483
  this.jwtToken = null;
2292
2484
  this.gameId = null;
2485
+ this.adsService.setGameId(null);
2293
2486
  logger.info("User logged out");
2294
2487
  }
2295
2488
  /**
@@ -2464,9 +2657,11 @@ var HyveClient = class {
2464
2657
  /**
2465
2658
  * Show an ad
2466
2659
  * @param type Type of ad to show ('rewarded', 'interstitial', or 'preroll')
2660
+ * @param placement Optional placement key, forwarded to the native AdMob path
2661
+ * to resolve a per-placement unit ID. Ignored on the web (H5/Playgama) path.
2467
2662
  * @returns Promise resolving to ad result
2468
2663
  */
2469
- async showAd(type) {
2664
+ async showAd(type, placement) {
2470
2665
  if (this.crazyGamesService) {
2471
2666
  if (this.crazyGamesInitPromise) {
2472
2667
  await this.crazyGamesInitPromise;
@@ -2505,7 +2700,7 @@ var HyveClient = class {
2505
2700
  });
2506
2701
  }
2507
2702
  }
2508
- return this.adsService.show(type);
2703
+ return this.adsService.show(type, placement);
2509
2704
  }
2510
2705
  /**
2511
2706
  * Required lifecycle telemetry — Gameplay start.