@wvdsh/sdk-js 1.3.16 → 1.3.18

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +371 -334
  2. package/dist/index.js +2866 -2758
  3. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -5,6 +5,83 @@ import { FunctionReturnType, FunctionArgs } from 'convex/server';
5
5
  import { LOBBY_VISIBILITY, api, UGC_TYPE, UGC_VISIBILITY, LEADERBOARD_SORT_ORDER, LEADERBOARD_DISPLAY_TYPE, GAME_ENGINE, SDKUser, IFrameEventPayloadMap, IFRAME_MESSAGE_TYPE, SDKConfig, GameLaunchParams } from '@wvdsh/api';
6
6
  export { GameLaunchParams } from '@wvdsh/api';
7
7
 
8
+ /**
9
+ * Base class for SDK managers. Provides the shared `sdk` reference and a
10
+ * default no-op `destroy()` so the SDK can safely iterate every manager
11
+ * during teardown without each one having to define an empty stub.
12
+ *
13
+ * Override `destroy()` in any manager that owns ongoing state — Convex
14
+ * subscriptions, intervals, peer connections, monkey-patched globals, etc.
15
+ * — to make sure that state is released when the SDK is torn down.
16
+ */
17
+ declare abstract class WavedashManager {
18
+ protected sdk: WavedashSDK;
19
+ constructor(sdk: WavedashSDK);
20
+ destroy(): void;
21
+ }
22
+
23
+ /**
24
+ * AudioManager
25
+ *
26
+ * Mutes & unmutes the game in response to MUTE_CHANGED iframe messages, without
27
+ * the game needing to handle it itself.
28
+ *
29
+ * Web Audio: subclass `AudioContext` so `ctx.destination` resolves to a master
30
+ * GainNode that we control. The master gain wires to the real native destination,
31
+ * so `node.connect(ctx.destination)` and any other game code is unaffected.
32
+ *
33
+ * HTML Media (`<audio>`/`<video>`): override `HTMLMediaElement.prototype.muted`
34
+ * to record the game's intended state, but write `true` to the underlying element
35
+ * whenever the SDK is muted. Tracked elements come from four sources:
36
+ * 1. Pre-existing DOM media (`querySelectorAll`)
37
+ * 2. `new Audio()` constructor shim (covers detached SFX)
38
+ * 3. MutationObserver for any media added to the DOM later (covers innerHTML,
39
+ * framework rendering, createElement + append, etc.)
40
+ * 4. `HTMLMediaElement.prototype.play()` shim — the universal point where an
41
+ * element starts producing audio. Catches anything driven purely via
42
+ * `.play()`/`.volume` (never assigning `.muted`, never entering the DOM,
43
+ * e.g. a PIXI/GDevelop intro video), force-muting it before playback begins
44
+ * regardless of how it was created — the one path the DOM-based sources and
45
+ * the `muted` setter all miss.
46
+ */
47
+ declare class AudioManager extends WavedashManager {
48
+ private _isMuted;
49
+ private contexts;
50
+ private elements;
51
+ private intendedMuted;
52
+ private originalAudioContext;
53
+ private originalWebKitAudioContext;
54
+ private originalAudio;
55
+ private originalMutedDescriptor;
56
+ private originalPlay;
57
+ private mutationObserver;
58
+ constructor(sdk: WavedashSDK);
59
+ isMuted(): boolean;
60
+ /**
61
+ * Ask the host to mute (true) or unmute (false). Resolves to `true` if the
62
+ * host applied the change, `false` otherwise — notably, the host rejects an
63
+ * unmute when the user muted the game from the Wavedash UI, so games can't
64
+ * override an explicit user mute. The resulting state arrives via the usual
65
+ * MUTE_CHANGED broadcast, so `isMuted()` updates independently of this result.
66
+ */
67
+ requestMute(muted: boolean): Promise<boolean>;
68
+ /**
69
+ * Toggle mute. Like `requestMute`, the host may reject the unmute half of a
70
+ * toggle if the user muted from the Wavedash UI. Resolves to `true` if the
71
+ * host applied the change.
72
+ */
73
+ toggleMute(): Promise<boolean>;
74
+ private handleMute;
75
+ /**
76
+ * Track a media element and (if SDK is currently muted) silence it.
77
+ * Idempotent — safe to call multiple times for the same element.
78
+ */
79
+ private trackElement;
80
+ private installShims;
81
+ private shimAudioContextClass;
82
+ destroy(): void;
83
+ }
84
+
8
85
  /**
9
86
  * SDK-to-Engine Events
10
87
  *
@@ -262,18 +339,234 @@ interface P2PConfig {
262
339
  }
263
340
 
264
341
  /**
265
- * Base class for SDK managers. Provides the shared `sdk` reference and a
266
- * default no-op `destroy()` so the SDK can safely iterate every manager
267
- * during teardown without each one having to define an empty stub.
342
+ * File system service
343
+ * Utilities for syncing local IndexedDB files with remote storage.
268
344
  *
269
- * Override `destroy()` in any manager that owns ongoing state Convex
270
- * subscriptions, intervals, peer connections, monkey-patched globals, etc.
271
- * — to make sure that state is released when the SDK is torn down.
345
+ * Exposes a specific remote folder for the game to save user-specific files to.
346
+ * TODO: Extend this to game-level assets as well.
272
347
  */
