@hyve-sdk/js 2.12.0 → 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.mjs CHANGED
@@ -148,6 +148,12 @@ function parseUrlParams(searchParams) {
148
148
  var NativeBridge = class {
149
149
  static handlers = /* @__PURE__ */ new Map();
150
150
  static isInitialized = false;
151
+ /**
152
+ * Pending `sendAdRequest()` promises, keyed by the ad type native echoes
153
+ * back in `AD_RESULT.type` ("rewarded" | "interstitial"). One in-flight
154
+ * request per type — ads are shown sequentially.
155
+ */
156
+ static adResultResolvers = /* @__PURE__ */ new Map();
151
157
  /**
152
158
  * Checks if the app is running inside a React Native WebView
153
159
  */
@@ -200,16 +206,20 @@ var NativeBridge = class {
200
206
  "PUSH_PERMISSION_DENIED" /* PUSH_PERMISSION_DENIED */,
201
207
  "PRODUCTS_RESULT" /* PRODUCTS_RESULT */,
202
208
  "PURCHASE_COMPLETE" /* PURCHASE_COMPLETE */,
203
- "PURCHASE_ERROR" /* PURCHASE_ERROR */
209
+ "PURCHASE_ERROR" /* PURCHASE_ERROR */,
210
+ "AD_RESULT" /* AD_RESULT */
204
211
  ];
205
212
  if (!nativeResponseTypes.includes(data.type)) {
206
213
  return;
207
214
  }
215
+ if (data.type === "AD_RESULT" /* AD_RESULT */) {
216
+ this.resolveAdResult(data.payload);
217
+ }
208
218
  const handler = this.handlers.get(data.type);
209
219
  if (handler) {
210
220
  logger.debug(`[NativeBridge] Handling message: ${data.type}`);
211
221
  handler(data.payload);
212
- } else {
222
+ } else if (data.type !== "AD_RESULT" /* AD_RESULT */) {
213
223
  logger.warn(`[NativeBridge] No handler registered for: ${data.type}`);
214
224
  }
215
225
  } catch (error) {
@@ -335,6 +345,81 @@ var NativeBridge = class {
335
345
  static purchase(productId, userId) {
336
346
  this.send("PURCHASE" /* PURCHASE */, { productId, userId });
337
347
  }
348
+ /**
349
+ * Sends a native ad request and resolves with the `AD_RESULT` native sends
350
+ * back. Never rejects — a missing native context, timeout, or superseded
351
+ * request resolves to `{ success: false }` with an error code so callers can
352
+ * decide whether to fall back to the web ad path.
353
+ *
354
+ * @param messageType - `SHOW_REWARDED_AD` or `SHOW_INTERSTITIAL_AD`
355
+ * @param payload - `{ gameId, format, placement? }`
356
+ * @param timeoutMs - How long to wait for `AD_RESULT` before giving up
357
+ *
358
+ * @example
359
+ * const result = await NativeBridge.sendAdRequest(
360
+ * NativeMessageType.SHOW_REWARDED_AD,
361
+ * { gameId: "123", format: "rewarded", placement: "level_end" },
362
+ * );
363
+ * if (result.success) grantReward();
364
+ */
365
+ static sendAdRequest(messageType, payload, timeoutMs = 12e4) {
366
+ const expectedType = messageType === "SHOW_REWARDED_AD" /* SHOW_REWARDED_AD */ ? "rewarded" : "interstitial";
367
+ return new Promise((resolve) => {
368
+ if (!this.isNativeContext()) {
369
+ resolve({ type: expectedType, success: false, error: "no_native_context" });
370
+ return;
371
+ }
372
+ this.initialize();
373
+ const existing = this.adResultResolvers.get(expectedType);
374
+ if (existing) {
375
+ logger.warn(`[NativeBridge] Superseding in-flight ${expectedType} ad request`);
376
+ existing({ type: expectedType, success: false, error: "superseded" });
377
+ }
378
+ let settled = false;
379
+ const timer = setTimeout(() => {
380
+ if (settled) return;
381
+ settled = true;
382
+ this.adResultResolvers.delete(expectedType);
383
+ logger.warn(`[NativeBridge] Ad request timed out: ${expectedType}`);
384
+ resolve({ type: expectedType, success: false, error: "timeout" });
385
+ }, timeoutMs);
386
+ this.adResultResolvers.set(expectedType, (result) => {
387
+ if (settled) return;
388
+ settled = true;
389
+ clearTimeout(timer);
390
+ this.adResultResolvers.delete(expectedType);
391
+ resolve(result);
392
+ });
393
+ this.send(messageType, payload);
394
+ });
395
+ }
396
+ /**
397
+ * Registers a handler invoked for every `AD_RESULT` message from native,
398
+ * in addition to resolving any pending {@link sendAdRequest} promise.
399
+ * Useful for diagnostics or availability tracking.
400
+ *
401
+ * @param handler - Called with the raw `AD_RESULT` payload
402
+ */
403
+ static onAdResult(handler) {
404
+ this.on("AD_RESULT" /* AD_RESULT */, handler);
405
+ }
406
+ /**
407
+ * Resolves the pending {@link sendAdRequest} promise that matches an
408
+ * incoming `AD_RESULT` payload. No-op when nothing is waiting (e.g. a late
409
+ * result after a timeout).
410
+ */
411
+ static resolveAdResult(payload) {
412
+ if (!payload?.type) {
413
+ logger.warn("[NativeBridge] AD_RESULT received without a type");
414
+ return;
415
+ }
416
+ const resolver = this.adResultResolvers.get(payload.type);
417
+ if (resolver) {
418
+ resolver(payload);
419
+ } else {
420
+ logger.debug(`[NativeBridge] No pending ad request for: ${payload.type}`);
421
+ }
422
+ }
338
423
  };
339
424
 
340
425
  // src/utils/jwt.ts
@@ -691,10 +776,12 @@ function getAttributionData() {
691
776
  }
692
777
 
693
778
  // src/services/ads.ts
779
+ var FALLBACK_ERROR_CODES = ["not_configured", "config_fetch_failed"];
694
780
  var AdsService = class {
695
781
  config = {
696
782
  sound: "on",
697
783
  debug: false,
784
+ useNativeAds: false,
698
785
  onBeforeAd: () => {
699
786
  },
700
787
  onAfterAd: () => {
@@ -705,6 +792,16 @@ var AdsService = class {
705
792
  // Cached init promise — ensures adConfig() is only called once
706
793
  initPromise = null;
707
794
  ready = false;
795
+ // Game this SDK instance serves — sent to native so it can resolve unit IDs.
796
+ // The SDK never stores or resolves unit IDs itself (Decision 7).
797
+ gameId = null;
798
+ /**
799
+ * Set the game ID used in native ad requests. Called by the client once the
800
+ * game ID is known (from URL params or JWT). No-op for the web ad path.
801
+ */
802
+ setGameId(gameId) {
803
+ this.gameId = gameId;
804
+ }
708
805
  /**
709
806
  * Optionally configure the ads service.
710
807
  * Not required — ads work without calling this.
@@ -718,7 +815,10 @@ var AdsService = class {
718
815
  onRewardEarned: config.onRewardEarned ?? this.config.onRewardEarned
719
816
  };
720
817
  if (this.config.debug) {
721
- logger.debug("[AdsService] Configuration updated:", { sound: this.config.sound });
818
+ logger.debug("[AdsService] Configuration updated:", {
819
+ sound: this.config.sound,
820
+ useNativeAds: this.config.useNativeAds
821
+ });
722
822
  }
723
823
  }
724
824
  /**
@@ -755,10 +855,93 @@ var AdsService = class {
755
855
  }
756
856
  /**
757
857
  * Show an ad. Auto-initializes on the first call.
758
- * Returns immediately with success: false if ads are disabled or unavailable.
858
+ *
859
+ * Inside the Hyve mobile shell (`window.ReactNativeWebView` present) with
860
+ * `useNativeAds` enabled, the request is routed to native AdMob via the
861
+ * bridge. Otherwise — or when native reports the slot is `not_configured` /
862
+ * `config_fetch_failed` — it falls back to the Google H5 web path.
863
+ *
864
+ * Returns `success: false` if ads are disabled or unavailable.
865
+ *
866
+ * @param type - Ad type to show
867
+ * @param placement - Optional placement key, forwarded to native; ignored on
868
+ * the web path (H5 has no placement concept).
759
869
  */
760
- async show(type) {
870
+ async show(type, placement) {
761
871
  const requestedAt = Date.now();
872
+ if (this.isNativeAdsContext()) {
873
+ const nativeResult = await this.showNative(type, placement, requestedAt);
874
+ if (nativeResult) return nativeResult;
875
+ }
876
+ return this.showWeb(type, requestedAt, true);
877
+ }
878
+ /**
879
+ * True when running inside the Hyve mobile shell with native ads enabled.
880
+ */
881
+ isNativeAdsContext() {
882
+ return NativeBridge.isNativeContext() && this.config.useNativeAds === true;
883
+ }
884
+ /**
885
+ * Route an ad request to native AdMob via the bridge (Decision 7 — native
886
+ * owns unit-ID resolution; the SDK only sends `{ gameId, format, placement }`).
887
+ *
888
+ * Returns the resolved `AdResult`, or `null` to indicate the caller should
889
+ * fall back to the web ad path for this single call.
890
+ */
891
+ async showNative(type, placement, requestedAt) {
892
+ if (!this.gameId) {
893
+ if (this.config.debug) {
894
+ logger.debug("[AdsService] Native ads enabled but no gameId set \u2014 using web path");
895
+ }
896
+ return null;
897
+ }
898
+ const format = type === "rewarded" ? "rewarded" : "interstitial";
899
+ const messageType = type === "rewarded" ? "SHOW_REWARDED_AD" /* SHOW_REWARDED_AD */ : "SHOW_INTERSTITIAL_AD" /* SHOW_INTERSTITIAL_AD */;
900
+ if (this.config.debug) {
901
+ logger.debug(`[AdsService] Requesting native ${type} ad (format: ${format})`, {
902
+ gameId: this.gameId,
903
+ placement
904
+ });
905
+ }
906
+ this.config.onBeforeAd(type);
907
+ const result = await NativeBridge.sendAdRequest(messageType, {
908
+ gameId: this.gameId,
909
+ format,
910
+ placement
911
+ });
912
+ const completedAt = Date.now();
913
+ if (result.error && FALLBACK_ERROR_CODES.includes(result.error)) {
914
+ if (this.config.debug) {
915
+ logger.debug(`[AdsService] Native ad ${result.error} \u2014 falling back to web path`);
916
+ }
917
+ return this.showWeb(type, requestedAt, false);
918
+ }
919
+ this.config.onAfterAd(type);
920
+ if (type === "rewarded" && result.success) {
921
+ this.config.onRewardEarned();
922
+ }
923
+ if (this.config.debug) {
924
+ logger.debug("[AdsService] Native ad result:", {
925
+ type,
926
+ success: result.success,
927
+ error: result.error
928
+ });
929
+ }
930
+ return {
931
+ success: result.success,
932
+ type,
933
+ error: result.error ? new Error(result.error) : void 0,
934
+ requestedAt,
935
+ completedAt
936
+ };
937
+ }
938
+ /**
939
+ * Show an ad via the Google H5 web path. Auto-initializes on first call.
940
+ *
941
+ * @param fireBeforeAd - When false, `onBeforeAd` is not fired (the native
942
+ * fallback path already fired it before delegating here).
943
+ */
944
+ async showWeb(type, requestedAt, fireBeforeAd) {
762
945
  const ready = await this.initialize();
763
946
  if (!ready || !window.adBreak) {
764
947
  return {
@@ -769,12 +952,15 @@ var AdsService = class {
769
952
  completedAt: Date.now()
770
953
  };
771
954
  }
772
- return this.showAdBreak(type);
955
+ return this.showAdBreak(type, fireBeforeAd);
773
956
  }
774
957
  /**
775
958
  * Show an ad break via the Google H5 API.
959
+ *
960
+ * @param fireBeforeAd - When false, skips `onBeforeAd` (already fired by the
961
+ * native fallback path that delegated here).
776
962
  */
777
- async showAdBreak(type) {
963
+ async showAdBreak(type, fireBeforeAd = true) {
778
964
  const requestedAt = Date.now();
779
965
  return new Promise((resolve) => {
780
966
  const googleType = type === "rewarded" ? "reward" : type === "preroll" ? "start" : "next";
@@ -782,7 +968,9 @@ var AdsService = class {
782
968
  if (this.config.debug) {
783
969
  logger.debug(`[AdsService] Showing ${type} ad`);
784
970
  }
785
- this.config.onBeforeAd(type);
971
+ if (fireBeforeAd) {
972
+ this.config.onBeforeAd(type);
973
+ }
786
974
  const adBreakConfig = {
787
975
  type: googleType,
788
976
  name: adName,
@@ -1950,6 +2138,7 @@ var HyveClient = class {
1950
2138
  this.gameId = params.gameId;
1951
2139
  logger.info("Game ID extracted from game-id parameter:", this.gameId);
1952
2140
  }
2141
+ this.adsService.setGameId(this.gameId);
1953
2142
  if (this.jwtToken) {
1954
2143
  logger.info("Authentication successful via JWT");
1955
2144
  } else {
@@ -2263,6 +2452,7 @@ var HyveClient = class {
2263
2452
  this.userId = null;
2264
2453
  this.jwtToken = null;
2265
2454
  this.gameId = null;
2455
+ this.adsService.setGameId(null);
2266
2456
  logger.info("User logged out");
2267
2457
  }
2268
2458
  /**
@@ -2437,9 +2627,11 @@ var HyveClient = class {
2437
2627
  /**
2438
2628
  * Show an ad
2439
2629
  * @param type Type of ad to show ('rewarded', 'interstitial', or 'preroll')
2630
+ * @param placement Optional placement key, forwarded to the native AdMob path
2631
+ * to resolve a per-placement unit ID. Ignored on the web (H5/Playgama) path.
2440
2632
  * @returns Promise resolving to ad result
2441
2633
  */
2442
- async showAd(type) {
2634
+ async showAd(type, placement) {
2443
2635
  if (this.crazyGamesService) {
2444
2636
  if (this.crazyGamesInitPromise) {
2445
2637
  await this.crazyGamesInitPromise;
@@ -2478,7 +2670,7 @@ var HyveClient = class {
2478
2670
  });
2479
2671
  }
2480
2672
  }
2481
- return this.adsService.show(type);
2673
+ return this.adsService.show(type, placement);
2482
2674
  }
2483
2675
  /**
2484
2676
  * Required lifecycle telemetry — Gameplay start.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyve-sdk/js",
3
- "version": "2.12.0",
3
+ "version": "2.13.0-canary.0",
4
4
  "description": "Hyve SDK - TypeScript wrapper for Hyve game server integration",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -27,14 +27,6 @@
27
27
  "README.md",
28
28
  "LICENSE"
29
29
  ],
30
- "scripts": {
31
- "lint": "eslint . --max-warnings 0",
32
- "check-types": "tsc --noEmit",
33
- "build": "tsup",
34
- "prepublishOnly": "pnpm run build && pnpm run check-types",
35
- "publish:npm": "pnpm publish --access public",
36
- "publish:dry-run": "pnpm publish --dry-run --access public --no-git-checks"
37
- },
38
30
  "keywords": [
39
31
  "hyve",
40
32
  "game",
@@ -71,12 +63,23 @@
71
63
  }
72
64
  },
73
65
  "devDependencies": {
74
- "@repo/eslint-config": "workspace:*",
75
- "@repo/typescript-config": "workspace:*",
76
66
  "@types/minimatch": "^5.1.2",
77
67
  "@types/react": "^18.3.28",
78
68
  "@types/uuid": "^10.0.0",
79
69
  "tsup": "^8.4.0",
80
- "typescript": "^5.3.3"
70
+ "typescript": "^5.3.3",
71
+ "vitest": "^4.1.8",
72
+ "@repo/typescript-config": "0.0.0",
73
+ "@repo/eslint-config": "0.0.0"
74
+ },
75
+ "scripts": {
76
+ "lint": "eslint . --max-warnings 0",
77
+ "test": "vitest run",
78
+ "test:watch": "vitest",
79
+ "check-types": "tsc --noEmit",
80
+ "build": "tsup",
81
+ "publish:npm": "pnpm publish --access public",
82
+ "publish:canary": "pnpm publish --access public --tag canary --no-git-checks",
83
+ "publish:dry-run": "pnpm publish --dry-run --access public --no-git-checks"
81
84
  }
82
- }
85
+ }