@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/react.d.mts CHANGED
@@ -138,6 +138,10 @@ interface GetGameDataLeaderboardParams {
138
138
  *
139
139
  * @packageDocumentation
140
140
  */
141
+ /**
142
+ * Ad types the SDK's `show()` accepts. Limited to formats with a working
143
+ * native path; `'preroll'` maps to a native interstitial (Decision 2).
144
+ */
141
145
  type AdType = 'rewarded' | 'interstitial' | 'preroll';
142
146
  interface AdResult {
143
147
  success: boolean;
@@ -149,6 +153,19 @@ interface AdResult {
149
153
  interface AdConfig {
150
154
  sound?: 'on' | 'off';
151
155
  debug?: boolean;
156
+ /**
157
+ * Route ad requests through the native AdMob bridge when running inside the
158
+ * Hyve mobile shell (`window.ReactNativeWebView` present).
159
+ *
160
+ * Phase 0 rollout flag: defaults to `false`, where ads always use the
161
+ * existing H5 / Playgama web path. The native path is wired up behind this
162
+ * flag in a later phase — until then this is an inert toggle that exists so
163
+ * later releases can flip behavior without an API change. See
164
+ * `docs/admob-migration.md`.
165
+ *
166
+ * @default false
167
+ */
168
+ useNativeAds?: boolean;
152
169
  onBeforeAd?: (type: AdType) => void;
153
170
  onAfterAd?: (type: AdType) => void;
154
171
  onRewardEarned?: () => void;
@@ -480,9 +497,11 @@ declare class HyveClient {
480
497
  /**
481
498
  * Show an ad
482
499
  * @param type Type of ad to show ('rewarded', 'interstitial', or 'preroll')
500
+ * @param placement Optional placement key, forwarded to the native AdMob path
501
+ * to resolve a per-placement unit ID. Ignored on the web (H5/Playgama) path.
483
502
  * @returns Promise resolving to ad result
484
503
  */
485
- showAd(type: AdType): Promise<AdResult>;
504
+ showAd(type: AdType, placement?: string): Promise<AdResult>;
486
505
  /**
487
506
  * Required lifecycle telemetry — Gameplay start.
488
507
  * Also notifies CrazyGames that gameplay has started.
package/dist/react.d.ts CHANGED
@@ -138,6 +138,10 @@ interface GetGameDataLeaderboardParams {
138
138
  *
139
139
  * @packageDocumentation
140
140
  */
141
+ /**
142
+ * Ad types the SDK's `show()` accepts. Limited to formats with a working
143
+ * native path; `'preroll'` maps to a native interstitial (Decision 2).
144
+ */
141
145
  type AdType = 'rewarded' | 'interstitial' | 'preroll';
142
146
  interface AdResult {
143
147
  success: boolean;
@@ -149,6 +153,19 @@ interface AdResult {
149
153
  interface AdConfig {
150
154
  sound?: 'on' | 'off';
151
155
  debug?: boolean;
156
+ /**
157
+ * Route ad requests through the native AdMob bridge when running inside the
158
+ * Hyve mobile shell (`window.ReactNativeWebView` present).
159
+ *
160
+ * Phase 0 rollout flag: defaults to `false`, where ads always use the
161
+ * existing H5 / Playgama web path. The native path is wired up behind this
162
+ * flag in a later phase — until then this is an inert toggle that exists so
163
+ * later releases can flip behavior without an API change. See
164
+ * `docs/admob-migration.md`.
165
+ *
166
+ * @default false
167
+ */
168
+ useNativeAds?: boolean;
152
169
  onBeforeAd?: (type: AdType) => void;
153
170
  onAfterAd?: (type: AdType) => void;
154
171
  onRewardEarned?: () => void;
@@ -480,9 +497,11 @@ declare class HyveClient {
480
497
  /**
481
498
  * Show an ad
482
499
  * @param type Type of ad to show ('rewarded', 'interstitial', or 'preroll')
500
+ * @param placement Optional placement key, forwarded to the native AdMob path
501
+ * to resolve a per-placement unit ID. Ignored on the web (H5/Playgama) path.
483
502
  * @returns Promise resolving to ad result
484
503
  */
485
- showAd(type: AdType): Promise<AdResult>;
504
+ showAd(type: AdType, placement?: string): Promise<AdResult>;
486
505
  /**
487
506
  * Required lifecycle telemetry — Gameplay start.
488
507
  * Also notifies CrazyGames that gameplay has started.
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 {
@@ -2285,6 +2474,7 @@ var HyveClient = class {
2285
2474
  this.userId = null;
2286
2475
  this.jwtToken = null;
2287
2476
  this.gameId = null;
2477
+ this.adsService.setGameId(null);
2288
2478
  logger.info("User logged out");
2289
2479
  }
2290
2480
  /**
@@ -2459,9 +2649,11 @@ var HyveClient = class {
2459
2649
  /**
2460
2650
  * Show an ad
2461
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.
2462
2654
  * @returns Promise resolving to ad result
2463
2655
  */
2464
- async showAd(type) {
2656
+ async showAd(type, placement) {
2465
2657
  if (this.crazyGamesService) {
2466
2658
  if (this.crazyGamesInitPromise) {
2467
2659
  await this.crazyGamesInitPromise;
@@ -2500,7 +2692,7 @@ var HyveClient = class {
2500
2692
  });
2501
2693
  }
2502
2694
  }
2503
- return this.adsService.show(type);
2695
+ return this.adsService.show(type, placement);
2504
2696
  }
2505
2697
  /**
2506
2698
  * Required lifecycle telemetry — Gameplay start.