273
- declare abstract class WavedashManager {
274
- protected sdk: WavedashSDK;
348
+
349
+ declare class FileSystemManager extends WavedashManager {
350
+ private remoteStorageOrigin;
351
+ constructor(sdk: WavedashSDK);
352
+ /**
353
+ * Converts a local filesystem path into a full R2 object key.
354
+ * Normalizes the Unity persistentDataPath and prepends the R2 prefix.
355
+ */
356
+ private toRemoteKey;
357
+ /**
358
+ * Converts a full R2 object key back into the local filesystem path
359
+ * the engine expects. Inverse of toRemoteKey.
360
+ */
361
+ private toLocalPath;
362
+ /**
363
+ * Uploads a local file to remote storage
364
+ * @param filePath - The path of the local file to upload
365
+ * @returns The path of the remote file that the local file was uploaded to
366
+ */
367
+ uploadRemoteFile(filePath: string): Promise<string>;
368
+ /**
369
+ * Deletes a remote file from storage
370
+ * @param filePath - The path of the remote file to delete
371
+ * @returns The path of the remote file that was deleted
372
+ */
373
+ deleteRemoteFile(filePath: string): Promise<string>;
374
+ /**
375
+ * Downloads a remote file to a local location.
376
+ * Throws on failure; the error message is the server's HTTP status (e.g. "404 (Not Found)")
377
+ * or a network-level description if the server didn't respond. See also: {@link remoteFileExists}
378
+ * @param filePath - The path of the remote file to download
379
+ * @returns The path of the local file that the remote file was downloaded to
380
+ */
381
+ downloadRemoteFile(filePath: string): Promise<string>;
382
+ /**
383
+ * Checks whether a remote file exists by issuing a HEAD request.
384
+ * Does NOT throw for the "file does not exist" case — returns false.
385
+ * Throws only for real errors (network failure, auth failure, server error).
386
+ * @param filePath - The path of the remote file to check
387
+ * @returns true if the remote file exists, false otherwise
388
+ */
389
+ remoteFileExists(filePath: string): Promise<boolean>;
390
+ /**
391
+ * Lists each file in a remote directory, including its subdirectories.
392
+ * Returns only file paths, no directory paths.
393
+ * An empty or non-existent directory returns an empty array — not an error.
394
+ * @param path - The path of the remote directory to list
395
+ * @returns A list of metadata for each file in the remote directory
396
+ */
397
+ listRemoteDirectory(path: string): Promise<RemoteFileMetadata[]>;
398
+ downloadRemoteDirectory(path: string): Promise<string>;
399
+ writeLocalFile(filePath: string, data: Uint8Array): Promise<boolean>;
400
+ readLocalFile(filePath: string): Promise<Uint8Array | null>;
401
+ upload(presignedUploadUrl: string, filePath: string): Promise<boolean>;
402
+ download(url: string, filePath: string): Promise<void>;
403
+ private getRemoteStorageOrigin;
404
+ private getRemoteStorageUrl;
405
+ private uploadFromIndexedDb;
406
+ private uploadFromFS;
407
+ private readLocalFileBlob;
408
+ }
409
+
410
+ /**
411
+ * Friends service
412
+ *
413
+ * Implements friend-related methods for the Wavedash SDK
414
+ */
415
+
416
+ declare class FriendsManager extends WavedashManager {
417
+ private userCache;
418
+ private leaderboardPageUserCache;
275
419
  constructor(sdk: WavedashSDK);
420
+ /**
421
+ * Returns CDN URL with size transformation for a cached user's avatar.
422
+ * @param userId - The user ID to get the avatar URL for
423
+ * @param size - Pixel size for width and height. Use a value from
424
+ * `AvatarSize` (SMALL=64, MEDIUM=128, LARGE=256) or any custom pixel size.
425
+ * @returns CDN URL with size transformation, or null if user not cached or has no avatar
426
+ */
427
+ getUserAvatarUrl(userId: GenericId<"users">, size?: number): string | null;
428
+ /**
429
+ * Returns the cached username for a given user ID
430
+ * @param userId - The user ID to get the username for
431
+ * @returns The username, or null if user not cached
432
+ */
433
+ getUsername(userId: GenericId<"users">): string | null;
434
+ /**
435
+ * List all friends for the logged in user
436
+ * @returns Array<{
437
+ * avatarUrl?: string;
438
+ * isOnline: boolean;
439
+ * userId: Id<"users">;
440
+ * username: string;
441
+ * }>
442
+ */
443
+ listFriends(): Promise<Friend[]>;
444
+ }
445
+
446
+ /**
447
+ * FullscreenManager
448
+ *
449
+ * Wavedash owns the fullscreen target (a wrapper DIV on the host page that
450
+ * contains both the game iframe and our overlay UI). The SDK inside the iframe
451
+ * therefore can't call `requestFullscreen` directly — it asks the parent to
452
+ * do it via postMessage, and the parent broadcasts state changes back through
453
+ * FULLSCREEN_CHANGED so we can keep a local mirror of `isFullscreen`.
454
+ *
455
+ * User activation: browsers require a fresh user gesture to enter fullscreen.
456
+ * The click happens in the iframe, User Activation v2 propagates transient
457
+ * activation to ancestor frames, and the parent's message handler runs within
458
+ * the ~5s window — so the parent's requestFullscreen call stays activated.
459
+ *
460
+ * Legacy compat: games that call `element.requestFullscreen()` or listen for
461
+ * `fullscreenchange` directly are monkey-patched in the constructor so those
462
+ * calls route through us. The iframe isn't granted the fullscreen feature
463
+ * policy anymore, so without these shims those calls would silently reject.
464
+ */
465
+ declare class FullscreenManager extends WavedashManager {
466
+ private _isFullscreen;
467
+ private listeners;
468
+ constructor(sdk: WavedashSDK);
469
+ isFullscreen(): boolean;
470
+ /**
471
+ * Ask the host to enter (true) or exit (false) fullscreen. Resolves to
472
+ * `true` if the host reports the operation succeeded, `false` otherwise
473
+ * (e.g. browser rejected for lack of user activation).
474
+ */
475
+ requestFullscreen(fullscreen: boolean): Promise<boolean>;
476
+ toggleFullscreen(): Promise<boolean>;
477
+ /** Subscribe to state flips. Returns an unsubscribe fn. */
478
+ subscribe(listener: (isFullscreen: boolean) => void): () => void;
479
+ private setState;
480
+ private installCompatShims;
481
+ }
482
+
483
+ declare class GameEventManager extends WavedashManager {
484
+ private eventQueue;
485
+ constructor(sdk: WavedashSDK);
486
+ notifyGame(event: WavedashEvent, payload: string | number | object): void;
487
+ private sendGameEvent;
488
+ flushEventQueue(): void;
489
+ }
490
+
491
+ /**
492
+ * Heartbeat service
493
+ *
494
+ * Polls connection state and allows the game to update rich user presence
495
+ * Lets the game know if backend connection ever changes.
496
+ * Lets the game update userPresence in the backend
497
+ */
498
+
499
+ declare class HeartbeatManager extends WavedashManager {
500
+ private deviceFingerprint;
501
+ private deviceFingerprintReady;
502
+ private testConnectionInterval;
503
+ private heartbeatInterval;
504
+ private gamepadPollInterval;
505
+ private inactivityTimeout;
506
+ private isConnected;
507
+ private sentDisconnectedEvent;
508
+ private disconnectedAt;
509
+ private lastHeartbeatTime;
510
+ private lastInputResetAt;
511
+ private heartbeatInFlight;
512
+ private isFirstTick;
513
+ private readonly TEST_CONNECTION_INTERVAL_MS;
514
+ private readonly DISCONNECTED_TIMEOUT_MS;
515
+ private readonly INACTIVITY_TIMEOUT_MS;
516
+ private readonly INPUT_THROTTLE_MS;
517
+ private readonly GAMEPAD_POLL_INTERVAL_MS;
518
+ private readonly GAMEPAD_AXIS_DEADZONE;
519
+ private cachedPresenceData;
520
+ constructor(sdk: WavedashSDK);
521
+ /**
522
+ * Start (or refresh) the heartbeat. Idempotent: if intervals are already
523
+ * running this just reschedules the inactivity timer. No-op if the game
524
+ * hasn't loaded yet or the tab is hidden.
525
+ */
526
+ start(): void;
527
+ /** Stop the heartbeat and clear the inactivity timer. Idempotent. */
528
+ stop(): void;
529
+ /**
530
+ * Updates user presence in the backend.
531
+ * @param data - Data to send to the backend
532
+ * @returns true if the presence was updated successfully
533
+ */
534
+ updateUserPresence(data: Record<string, string | number | boolean | null>): Promise<boolean>;
535
+ isCurrentlyConnected(): boolean;
536
+ /** Full teardown — stops intervals and removes all listeners */
276
537
  destroy(): void;
538
+ private tickHeartbeat;
539
+ private sendHeartbeat;
540
+ private handleVisibilityChange;
541
+ private handleUserInput;
542
+ /**
543
+ * Polls connected gamepads; any pressed button or out-of-deadzone axis
544
+ * counts as user activity and (re)starts the heartbeat.
545
+ */
546
+ private pollGamepads;
547
+ /**
548
+ * Tests the connection to the backend
549
+ */
550
+ private testConnection;
551
+ }
552
+
553
+ /**
554
+ * Leaderboard service
555
+ *
556
+ * Implements each of the leaderboard methods of the Wavedash SDK
557
+ */
558
+
559
+ declare class LeaderboardManager extends WavedashManager {
560
+ private leaderboardCache;
561
+ constructor(sdk: WavedashSDK);
562
+ getLeaderboard(name: string): Promise<Leaderboard>;
563
+ getOrCreateLeaderboard(name: string, sortOrder: LeaderboardSortOrder, displayType: LeaderboardDisplayType): Promise<Leaderboard>;
564
+ getLeaderboardEntryCount(leaderboardId: GenericId<"leaderboards">): number;
565
+ getMyLeaderboardEntries(leaderboardId: GenericId<"leaderboards">): Promise<LeaderboardEntries>;
566
+ listLeaderboardEntriesAroundUser(leaderboardId: GenericId<"leaderboards">, countAhead: number, countBehind: number, friendsOnly?: boolean): Promise<LeaderboardEntries>;
567
+ listLeaderboardEntries(leaderboardId: GenericId<"leaderboards">, offset: number, limit: number, friendsOnly?: boolean): Promise<LeaderboardEntries>;
568
+ uploadLeaderboardScore(leaderboardId: GenericId<"leaderboards">, score: number, keepBest: boolean, ugcId?: GenericId<"userGeneratedContent">): Promise<UpsertedLeaderboardEntry>;
569
+ private updateCachedTotalEntries;
277
570
  }
278
571
 
279
572
  /**
@@ -357,116 +650,31 @@ declare class LobbyManager extends WavedashManager {
357
650
  * @param newUsers - The updated list of lobby users
358
651
  */
359
652
  private processUserUpdates;
360
- private processMessageUpdates;
361
- private processInviteUpdates;
362
- /**
363
- * Update P2P connections when lobby membership changes
364
- * @param newUsers - The updated list of lobby users
365
- */
366
- private updateP2PConnections;
367
- }
368
-
369
- /**
370
- * File system service
371
- * Utilities for syncing local IndexedDB files with remote storage.
372
- *
373
- * Exposes a specific remote folder for the game to save user-specific files to.
374
- * TODO: Extend this to game-level assets as well.
375
- */
376
-
377
- declare class FileSystemManager extends WavedashManager {
378
- private remoteStorageOrigin;
379
- constructor(sdk: WavedashSDK);
380
- /**
381
- * Converts a local filesystem path into a full R2 object key.
382
- * Normalizes the Unity persistentDataPath and prepends the R2 prefix.
383
- */
384
- private toRemoteKey;
385
- /**
386
- * Converts a full R2 object key back into the local filesystem path
387
- * the engine expects. Inverse of toRemoteKey.
388
- */
389
- private toLocalPath;
390
- /**
391
- * Uploads a local file to remote storage
392
- * @param filePath - The path of the local file to upload
393
- * @returns The path of the remote file that the local file was uploaded to
394
- */
395
- uploadRemoteFile(filePath: string): Promise<string>;
396
- /**
397
- * Deletes a remote file from storage
398
- * @param filePath - The path of the remote file to delete
399
- * @returns The path of the remote file that was deleted
400
- */
401
- deleteRemoteFile(filePath: string): Promise<string>;
402
- /**
403
- * Downloads a remote file to a local location.
404
- * Throws on failure; the error message is the server's HTTP status (e.g. "404 (Not Found)")
405
- * or a network-level description if the server didn't respond. See also: {@link remoteFileExists}
406
- * @param filePath - The path of the remote file to download
407
- * @returns The path of the local file that the remote file was downloaded to
408
- */
409
- downloadRemoteFile(filePath: string): Promise<string>;
410
- /**
411
- * Checks whether a remote file exists by issuing a HEAD request.
412
- * Does NOT throw for the "file does not exist" case — returns false.
413
- * Throws only for real errors (network failure, auth failure, server error).
414
- * @param filePath - The path of the remote file to check
415
- * @returns true if the remote file exists, false otherwise
416
- */
417
- remoteFileExists(filePath: string): Promise<boolean>;
418
- /**
419
- * Lists each file in a remote directory, including its subdirectories.
420
- * Returns only file paths, no directory paths.
421
- * An empty or non-existent directory returns an empty array — not an error.
422
- * @param path - The path of the remote directory to list
423
- * @returns A list of metadata for each file in the remote directory
424
- */
425
- listRemoteDirectory(path: string): Promise<RemoteFileMetadata[]>;
426
- downloadRemoteDirectory(path: string): Promise<string>;
427
- writeLocalFile(filePath: string, data: Uint8Array): Promise<boolean>;
428
- readLocalFile(filePath: string): Promise<Uint8Array | null>;
429
- upload(presignedUploadUrl: string, filePath: string): Promise<boolean>;
430
- download(url: string, filePath: string): Promise<void>;
431
- private getRemoteStorageOrigin;
432
- private getRemoteStorageUrl;
433
- private uploadFromIndexedDb;
434
- private uploadFromFS;
435
- private readLocalFileBlob;
436
- }
437
-
438
- /**
439
- * UGC service
440
- *
441
- * Implements each of the user generated content methods of the Wavedash SDK
442
- */
443
-
444
- declare class UGCManager extends WavedashManager {
445
- constructor(sdk: WavedashSDK);
446
- createUGCItem(ugcType: UGCType, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<GenericId<"userGeneratedContent">>;
447
- updateUGCItem(ugcId: GenericId<"userGeneratedContent">, updates?: UpdateUGCItemArgs): Promise<GenericId<"userGeneratedContent">>;
448
- deleteUGCItem(ugcId: GenericId<"userGeneratedContent">): Promise<GenericId<"userGeneratedContent">>;
449
- downloadUGCItem(ugcId: GenericId<"userGeneratedContent">, filePath: string): Promise<GenericId<"userGeneratedContent">>;
450
- listUGCItems(args?: ListUGCItemsArgs): Promise<PaginatedUGCItems>;
653
+ private processMessageUpdates;
654
+ private processInviteUpdates;
655
+ /**
656
+ * Update P2P connections when lobby membership changes
657
+ * @param newUsers - The updated list of lobby users
658
+ */
659
+ private updateP2PConnections;
451
660
  }
452
661
 
453
662
  /**
454
- * Leaderboard service
663
+ * OverlayManager
455
664
  *
456
- * Implements each of the leaderboard methods of the Wavedash SDK
665
+ * Owns the iframe parent interactions for the Wavedash overlay UI:
666
+ * - Shift+Tab inside the iframe toggles the overlay on the host page
667
+ * (the host owns the overlay, so we postMessage up).
668
+ * - When the parent closes the overlay it sends TAKE_FOCUS so keyboard
669
+ * input goes back to the game; we walk the DOM for a focusable target.
670
+ * - `takeFocus()` is also called after load completes so the game starts
671
+ * with keyboard focus without the player clicking first.
457
672
  */
458
-
459
- declare class LeaderboardManager extends WavedashManager {
460
- private leaderboardCache;
673
+ declare class OverlayManager extends WavedashManager {
461
674
  constructor(sdk: WavedashSDK);
462
- getLeaderboard(name: string): Promise<Leaderboard>;
463
- getOrCreateLeaderboard(name: string, sortOrder: LeaderboardSortOrder, displayType: LeaderboardDisplayType): Promise<Leaderboard>;
464
- getLeaderboardEntryCount(leaderboardId: GenericId<"leaderboards">): number;
465
- getMyLeaderboardEntries(leaderboardId: GenericId<"leaderboards">): Promise<LeaderboardEntries>;
466
- listLeaderboardEntriesAroundUser(leaderboardId: GenericId<"leaderboards">, countAhead: number, countBehind: number, friendsOnly?: boolean): Promise<LeaderboardEntries>;
467
- listLeaderboardEntries(leaderboardId: GenericId<"leaderboards">, offset: number, limit: number, friendsOnly?: boolean): Promise<LeaderboardEntries>;
468
- uploadLeaderboardScore(leaderboardId: GenericId<"leaderboards">, score: number, keepBest: boolean, ugcId?: GenericId<"userGeneratedContent">): Promise<UpsertedLeaderboardEntry>;
469
- private updateCachedTotalEntries;
675
+ toggleOverlay(): void;
676
+ takeFocus(): void;
677
+ private handleKeyDown;
470
678
  }
471
679
 
472
680
  /**
@@ -586,6 +794,12 @@ declare class P2PManager extends WavedashManager {
586
794
  private decodeBinaryMessage;
587
795
  }
588
796
 
797
+ declare class PaidContentManager extends WavedashManager {
798
+ isEntitled(contentId: string): Promise<boolean>;
799
+ getEntitlements(): Promise<string[]>;
800
+ triggerPaywall(contentIdentifier: string): Promise<boolean>;
801
+ }
802
+
589
803
  declare class StatsManager extends WavedashManager {
590
804
  private stats;
591
805
  private unlockedAchievements;
@@ -615,219 +829,18 @@ declare class StatsManager extends WavedashManager {
615
829
  }
616
830
 
617
831
  /**
618
- * Heartbeat service
619
- *
620
- * Polls connection state and allows the game to update rich user presence
621
- * Lets the game know if backend connection ever changes.
622
- * Lets the game update userPresence in the backend
623
- */
624
-
625
- declare class HeartbeatManager extends WavedashManager {
626
- private deviceFingerprint;
627
- private deviceFingerprintReady;
628
- private testConnectionInterval;
629
- private heartbeatInterval;
630
- private gamepadPollInterval;
631
- private inactivityTimeout;
632
- private isConnected;
633
- private sentDisconnectedEvent;
634
- private disconnectedAt;
635
- private lastHeartbeatTime;
636
- private lastInputResetAt;
637
- private heartbeatInFlight;
638
- private isFirstTick;
639
- private readonly TEST_CONNECTION_INTERVAL_MS;
640
- private readonly DISCONNECTED_TIMEOUT_MS;
641
- private readonly INACTIVITY_TIMEOUT_MS;
642
- private readonly INPUT_THROTTLE_MS;
643
- private readonly GAMEPAD_POLL_INTERVAL_MS;
644
- private readonly GAMEPAD_AXIS_DEADZONE;
645
- private cachedPresenceData;
646
- constructor(sdk: WavedashSDK);
647
- /**
648
- * Start (or refresh) the heartbeat. Idempotent: if intervals are already
649
- * running this just reschedules the inactivity timer. No-op if the game
650
- * hasn't loaded yet or the tab is hidden.
651
- */
652
- start(): void;
653
- /** Stop the heartbeat and clear the inactivity timer. Idempotent. */
654
- stop(): void;
655
- /**
656
- * Updates user presence in the backend.
657
- * @param data - Data to send to the backend
658
- * @returns true if the presence was updated successfully
659
- */
660
- updateUserPresence(data: Record<string, string | number | boolean | null>): Promise<boolean>;
661
- isCurrentlyConnected(): boolean;
662
- /** Full teardown — stops intervals and removes all listeners */
663
- destroy(): void;
664
- private tickHeartbeat;
665
- private sendHeartbeat;
666
- private handleVisibilityChange;
667
- private handleUserInput;
668
- /**
669
- * Polls connected gamepads; any pressed button or out-of-deadzone axis
670
- * counts as user activity and (re)starts the heartbeat.
671
- */
672
- private pollGamepads;
673
- /**
674
- * Tests the connection to the backend
675
- */
676
- private testConnection;
677
- }
678
-
679
- declare class GameEventManager extends WavedashManager {
680
- private eventQueue;
681
- constructor(sdk: WavedashSDK);
682
- notifyGame(event: WavedashEvent, payload: string | number | object): void;
683
- private sendGameEvent;
684
- flushEventQueue(): void;
685
- }
686
-
687
- /**
688
- * FullscreenManager
689
- *
690
- * Wavedash owns the fullscreen target (a wrapper DIV on the host page that
691
- * contains both the game iframe and our overlay UI). The SDK inside the iframe
692
- * therefore can't call `requestFullscreen` directly — it asks the parent to
693
- * do it via postMessage, and the parent broadcasts state changes back through
694
- * FULLSCREEN_CHANGED so we can keep a local mirror of `isFullscreen`.
695
- *
696
- * User activation: browsers require a fresh user gesture to enter fullscreen.
697
- * The click happens in the iframe, User Activation v2 propagates transient
698
- * activation to ancestor frames, and the parent's message handler runs within
699
- * the ~5s window — so the parent's requestFullscreen call stays activated.
700
- *
701
- * Legacy compat: games that call `element.requestFullscreen()` or listen for
702
- * `fullscreenchange` directly are monkey-patched in the constructor so those
703
- * calls route through us. The iframe isn't granted the fullscreen feature
704
- * policy anymore, so without these shims those calls would silently reject.
705
- */
706
- declare class FullscreenManager extends WavedashManager {
707
- private _isFullscreen;
708
- private listeners;
709
- constructor(sdk: WavedashSDK);
710
- isFullscreen(): boolean;
711
- /**
712
- * Ask the host to enter (true) or exit (false) fullscreen. Resolves to
713
- * `true` if the host reports the operation succeeded, `false` otherwise
714
- * (e.g. browser rejected for lack of user activation).
715
- */
716
- requestFullscreen(fullscreen: boolean): Promise<boolean>;
717
- toggleFullscreen(): Promise<boolean>;
718
- /** Subscribe to state flips. Returns an unsubscribe fn. */
719
- subscribe(listener: (isFullscreen: boolean) => void): () => void;
720
- private setState;
721
- private installCompatShims;
722
- }
723
-
724
- /**
725
- * OverlayManager
726
- *
727
- * Owns the iframe ↔ parent interactions for the Wavedash overlay UI:
728
- * - Shift+Tab inside the iframe toggles the overlay on the host page
729
- * (the host owns the overlay, so we postMessage up).
730
- * - When the parent closes the overlay it sends TAKE_FOCUS so keyboard
731
- * input goes back to the game; we walk the DOM for a focusable target.
732
- * - `takeFocus()` is also called after load completes so the game starts
733
- * with keyboard focus without the player clicking first.
734
- */
735
- declare class OverlayManager extends WavedashManager {
736
- constructor(sdk: WavedashSDK);
737
- toggleOverlay(): void;
738
- takeFocus(): void;
739
- private handleKeyDown;
740
- }
741
-
742
- /**
743
- * AudioManager
744
- *
745
- * Mutes & unmutes the game in response to MUTE_CHANGED iframe messages, without
746
- * the game needing to know anything about it.
747
- *
748
- * Web Audio: subclass `AudioContext` so `ctx.destination` resolves to a master
749
- * GainNode that we control. The master gain wires to the real native destination,
750
- * so `node.connect(ctx.destination)` and any other game code is unaffected.
751
- *
752
- * HTML Media (`<audio>`/`<video>`): override `HTMLMediaElement.prototype.muted`
753
- * to record the game's intended state, but write `true` to the underlying element
754
- * whenever the SDK is muted. Tracked elements come from three sources:
755
- * 1. Pre-existing DOM media (`querySelectorAll`)
756
- * 2. `new Audio()` constructor shim (covers detached SFX)
757
- * 3. MutationObserver for any media added to the DOM later (covers innerHTML,
758
- * framework rendering, createElement + append, etc.)
759
- */
760
- declare class AudioManager extends WavedashManager {
761
- private _isMuted;
762
- private contexts;
763
- private elements;
764
- private intendedMuted;
765
- private originalAudioContext;
766
- private originalWebKitAudioContext;
767
- private originalAudio;
768
- private originalMutedDescriptor;
769
- private mutationObserver;
770
- constructor(sdk: WavedashSDK);
771
- isMuted(): boolean;
772
- /**
773
- * Ask the host to mute (true) or unmute (false). Resolves to `true` if the
774
- * host applied the change, `false` otherwise — notably, the host rejects an
775
- * unmute when the user muted the game from the Wavedash UI, so games can't
776
- * override an explicit user mute. The resulting state arrives via the usual
777
- * MUTE_CHANGED broadcast, so `isMuted()` updates independently of this result.
778
- */
779
- requestMute(muted: boolean): Promise<boolean>;
780
- /**
781
- * Toggle mute. Like `requestMute`, the host may reject the unmute half of a
782
- * toggle if the user muted from the Wavedash UI. Resolves to `true` if the
783
- * host applied the change.
784
- */
785
- toggleMute(): Promise<boolean>;
786
- private handleMute;
787
- /**
788
- * Track a media element and (if SDK is currently muted) silence it.
789
- * Idempotent — safe to call multiple times for the same element.
790
- */
791
- private trackElement;
792
- private installShims;
793
- private shimAudioContextClass;
794
- destroy(): void;
795
- }
796
-
797
- /**
798
- * Friends service
832
+ * UGC service
799
833
  *
800
- * Implements friend-related methods for the Wavedash SDK
834
+ * Implements each of the user generated content methods of the Wavedash SDK
801
835
  */
802
836
 
803
- declare class FriendsManager extends WavedashManager {
804
- private userCache;
805
- private leaderboardPageUserCache;
837
+ declare class UGCManager extends WavedashManager {
806
838
  constructor(sdk: WavedashSDK);
807
- /**
808
- * Returns CDN URL with size transformation for a cached user's avatar.
809
- * @param userId - The user ID to get the avatar URL for
810
- * @param size - Pixel size for width and height. Use a value from
811
- * `AvatarSize` (SMALL=64, MEDIUM=128, LARGE=256) or any custom pixel size.
812
- * @returns CDN URL with size transformation, or null if user not cached or has no avatar
813
- */
814
- getUserAvatarUrl(userId: GenericId<"users">, size?: number): string | null;
815
- /**
816
- * Returns the cached username for a given user ID
817
- * @param userId - The user ID to get the username for
818
- * @returns The username, or null if user not cached
819
- */
820
- getUsername(userId: GenericId<"users">): string | null;
821
- /**
822
- * List all friends for the logged in user
823
- * @returns Array<{
824
- * avatarUrl?: string;
825
- * isOnline: boolean;
826
- * userId: Id<"users">;
827
- * username: string;
828
- * }>
829
- */
830
- listFriends(): Promise<Friend[]>;
839
+ createUGCItem(ugcType: UGCType, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<GenericId<"userGeneratedContent">>;
840
+ updateUGCItem(ugcId: GenericId<"userGeneratedContent">, updates?: UpdateUGCItemArgs): Promise<GenericId<"userGeneratedContent">>;
841
+ deleteUGCItem(ugcId: GenericId<"userGeneratedContent">): Promise<GenericId<"userGeneratedContent">>;
842
+ downloadUGCItem(ugcId: GenericId<"userGeneratedContent">, filePath: string): Promise<GenericId<"userGeneratedContent">>;
843
+ listUGCItems(args?: ListUGCItemsArgs): Promise<PaginatedUGCItems>;
831
844
  }
832
845
 
833
846
  /**
@@ -853,7 +866,7 @@ declare class IFrameMessenger {
853
866
  removeEventListener<T extends PushType>(type: T, listener: PushListener<T>): void;
854
867
  private handleMessage;
855
868
  postToParent(requestType: (typeof IFRAME_MESSAGE_TYPE)[keyof typeof IFRAME_MESSAGE_TYPE], data: Record<string, string | number | boolean>): boolean;
856
- requestFromParent<T extends keyof IFrameEventPayloadMap>(requestType: T, data?: Record<string, unknown>): Promise<IFrameEventPayloadMap[T]>;
869
+ requestFromParent<T extends keyof IFrameEventPayloadMap>(requestType: T, data?: Record<string, unknown>, timeoutMs?: number): Promise<IFrameEventPayloadMap[T]>;
857
870
  }
858
871
 
859
872
  /**
@@ -981,6 +994,7 @@ declare class WavedashSDK extends EventTarget {
981
994
  fullscreenManager: FullscreenManager;
982
995
  overlayManager: OverlayManager;
983
996
  audioManager: AudioManager;
997
+ paidContentManager: PaidContentManager;
984
998
  private managers;
985
999
  private gameplayJwt;
986
1000
  private gameplayJwtPromise;
@@ -1260,6 +1274,31 @@ declare class WavedashSDK extends EventTarget {
1260
1274
  sendLobbyMessage(lobbyId: GenericId<"lobbies">, message: string): boolean;
1261
1275
  inviteUserToLobby(lobbyId: GenericId<"lobbies">, userId: GenericId<"users">): Promise<WavedashResponse<boolean>>;
1262
1276
  getLobbyInviteLink(copyToClipboard?: boolean): Promise<WavedashResponse<string>>;
1277
+ /**
1278
+ * Returns true if the player owns the given paid content for this game.
1279
+ * Reads the `entitlements` claim from the gameplay JWT — this is a UX hint, not a
1280
+ * security check. The builds server re-verifies the JWT signature and gates
1281
+ * paid asset bytes on every request, so a tampered client return value
1282
+ * doesn't actually unlock anything. Pair with triggerPaywall() to drive
1283
+ * in-game UI.
1284
+ */
1285
+ isEntitled_EXPERIMENTAL(contentId: string): Promise<WavedashResponse<boolean>>;
1286
+ /**
1287
+ * Returns the full list of paid-content IDs the player owns for this game.
1288
+ * Reads the `entitlements` claim from the gameplay JWT — this is a UX hint,
1289
+ * not a security check (see {@link isEntitled_EXPERIMENTAL}). Useful
1290
+ * for access gating multiple items at once without a call per content ID.
1291
+ */
1292
+ getEntitlements_EXPERIMENTAL(): Promise<WavedashResponse<string[]>>;
1293
+ /**
1294
+ * Trigger the Wavedash-rendered paywall flow for the given content. Resolves
1295
+ * immediately with data `true` if the player already owns it; otherwise
1296
+ * opens the modal and resolves with whether the user completed the purchase.
1297
+ * After a successful purchase the JWT is refreshed automatically so a
1298
+ * subsequent resource fetch is authenticated with the new purchase, and isEntitled
1299
+ * will return true if the purchase was successful.
1300
+ */
1301
+ triggerPaywall_EXPERIMENTAL(contentIdentifier: string): Promise<WavedashResponse<boolean>>;
1263
1302
  /**
1264
1303
  * Updates rich user presence so friends can see what the player is doing in game.
1265
1304
  * Supported keys:
@@ -1277,17 +1316,15 @@ declare class WavedashSDK extends EventTarget {
1277
1316
  private apiCall;
1278
1317
  private apiCallSync;
1279
1318
  /**
1280
- * Fetches (or returns cached) gameplay JWT. Callers outside of Convex's
1281
- * setAuth should use {@link ensureGameplayJwt} instead; this method is the
1282
- * fetcher wired into `ConvexClient.setAuth` and honors `forceRefresh` so the
1283
- * server can invalidate a stale token.
1284
- *
1285
- * Same-origin POST to /auth/refresh on the play domain — the
1286
- * gameplaySession cookie set by the play server during playKey exchange
1287
- * authenticates the request, so no cross-origin credentials handling needed.
1319
+ * Fetcher wired into `ConvexClient.setAuth`; other callers use
1320
+ * {@link ensureGameplayJwt}. Same-origin POST to /auth/refresh, authenticated
1321
+ * by the gameplaySession cookie.
1288
1322
  *
1289
- * Concurrent callers share a single in-flight fetch to avoid duplicate
1290
- * refresh round-trips.
1323
+ * Concurrent callers share one in-flight fetch. A forced refresh instead
1324
+ * serializes behind any in-flight fetch (it may predate the event that
1325
+ * required it, e.g. a purchase) and becomes the current promise; only the
1326
+ * current promise notifies the parent, so a superseded refresh can't
1327
+ * broadcast a stale token
1291
1328
  */
1292
1329
  private getAuthToken;
1293
1330
  /**
@@ -1295,14 +1332,14 @@ declare class WavedashSDK extends EventTarget {
1295
1332
  * already running (e.g. from Convex's initial setAuth). Use this anywhere
1296
1333
  * you need to authenticate a request outside of the Convex client.
1297
1334
  */
1298
- ensureGameplayJwt(): Promise<string>;
1335
+ ensureGameplayJwt(forceRefresh?: boolean): Promise<string>;
1299
1336
  /**
1300
1337
  * Tear down every manager. Called on the parent's `END_SESSION` signal
1301
1338
  */
1302
1339
  private destroy;
1303
1340
  private setupSessionEndListeners;
1304
1341
  /**
1305
- * Respond to the service worker's `embed.creds-request` with the SDK's
1342
+ * Respond to the service worker's creds request with the SDK's
1306
1343
  * current gameplay JWT. The SW asks when it wakes from termination with no
1307
1344
  * in-memory or IDB credentials (e.g. Safari ITP storage decay) — we're the
1308
1345
  * fastest live source. JWT only; sessionToken is owned by the SW + cookies.