@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.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 {
@@ -2055,6 +2244,93 @@ var HyveClient = class {
2055
2244
  return false;
2056
2245
  }
2057
2246
  }
2247
+ /**
2248
+ * Required lifecycle telemetry — Session start.
2249
+ * See https://docs.hyve.gg/docs/telemetry#required-lifecycle-events
2250
+ */
2251
+ async sessionStart() {
2252
+ return this.sendTelemetry("game", "session", "start");
2253
+ }
2254
+ /**
2255
+ * Required lifecycle telemetry — Session end.
2256
+ */
2257
+ async sessionEnd() {
2258
+ return this.sendTelemetry("game", "session", "end");
2259
+ }
2260
+ /**
2261
+ * Required lifecycle telemetry — Lobby loading start.
2262
+ */
2263
+ async lobbyLoadingStart() {
2264
+ return this.sendTelemetry("game", "loading", "start");
2265
+ }
2266
+ /**
2267
+ * Required lifecycle telemetry — Lobby loading end.
2268
+ */
2269
+ async lobbyLoadingEnd() {
2270
+ return this.sendTelemetry("game", "loading", "end");
2271
+ }
2272
+ /**
2273
+ * Required lifecycle telemetry — Game loading start.
2274
+ */
2275
+ async gameLoadingStart() {
2276
+ return this.sendTelemetry("game", "game_loading", "start");
2277
+ }
2278
+ /**
2279
+ * Required lifecycle telemetry — Game loading end.
2280
+ */
2281
+ async gameLoadingEnd() {
2282
+ return this.sendTelemetry("game", "game_loading", "end");
2283
+ }
2284
+ /**
2285
+ * Required lifecycle telemetry — Lobby initialization.
2286
+ */
2287
+ async lobbyInit() {
2288
+ return this.sendTelemetry("game", "lobby", "init");
2289
+ }
2290
+ /**
2291
+ * Required lifecycle telemetry — Gameplay end.
2292
+ * Also notifies CrazyGames that gameplay has stopped.
2293
+ */
2294
+ async gameplayEnd() {
2295
+ if (this.crazyGamesService) {
2296
+ if (this.crazyGamesInitPromise) await this.crazyGamesInitPromise;
2297
+ this.crazyGamesService.gameplayStop();
2298
+ }
2299
+ return this.sendTelemetry("game", "gameplay", "end");
2300
+ }
2301
+ /**
2302
+ * Required lifecycle telemetry — Store opened.
2303
+ */
2304
+ async storeOpen() {
2305
+ return this.sendTelemetry("game", "store", "start");
2306
+ }
2307
+ /**
2308
+ * Required lifecycle telemetry — Purchase complete.
2309
+ * @param itemName Name of the purchased item (required by the platform)
2310
+ */
2311
+ async purchaseComplete(itemName) {
2312
+ return this.sendTelemetry("game", "purchase", "complete", null, null, {
2313
+ item_name: itemName
2314
+ });
2315
+ }
2316
+ /**
2317
+ * Required lifecycle telemetry — Purchase failed.
2318
+ * @param itemName Name of the item the purchase was for
2319
+ */
2320
+ async purchaseFail(itemName) {
2321
+ return this.sendTelemetry("game", "purchase", "fail", null, null, {
2322
+ item_name: itemName
2323
+ });
2324
+ }
2325
+ /**
2326
+ * Required lifecycle telemetry — Purchase cancelled.
2327
+ * @param itemName Name of the item the purchase was for
2328
+ */
2329
+ async purchaseCancel(itemName) {
2330
+ return this.sendTelemetry("game", "purchase", "cancel", null, null, {
2331
+ item_name: itemName
2332
+ });
2333
+ }
2058
2334
  /**
2059
2335
  * Makes an authenticated API call using the JWT token
2060
2336
  * @param endpoint API endpoint path (will be appended to base URL)
@@ -2176,6 +2452,7 @@ var HyveClient = class {
2176
2452
  this.userId = null;
2177
2453
  this.jwtToken = null;
2178
2454
  this.gameId = null;
2455
+ this.adsService.setGameId(null);
2179
2456
  logger.info("User logged out");
2180
2457
  }
2181
2458
  /**
@@ -2350,9 +2627,11 @@ var HyveClient = class {
2350
2627
  /**
2351
2628
  * Show an ad
2352
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.
2353
2632
  * @returns Promise resolving to ad result
2354
2633
  */
2355
- async showAd(type) {
2634
+ async showAd(type, placement) {
2356
2635
  if (this.crazyGamesService) {
2357
2636
  if (this.crazyGamesInitPromise) {
2358
2637
  await this.crazyGamesInitPromise;
@@ -2391,17 +2670,18 @@ var HyveClient = class {
2391
2670
  });
2392
2671
  }
2393
2672
  }
2394
- return this.adsService.show(type);
2673
+ return this.adsService.show(type, placement);
2395
2674
  }
2396
2675
  /**
2397
- * Notifies CrazyGames that gameplay has started.
2398
- * No-op on other platforms.
2676
+ * Required lifecycle telemetry Gameplay start.
2677
+ * Also notifies CrazyGames that gameplay has started.
2399
2678
  */
2400
2679
  async gameplayStart() {
2401
2680
  if (this.crazyGamesService) {
2402
2681
  if (this.crazyGamesInitPromise) await this.crazyGamesInitPromise;
2403
2682
  this.crazyGamesService.gameplayStart();
2404
2683
  }
2684
+ return this.sendTelemetry("game", "gameplay", "start");
2405
2685
  }
2406
2686
  /**
2407
2687
  * Notifies CrazyGames that gameplay has stopped.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyve-sdk/js",
3
- "version": "2.11.2",
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
+ }