@thestatic-tv/dcl-sdk 2.3.0 → 2.5.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/README.md CHANGED
@@ -5,489 +5,117 @@ Connect your Decentraland scene to [thestatic.tv](https://thestatic.tv) - the de
5
5
  [![npm version](https://img.shields.io/npm/v/@thestatic-tv/dcl-sdk?color=00e5e5)](https://www.npmjs.com/package/@thestatic-tv/dcl-sdk)
6
6
  [![DCL SDK](https://img.shields.io/badge/DCL%20SDK-7.x-blue)](https://docs.decentraland.org/)
7
7
 
8
- This SDK allows DCL scene builders to:
9
- - Track visitors to your Decentraland scene
10
- - Display the full thestatic.tv channel lineup in their scenes
11
- - Track video watching metrics
12
- - Enable likes/follows from within DCL
13
- - Get referral credit for driving traffic to channels
8
+ ## Features
14
9
 
15
- ## Pricing
10
+ - **Visitor Analytics** - Track visitors and session metrics
11
+ - **Channel Guide** - Display live streams and VODs in your scene
12
+ - **Watch Metrics** - Track video viewing time
13
+ - **Interactions** - Enable likes/follows from within DCL
14
+ - **Built-in UI** - Pre-built Guide and Chat components
15
+ - **Admin Panel** - In-scene controls for Pro tier
16
16
 
17
- | Tier | Price | Features |
18
- |------|-------|----------|
19
- | **Free** | $5/mo | Session/visitor tracking only |
20
- | **Standard** | $10/mo | Free + Guide UI, Chat UI, Heartbeat, Interactions |
21
- | **Pro** | $15/mo | Standard + Admin Panel (Video tab, Mod tab, Custom scene tabs) |
22
-
23
- > **All keys use `dcls_` prefix** - the server determines your subscription level.
24
- >
25
- > **Free Trial**: All new scene keys include a 7-day free trial!
26
- >
27
- > **Expiry Warnings**: You'll receive dashboard notifications at 7, 3, and 1 days before your subscription expires.
28
-
29
- Get your key from [thestatic.tv/dashboard](https://thestatic.tv/dashboard) → DCL Scenes tab.
30
-
31
- ### Coordinate Locking
32
-
33
- When you create a scene key, you'll be asked to confirm your parcel coordinates. **Once confirmed, coordinates are permanently locked** to prevent key reuse across different scenes. This ensures analytics accurately reflect a single scene.
34
-
35
- ### Key Rotation
36
-
37
- If you need to rotate your API key (e.g., if compromised), you can do so from the dashboard without losing your subscription or analytics history. The old key is immediately invalidated.
38
-
39
- ## Example Scene
40
-
41
- Clone our example scene to get started quickly:
42
-
43
- ```bash
44
- git clone https://github.com/thestatic-tv/thestatic-dcl-example.git
45
- cd thestatic-dcl-example
46
- npm install
47
- npm start
48
- ```
17
+ ## Quick Start
49
18
 
50
- See the [example repo](https://github.com/thestatic-tv/thestatic-dcl-example) for a complete working scene with video player integration.
19
+ 1. Get your API key from [thestatic.tv/dashboard](https://thestatic.tv/dashboard) (DCL Scenes tab)
51
20
 
52
- ## Installation
21
+ 2. Install the SDK:
53
22
 
54
23
  ```bash
55
24
  npm install @thestatic-tv/dcl-sdk
56
25
  ```
57
26
 
58
- ## Quick Start
59
-
60
- 1. Get your API key from [thestatic.tv/dashboard](https://thestatic.tv/dashboard) (DCL Scenes tab)
61
-
62
- 2. Initialize the client in your scene's `main()` function:
27
+ 3. Initialize in your scene:
63
28
 
64
29
  ```typescript
65
- import {} from '@dcl/sdk/math'
66
- import { engine } from '@dcl/sdk/ecs'
67
30
  import { StaticTVClient } from '@thestatic-tv/dcl-sdk'
68
31
 
69
32
  let staticTV: StaticTVClient
70
33
 
71
34
  export function main() {
72
35
  staticTV = new StaticTVClient({
73
- apiKey: 'dcls_your_api_key_here'
36
+ apiKey: 'your_api_key_here'
74
37
  })
75
- // Session tracking starts automatically!
38
+ // Session tracking starts automatically
76
39
  }
77
40
  ```
78
41
 
79
- 3. Fetch channels and display them (Full mode only):
80
-
81
- ```typescript
82
- // Get all channels
83
- const channels = await staticTV.guide.getChannels()
42
+ ## Example Scene
84
43
 
85
- // Get only live channels
86
- const liveChannels = await staticTV.guide.getLiveChannels()
44
+ Clone our starter scene to get up and running quickly:
87
45
 
88
- // Get a specific channel
89
- const channel = await staticTV.guide.getChannel('channel-slug')
46
+ ```bash
47
+ git clone https://github.com/thestatic-tv/thestatic-dcl-starter.git
48
+ cd thestatic-dcl-starter
49
+ npm install
50
+ npm start
90
51
  ```
91
52
 
92
- 4. Track video watching:
53
+ ## Basic Usage
93
54
 
94
55
  ```typescript
95
- // When user enters video viewing area
96
- staticTV.heartbeat.startWatching('channel-slug')
56
+ // Get channels (Standard/Pro tier)
57
+ const channels = await staticTV.guide.getChannels()
58
+ const liveChannels = await staticTV.guide.getLiveChannels()
97
59
 
98
- // When user leaves video viewing area
60
+ // Track video watching
61
+ staticTV.heartbeat.startWatching('channel-slug')
99
62
  staticTV.heartbeat.stopWatching()
100
- ```
101
-
102
- 5. Enable interactions:
103
63
 
104
- ```typescript
105
- // Like a channel (requires wallet connection)
64
+ // User interactions (requires wallet)
106
65
  await staticTV.interactions.like('channel-slug')
107
-
108
- // Follow a channel
109
66
  await staticTV.interactions.follow('channel-slug')
110
- ```
111
67
 
112
- 6. Cleanup before scene unload:
68
+ // Check your tier
69
+ console.log('Tier:', staticTV.tier) // 'free', 'standard', or 'pro'
113
70
 
114
- ```typescript
71
+ // Cleanup
115
72
  await staticTV.destroy()
116
73
  ```
117
74
 
118
- ## Free Tier (Visitor Tracking Only)
119
-
120
- If you don't have a channel but want to track visitors to your scene:
121
-
122
- 1. Get a scene key from [thestatic.tv/dashboard](https://thestatic.tv/dashboard) (DCL Scenes tab)
123
-
124
- 2. Initialize with your scene key in `main()`:
125
-
126
- ```typescript
127
- import {} from '@dcl/sdk/math'
128
- import { engine } from '@dcl/sdk/ecs'
129
- import { StaticTVClient } from '@thestatic-tv/dcl-sdk'
130
-
131
- let staticTV: StaticTVClient
132
-
133
- export function main() {
134
- staticTV = new StaticTVClient({
135
- apiKey: 'dcls_your_scene_key_here'
136
- })
137
- // Session tracking starts automatically!
138
- }
139
- ```
140
-
141
- 3. Check the current tier:
142
-
143
- ```typescript
144
- // Check tier (free, standard, or pro)
145
- console.log('Current tier:', staticTV.tier)
146
-
147
- if (staticTV.isFree) {
148
- console.log('Running in free tier - session tracking only')
149
- }
150
-
151
- // guide, heartbeat, and interactions are null in free tier
152
- if (staticTV.guide) {
153
- const channels = await staticTV.guide.getChannels()
154
- }
155
- ```
156
-
157
- ## API Reference
158
-
159
- ### StaticTVClient
160
-
161
- Main client class for interacting with thestatic.tv.
162
-
163
- #### Constructor Options
164
-
165
- | Option | Type | Default | Description |
166
- |--------|------|---------|-------------|
167
- | `apiKey` | `string` | required | Your API key (all keys use `dcls_` prefix) |
168
- | `autoStartSession` | `boolean` | `true` | Automatically start session tracking |
169
- | `sessionHeartbeatInterval` | `number` | `30000` | Session heartbeat interval (ms) |
170
- | `watchHeartbeatInterval` | `number` | `60000` | Watch heartbeat interval (ms) |
171
- | `debug` | `boolean` | `false` | Enable debug logging |
172
-
173
- #### Properties
174
-
175
- | Property | Type | Description |
176
- |----------|------|-------------|
177
- | `keyType` | `'channel' \| 'scene'` | The detected key type |
178
- | `tier` | `'free' \| 'standard' \| 'pro'` | Current subscription tier |
179
- | `isFree` | `boolean` | `true` if free tier (session tracking only) |
180
- | `hasStandardFeatures` | `boolean` | `true` if standard features enabled |
181
- | `hasProFeatures` | `boolean` | `true` if pro features enabled |
182
- | `guide` | `GuideModule \| null` | Guide module (null in free tier) |
183
- | `session` | `SessionModule` | Session module (always available) |
184
- | `heartbeat` | `HeartbeatModule \| null` | Heartbeat module (null in free tier) |
185
- | `interactions` | `InteractionsModule \| null` | Interactions module (null in free tier) |
186
- | `guideUI` | `GuideUIModule \| null` | Guide UI module (null in free tier) |
187
- | `chatUI` | `ChatUIModule \| null` | Chat UI module (null in free tier) |
188
- | `adminPanel` | `AdminPanelUIModule \| null` | Admin panel (null until enableProFeatures() called) |
189
- | `isLite` | `boolean` | **Deprecated**: Use `isFree` instead |
190
-
191
- #### Methods
192
-
193
- | Method | Description |
194
- |--------|-------------|
195
- | `enableProFeatures(config)` | Enable Pro tier admin panel |
196
- | `registerSceneTab(tab)` | Add custom scene tab (Pro tier) |
197
- | `destroy()` | Cleanup before scene unload |
198
-
199
- ### Guide Module (Standard/Pro Tier)
200
-
201
- ```typescript
202
- staticTV.guide.getChannels() // Get all channels (cached 30s)
203
- staticTV.guide.getLiveChannels() // Get only live channels
204
- staticTV.guide.getChannel(slug) // Get a specific channel
205
- staticTV.guide.getVods() // Get VODs
206
- staticTV.guide.clearCache() // Clear the channel cache
207
- ```
208
-
209
- ### Session Module
210
-
211
- Session tracking starts automatically by default.
212
-
213
- ```typescript
214
- staticTV.session.startSession() // Manually start session
215
- staticTV.session.endSession() // End session
216
- staticTV.session.getSessionId() // Get current session ID
217
- staticTV.session.isSessionActive() // Check if session is active
218
-
219
- // Get scene stats (visitors, sessions, etc.)
220
- const stats = await staticTV.session.getStats()
221
- if (stats) {
222
- console.log('Total sessions today:', stats.totalSessions)
223
- console.log('Unique visitors:', stats.uniqueVisitors)
224
- console.log('Total minutes watched:', stats.totalMinutes)
225
- console.log('You are visitor #', stats.visitorNumber)
226
- }
227
- ```
228
-
229
- ### Heartbeat Module (Standard/Pro Tier)
230
-
231
- Track video watching. Each heartbeat represents 1 minute watched.
232
-
233
- ```typescript
234
- staticTV.heartbeat.startWatching(channelSlug) // Start tracking
235
- staticTV.heartbeat.stopWatching() // Stop tracking
236
- staticTV.heartbeat.getCurrentChannel() // Get currently watched channel
237
- staticTV.heartbeat.isCurrentlyWatching() // Check if watching
238
- ```
239
-
240
- ### Interactions Module (Standard/Pro Tier)
75
+ ## Built-in UI Components
241
76
 
242
- Like and follow channels. Requires wallet connection.
77
+ The SDK includes pre-built UI for channel browsing and chat:
243
78
 
244
79
  ```typescript
245
- staticTV.interactions.like(channelSlug) // Like a channel
246
- staticTV.interactions.follow(channelSlug) // Follow a channel
247
- ```
248
-
249
- ### Guide UI Module (Standard/Pro Tier)
250
-
251
- Built-in channel browser UI. You provide a callback to handle video selection.
252
-
253
- ```typescript
254
- let staticTV: StaticTVClient
255
-
256
- export function main() {
257
- staticTV = new StaticTVClient({
258
- apiKey: 'dcls_your_api_key',
259
- guideUI: {
260
- onVideoSelect: (video) => {
261
- // Handle video playback in your scene
262
- console.log('Playing:', video.name, video.src)
263
- }
80
+ staticTV = new StaticTVClient({
81
+ apiKey: 'your_api_key',
82
+ guideUI: {
83
+ onVideoSelect: (video) => {
84
+ // Handle video playback
85
+ console.log('Playing:', video.name)
264
86
  }
265
- })
87
+ },
88
+ chatUI: {
89
+ position: 'right'
90
+ }
91
+ })
266
92
 
267
- // Initialize the UI (call once after client is created)
268
- staticTV.guideUI.init()
269
- }
93
+ // Initialize UI components
94
+ staticTV.guideUI.init()
95
+ staticTV.chatUI.init()
270
96
 
271
- // Show/hide the guide
272
- staticTV.guideUI.show()
273
- staticTV.guideUI.hide()
97
+ // Show/hide
274
98
  staticTV.guideUI.toggle()
275
-
276
- // Check visibility
277
- if (staticTV.guideUI.isVisible) { ... }
278
-
279
- // Update "PLAYING" indicator
280
- staticTV.guideUI.currentVideoId = 'video-id'
281
-
282
- // Refresh data
283
- await staticTV.guideUI.refresh()
284
- ```
285
-
286
- ### Chat UI Module (Standard/Pro Tier)
287
-
288
- Real-time chat with Firebase authentication.
289
-
290
- ```typescript
291
- let staticTV: StaticTVClient
292
-
293
- export function main() {
294
- staticTV = new StaticTVClient({
295
- apiKey: 'dcls_your_api_key',
296
- chatUI: {
297
- position: 'right', // 'left', 'center', or 'right'
298
- fontScale: 1.0
299
- }
300
- })
301
-
302
- // Initialize the chat (call once after client is created)
303
- staticTV.chatUI.init()
304
- }
305
-
306
- // Show/hide the chat
307
- staticTV.chatUI.show()
308
- staticTV.chatUI.hide()
309
99
  staticTV.chatUI.toggle()
310
-
311
- // Check visibility
312
- if (staticTV.chatUI.isVisible) { ... }
313
-
314
- // Get unread message count (for badge display)
315
- const unread = staticTV.chatUI.unreadCount
316
-
317
- // Switch channels
318
- staticTV.chatUI.setChannel('channel-slug')
319
- ```
320
-
321
- ### Admin Panel Module (Pro Tier)
322
-
323
- In-scene admin panel with Video and Mod tabs. Allows scene owners/admins to:
324
- - Control live streaming (start/stop, rotate keys)
325
- - Play videos from URL or predefined slots
326
- - Manage scene admins and banned wallets
327
- - Send broadcast messages to all players
328
-
329
- ```typescript
330
- import { StaticTVClient } from '@thestatic-tv/dcl-sdk'
331
- import ReactEcs, { ReactEcsRenderer } from '@dcl/sdk/react-ecs'
332
-
333
- let staticTV: StaticTVClient
334
-
335
- export function main() {
336
- staticTV = new StaticTVClient({
337
- apiKey: 'dcls_your_api_key'
338
- })
339
-
340
- // Enable admin panel (Pro tier)
341
- staticTV.enableProFeatures({
342
- // sceneId is optional - defaults to your API key ID
343
- title: 'MY SCENE ADMIN', // Panel header title
344
- onVideoPlay: (url) => {
345
- // Handle video playback in your scene
346
- videoPlayer.play(url)
347
- },
348
- onVideoStop: () => {
349
- videoPlayer.stop()
350
- },
351
- onVideoSlotPlay: (slot) => {
352
- // Play a predefined video slot (slot1-slot5)
353
- fetchAndPlaySlot(slot)
354
- },
355
- onBroadcast: (text) => {
356
- // Show notification to all players
357
- showNotification(text)
358
- },
359
- onCommand: (type, payload) => {
360
- // Handle custom commands (kickAll, kickBanned, etc.)
361
- handleCommand(type, payload)
362
- }
363
- })
364
- }
365
-
366
- // Render the admin panel
367
- ReactEcsRenderer.setUiRenderer(() => {
368
- return staticTV.adminPanel?.getComponent()
369
- })
370
-
371
- // Toggle panel visibility
372
- staticTV.adminPanel.toggle()
373
-
374
- // Check if user has admin access
375
- if (staticTV.adminPanel.hasAccess) { ... }
376
100
  ```
377
101
 
378
- #### Admin Panel Configuration Options
379
-
380
- | Option | Type | Default | Description |
381
- |--------|------|---------|-------------|
382
- | `sceneId` | `string` | API key ID | Scene ID for API calls (optional - defaults to your API key ID) |
383
- | `title` | `string` | `'ADMIN PANEL'` | Header title |
384
- | `headerColor` | `{r,g,b,a}` | red | Header background color |
385
- | `showVideoTab` | `boolean` | `true` | Show Video tab |
386
- | `showModTab` | `boolean` | `true` | Show Mod tab (owners only) |
387
- | `onVideoPlay` | `(url) => void` | - | Called when video should play |
388
- | `onVideoStop` | `() => void` | - | Called when video should stop |
389
- | `onVideoSlotPlay` | `(slot) => void` | - | Called when slot is selected |
390
- | `onBroadcast` | `(text) => void` | - | Called for broadcast messages |
391
- | `onCommand` | `(type, payload) => void` | - | Called for custom commands |
392
- | `footerLink` | `string` | scene page | Link shown in footer |
393
- | `debug` | `boolean` | `false` | Enable debug logging |
394
-
395
- ### Custom Scene Tabs (Pro Tier)
102
+ ## Pricing & Tiers
396
103
 
397
- Add your own scene-specific control tabs to the admin panel:
104
+ See [thestatic.tv/info](https://thestatic.tv/info) for current pricing and tier features.
398
105
 
399
- ```typescript
400
- import ReactEcs, { UiEntity, Button, Label } from '@dcl/sdk/react-ecs'
401
-
402
- // Enable pro features first
403
- staticTV.enableProFeatures({ sceneId: 'my-scene', ... })
404
-
405
- // Register custom tabs
406
- staticTV.registerSceneTab({
407
- label: 'LIGHTS',
408
- id: 'lights',
409
- render: () => (
410
- <UiEntity uiTransform={{ flexDirection: 'column', padding: 8 }}>
411
- <Label value="Light Controls" fontSize={14} />
412
- <Button value="Disco Mode" onMouseDown={() => setDiscoLights()} />
413
- <Button value="Ambient" onMouseDown={() => setAmbientLights()} />
414
- <Button value="Off" onMouseDown={() => turnOffLights()} />
415
- </UiEntity>
416
- )
417
- })
418
-
419
- staticTV.registerSceneTab({
420
- label: 'DOORS',
421
- id: 'doors',
422
- render: () => <MyDoorsControls />
423
- })
424
- ```
106
+ All new keys include a **7-day free trial**.
425
107
 
426
- Custom tabs appear before the Video and Mod tabs in the tab bar.
108
+ ## Documentation
427
109
 
428
- ### Rendering UI Components
110
+ Full API reference and Pro tier features (Admin Panel, Custom Tabs) available at:
429
111
 
430
- The UI modules provide `getComponent()` methods that return React-ECS elements. Add the renderer **outside** your `main()` function:
431
-
432
- ```typescript
433
- import ReactEcs, { ReactEcsRenderer, UiEntity } from '@dcl/sdk/react-ecs'
434
-
435
- // Outside main() - required by DCL
436
- ReactEcsRenderer.setUiRenderer(() => {
437
- if (!staticTV) return null
438
- return ReactEcs.createElement(UiEntity, {
439
- uiTransform: { width: '100%', height: '100%', positionType: 'absolute' },
440
- children: [
441
- staticTV.guideUI?.getComponent(),
442
- staticTV.chatUI?.getComponent()
443
- ].filter(Boolean)
444
- })
445
- })
446
- ```
447
-
448
- ## Metrics Attribution
449
-
450
- This SDK implements dual attribution:
451
-
452
- - **Watched Channel**: Gets view metrics (minutes watched, likes, follows)
453
- - **Scene Owner**: Gets referral metrics (unique visitors, traffic driven)
454
-
455
- Your channel (linked to your API key) receives credit for all traffic your scene drives to thestatic.tv channels.
456
-
457
- ## Types
458
-
459
- ```typescript
460
- interface Channel {
461
- id: string
462
- slug: string
463
- name: string
464
- streamUrl: string
465
- isLive: boolean
466
- currentViewers: number
467
- poster: string | null
468
- logo: string | null
469
- description: string
470
- }
471
-
472
- interface Vod {
473
- id: string
474
- title: string
475
- url: string
476
- thumbnail: string | null
477
- channelId: string
478
- }
479
- ```
480
-
481
- ## Requirements
482
-
483
- - Decentraland SDK 7.0.0 or higher
484
- - API key from thestatic.tv
112
+ - **Docs**: [thestatic.tv/info](https://thestatic.tv/info)
113
+ - **Examples**: [github.com/thestatic-tv/thestatic-dcl-starter](https://github.com/thestatic-tv/thestatic-dcl-starter)
485
114
 
486
115
  ## Support
487
116
 
488
- - Documentation: [thestatic.tv/info](https://thestatic.tv/info)
489
- - Issues: [GitHub Issues](https://github.com/thestatic-tv/dcl-sdk/issues)
490
117
  - Discord: [thestatic.tv Discord](https://discord.gg/thestatic)
118
+ - Issues: [GitHub Issues](https://github.com/thestatic-tv/dcl-sdk/issues)
491
119
 
492
120
  ## License
493
121
 
package/dist/index.d.mts CHANGED
@@ -45,6 +45,12 @@ interface StaticTVConfig {
45
45
  * ```
46
46
  */
47
47
  videoScreen?: Entity;
48
+ /**
49
+ * Fallback video URL to play when no stream is active or stopVideo() is called.
50
+ * Prevents "broken screen" appearance. Set to empty string to disable.
51
+ * @default 'https://media.thestatic.tv/fallback-loop.mp4'
52
+ */
53
+ fallbackVideoUrl?: string;
48
54
  /**
49
55
  * Scene ID for Admin Panel API calls.
50
56
  * Optional - defaults to API key ID if not provided.
@@ -863,6 +869,9 @@ declare class StaticTVClient {
863
869
  private _standardFeaturesEnabled;
864
870
  private _proFeaturesEnabled;
865
871
  private _pendingProConfig;
872
+ private _featuresReadyPromise;
873
+ private _featuresReadyResolve;
874
+ private _featuresResolved;
866
875
  /** Guide module - fetch channel lineup (standard/pro tier) */
867
876
  guide: GuideModule | null;
868
877
  /** Session module - track visitor sessions (all tiers, null when disabled) */
@@ -933,6 +942,33 @@ declare class StaticTVClient {
933
942
  * @deprecated Use `isFree` instead. Kept for backward compatibility.
934
943
  */
935
944
  get isLite(): boolean;
945
+ /**
946
+ * Wait for tier confirmation from the server.
947
+ * Resolves when the SDK knows which features are available.
948
+ *
949
+ * Use this instead of polling `isLite` or `isFree` in a while loop.
950
+ *
951
+ * @returns Promise that resolves with the confirmed tier ('free', 'standard', or 'pro')
952
+ *
953
+ * @example
954
+ * ```typescript
955
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
956
+ *
957
+ * // Wait for tier to be confirmed
958
+ * const tier = await staticTV.onFeaturesReady()
959
+ *
960
+ * // Now modules are guaranteed to be initialized (or confirmed unavailable)
961
+ * if (staticTV.guideUI) {
962
+ * await staticTV.guideUI.init()
963
+ * console.log('Guide ready!')
964
+ * }
965
+ *
966
+ * if (tier === 'free') {
967
+ * console.log('Upgrade at thestatic.tv for Guide & Chat!')
968
+ * }
969
+ * ```
970
+ */
971
+ onFeaturesReady(): Promise<SDKTier>;
936
972
  /**
937
973
  * Make an authenticated API request
938
974
  * @internal
@@ -968,9 +1004,15 @@ declare class StaticTVClient {
968
1004
  playVideo(url: string): void;
969
1005
  /**
970
1006
  * Stop video playback on the configured videoScreen entity.
1007
+ * If fallbackVideoUrl is configured (default), plays the fallback loop.
971
1008
  * Called by Admin Panel.
972
1009
  */
973
1010
  stopVideo(): void;
1011
+ /**
1012
+ * Internal video player - handles both regular videos and fallback
1013
+ * @internal
1014
+ */
1015
+ private _playVideoInternal;
974
1016
  /**
975
1017
  * Get the currently playing video URL
976
1018
  */
@@ -995,6 +1037,11 @@ declare class StaticTVClient {
995
1037
  * @internal
996
1038
  */
997
1039
  private _initProModules;
1040
+ /**
1041
+ * Resolve the features ready promise (only once)
1042
+ * @internal
1043
+ */
1044
+ private _resolveFeaturesReady;
998
1045
  /**
999
1046
  * Called by SessionModule when server returns the tier
1000
1047
  * Enables modules based on tier level
package/dist/index.d.ts CHANGED
@@ -45,6 +45,12 @@ interface StaticTVConfig {
45
45
  * ```
46
46
  */
47
47
  videoScreen?: Entity;
48
+ /**
49
+ * Fallback video URL to play when no stream is active or stopVideo() is called.
50
+ * Prevents "broken screen" appearance. Set to empty string to disable.
51
+ * @default 'https://media.thestatic.tv/fallback-loop.mp4'
52
+ */
53
+ fallbackVideoUrl?: string;
48
54
  /**
49
55
  * Scene ID for Admin Panel API calls.
50
56
  * Optional - defaults to API key ID if not provided.
@@ -863,6 +869,9 @@ declare class StaticTVClient {
863
869
  private _standardFeaturesEnabled;
864
870
  private _proFeaturesEnabled;
865
871
  private _pendingProConfig;
872
+ private _featuresReadyPromise;
873
+ private _featuresReadyResolve;
874
+ private _featuresResolved;
866
875
  /** Guide module - fetch channel lineup (standard/pro tier) */
867
876
  guide: GuideModule | null;
868
877
  /** Session module - track visitor sessions (all tiers, null when disabled) */
@@ -933,6 +942,33 @@ declare class StaticTVClient {
933
942
  * @deprecated Use `isFree` instead. Kept for backward compatibility.
934
943
  */
935
944
  get isLite(): boolean;
945
+ /**
946
+ * Wait for tier confirmation from the server.
947
+ * Resolves when the SDK knows which features are available.
948
+ *
949
+ * Use this instead of polling `isLite` or `isFree` in a while loop.
950
+ *
951
+ * @returns Promise that resolves with the confirmed tier ('free', 'standard', or 'pro')
952
+ *
953
+ * @example
954
+ * ```typescript
955
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
956
+ *
957
+ * // Wait for tier to be confirmed
958
+ * const tier = await staticTV.onFeaturesReady()
959
+ *
960
+ * // Now modules are guaranteed to be initialized (or confirmed unavailable)
961
+ * if (staticTV.guideUI) {
962
+ * await staticTV.guideUI.init()
963
+ * console.log('Guide ready!')
964
+ * }
965
+ *
966
+ * if (tier === 'free') {
967
+ * console.log('Upgrade at thestatic.tv for Guide & Chat!')
968
+ * }
969
+ * ```
970
+ */
971
+ onFeaturesReady(): Promise<SDKTier>;
936
972
  /**
937
973
  * Make an authenticated API request
938
974
  * @internal
@@ -968,9 +1004,15 @@ declare class StaticTVClient {
968
1004
  playVideo(url: string): void;
969
1005
  /**
970
1006
  * Stop video playback on the configured videoScreen entity.
1007
+ * If fallbackVideoUrl is configured (default), plays the fallback loop.
971
1008
  * Called by Admin Panel.
972
1009
  */
973
1010
  stopVideo(): void;
1011
+ /**
1012
+ * Internal video player - handles both regular videos and fallback
1013
+ * @internal
1014
+ */
1015
+ private _playVideoInternal;
974
1016
  /**
975
1017
  * Get the currently playing video URL
976
1018
  */
@@ -995,6 +1037,11 @@ declare class StaticTVClient {
995
1037
  * @internal
996
1038
  */
997
1039
  private _initProModules;
1040
+ /**
1041
+ * Resolve the features ready promise (only once)
1042
+ * @internal
1043
+ */
1044
+ private _resolveFeaturesReady;
998
1045
  /**
999
1046
  * Called by SessionModule when server returns the tier
1000
1047
  * Enables modules based on tier level
package/dist/index.js CHANGED
@@ -2540,7 +2540,7 @@ var AdminPanelUIModule = class {
2540
2540
  {
2541
2541
  uiTransform: { height: this.s(24) },
2542
2542
  uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
2543
- value: "Edit slots at thestatic.tv \u2192",
2543
+ value: "Edit slots at thestatic.tv",
2544
2544
  fontSize: t.labelSmall,
2545
2545
  color: C.cyan,
2546
2546
  onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
@@ -2695,7 +2695,7 @@ var AdminPanelUIModule = class {
2695
2695
  {
2696
2696
  uiTransform: { height: this.s(24) },
2697
2697
  uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
2698
- value: "Manage at thestatic.tv \u2192",
2698
+ value: "Manage at thestatic.tv",
2699
2699
  fontSize: t.labelSmall,
2700
2700
  color: C.cyan,
2701
2701
  onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
@@ -2821,7 +2821,7 @@ var AdminPanelUIModule = class {
2821
2821
  {
2822
2822
  uiTransform: { height: this.s(24) },
2823
2823
  uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
2824
- value: `thestatic.tv/scene/${this.config.sceneId} \u2192`,
2824
+ value: `thestatic.tv/scene/${this.config.sceneId}`,
2825
2825
  fontSize: t.labelSmall,
2826
2826
  color: C.cyan,
2827
2827
  onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
@@ -3494,6 +3494,7 @@ function setupStaticUI(client) {
3494
3494
  // src/StaticTVClient.ts
3495
3495
  var import_ecs3 = require("@dcl/sdk/ecs");
3496
3496
  var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
3497
+ var DEFAULT_FALLBACK_VIDEO = "https://media.thestatic.tv/fallback-loop.mp4";
3497
3498
  var KEY_TYPE_CHANNEL = "channel";
3498
3499
  var KEY_TYPE_SCENE = "scene";
3499
3500
  var StaticTVClient = class {
@@ -3535,6 +3536,7 @@ var StaticTVClient = class {
3535
3536
  this._standardFeaturesEnabled = false;
3536
3537
  this._proFeaturesEnabled = false;
3537
3538
  this._pendingProConfig = null;
3539
+ this._featuresResolved = false;
3538
3540
  /** Guide module - fetch channel lineup (standard/pro tier) */
3539
3541
  this.guide = null;
3540
3542
  /** Session module - track visitor sessions (all tiers, null when disabled) */
@@ -3555,6 +3557,9 @@ var StaticTVClient = class {
3555
3557
  // --- VIDEO PLAYBACK (Internal handler for videoScreen) ---
3556
3558
  // =============================================================================
3557
3559
  this._currentVideoUrl = "";
3560
+ this._featuresReadyPromise = new Promise((resolve) => {
3561
+ this._featuresReadyResolve = resolve;
3562
+ });
3558
3563
  this.config = {
3559
3564
  autoStartSession: true,
3560
3565
  sessionHeartbeatInterval: 3e4,
@@ -3572,6 +3577,7 @@ var StaticTVClient = class {
3572
3577
  this.interactions = null;
3573
3578
  this.guideUI = null;
3574
3579
  this.chatUI = null;
3580
+ this._resolveFeaturesReady("free");
3575
3581
  return;
3576
3582
  }
3577
3583
  if (config.apiKey.startsWith("dclk_")) {
@@ -3582,12 +3588,14 @@ var StaticTVClient = class {
3582
3588
  console.warn("[TheStatic] Invalid API key format - get your key at thestatic.tv/dashboard");
3583
3589
  this._disabled = true;
3584
3590
  this._keyType = null;
3591
+ this._resolveFeaturesReady("free");
3585
3592
  return;
3586
3593
  }
3587
3594
  this.session = new SessionModule(this);
3588
3595
  if (this._keyType === KEY_TYPE_CHANNEL) {
3589
3596
  this._tier = "standard";
3590
3597
  this._initStandardModules();
3598
+ this._resolveFeaturesReady("standard");
3591
3599
  }
3592
3600
  if (this.config.autoStartSession) {
3593
3601
  fetchUserData().then(() => {
@@ -3638,6 +3646,35 @@ var StaticTVClient = class {
3638
3646
  get isLite() {
3639
3647
  return this.isFree;
3640
3648
  }
3649
+ /**
3650
+ * Wait for tier confirmation from the server.
3651
+ * Resolves when the SDK knows which features are available.
3652
+ *
3653
+ * Use this instead of polling `isLite` or `isFree` in a while loop.
3654
+ *
3655
+ * @returns Promise that resolves with the confirmed tier ('free', 'standard', or 'pro')
3656
+ *
3657
+ * @example
3658
+ * ```typescript
3659
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3660
+ *
3661
+ * // Wait for tier to be confirmed
3662
+ * const tier = await staticTV.onFeaturesReady()
3663
+ *
3664
+ * // Now modules are guaranteed to be initialized (or confirmed unavailable)
3665
+ * if (staticTV.guideUI) {
3666
+ * await staticTV.guideUI.init()
3667
+ * console.log('Guide ready!')
3668
+ * }
3669
+ *
3670
+ * if (tier === 'free') {
3671
+ * console.log('Upgrade at thestatic.tv for Guide & Chat!')
3672
+ * }
3673
+ * ```
3674
+ */
3675
+ async onFeaturesReady() {
3676
+ return this._featuresReadyPromise;
3677
+ }
3641
3678
  /**
3642
3679
  * Make an authenticated API request
3643
3680
  * @internal
@@ -3695,17 +3732,7 @@ var StaticTVClient = class {
3695
3732
  if (screen !== void 0) {
3696
3733
  this.log(`Playing video: ${url}`);
3697
3734
  this._currentVideoUrl = url;
3698
- if (import_ecs3.VideoPlayer.has(screen)) {
3699
- import_ecs3.VideoPlayer.deleteFrom(screen);
3700
- }
3701
- import_ecs3.VideoPlayer.create(screen, {
3702
- src: url,
3703
- playing: true,
3704
- volume: 1
3705
- });
3706
- import_ecs3.Material.setBasicMaterial(screen, {
3707
- texture: import_ecs3.Material.Texture.Video({ videoPlayerEntity: screen })
3708
- });
3735
+ this._playVideoInternal(url, false);
3709
3736
  if (this.guideUI) {
3710
3737
  const videos = this.guideUI.getVideos();
3711
3738
  const video = videos.find((v) => v.src === url);
@@ -3720,22 +3747,55 @@ var StaticTVClient = class {
3720
3747
  }
3721
3748
  /**
3722
3749
  * Stop video playback on the configured videoScreen entity.
3750
+ * If fallbackVideoUrl is configured (default), plays the fallback loop.
3723
3751
  * Called by Admin Panel.
3724
3752
  */
3725
3753
  stopVideo() {
3726
3754
  const screen = this.config.videoScreen;
3727
- if (screen !== void 0 && import_ecs3.VideoPlayer.has(screen)) {
3728
- this.log("Stopping video");
3729
- import_ecs3.VideoPlayer.getMutable(screen).playing = false;
3730
- this._currentVideoUrl = "";
3755
+ if (screen !== void 0) {
3731
3756
  if (this.guideUI) {
3732
3757
  this.guideUI.currentVideoId = null;
3733
3758
  }
3759
+ const fallbackUrl = this.config.fallbackVideoUrl;
3760
+ const fallbackDisabled = fallbackUrl === "";
3761
+ if (fallbackDisabled) {
3762
+ if (import_ecs3.VideoPlayer.has(screen)) {
3763
+ this.log("Stopping video (no fallback)");
3764
+ import_ecs3.VideoPlayer.getMutable(screen).playing = false;
3765
+ }
3766
+ this._currentVideoUrl = "";
3767
+ } else {
3768
+ const url = fallbackUrl || DEFAULT_FALLBACK_VIDEO;
3769
+ this.log(`Playing fallback: ${url}`);
3770
+ this._currentVideoUrl = "";
3771
+ this._playVideoInternal(url, true);
3772
+ }
3734
3773
  }
3735
3774
  if (this.config.onVideoStop) {
3736
3775
  this.config.onVideoStop();
3737
3776
  }
3738
3777
  }
3778
+ /**
3779
+ * Internal video player - handles both regular videos and fallback
3780
+ * @internal
3781
+ */
3782
+ _playVideoInternal(url, isFallback = false) {
3783
+ const screen = this.config.videoScreen;
3784
+ if (screen === void 0) return;
3785
+ if (import_ecs3.VideoPlayer.has(screen)) {
3786
+ import_ecs3.VideoPlayer.deleteFrom(screen);
3787
+ }
3788
+ import_ecs3.VideoPlayer.create(screen, {
3789
+ src: url,
3790
+ playing: true,
3791
+ loop: isFallback,
3792
+ // Loop fallback videos
3793
+ volume: 1
3794
+ });
3795
+ import_ecs3.Material.setBasicMaterial(screen, {
3796
+ texture: import_ecs3.Material.Texture.Video({ videoPlayerEntity: screen })
3797
+ });
3798
+ }
3739
3799
  /**
3740
3800
  * Get the currently playing video URL
3741
3801
  */
@@ -3818,6 +3878,16 @@ var StaticTVClient = class {
3818
3878
  });
3819
3879
  this.log(`Pro features enabled (admin panel) - sceneId: ${sceneId}`);
3820
3880
  }
3881
+ /**
3882
+ * Resolve the features ready promise (only once)
3883
+ * @internal
3884
+ */
3885
+ _resolveFeaturesReady(tier) {
3886
+ if (!this._featuresResolved) {
3887
+ this._featuresResolved = true;
3888
+ this._featuresReadyResolve(tier);
3889
+ }
3890
+ }
3821
3891
  /**
3822
3892
  * Called by SessionModule when server returns the tier
3823
3893
  * Enables modules based on tier level
@@ -3839,6 +3909,7 @@ var StaticTVClient = class {
3839
3909
  this.log("Pro tier detected but no video config - call enableProFeatures() or set videoScreen to enable admin panel");
3840
3910
  }
3841
3911
  }
3912
+ this._resolveFeaturesReady(tier);
3842
3913
  }
3843
3914
  /**
3844
3915
  * @deprecated Use `_enableFeaturesForTier` instead
package/dist/index.mjs CHANGED
@@ -2497,7 +2497,7 @@ var AdminPanelUIModule = class {
2497
2497
  {
2498
2498
  uiTransform: { height: this.s(24) },
2499
2499
  uiBackground: { color: Color45.create(0, 0, 0, 0) },
2500
- value: "Edit slots at thestatic.tv \u2192",
2500
+ value: "Edit slots at thestatic.tv",
2501
2501
  fontSize: t.labelSmall,
2502
2502
  color: C.cyan,
2503
2503
  onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
@@ -2652,7 +2652,7 @@ var AdminPanelUIModule = class {
2652
2652
  {
2653
2653
  uiTransform: { height: this.s(24) },
2654
2654
  uiBackground: { color: Color45.create(0, 0, 0, 0) },
2655
- value: "Manage at thestatic.tv \u2192",
2655
+ value: "Manage at thestatic.tv",
2656
2656
  fontSize: t.labelSmall,
2657
2657
  color: C.cyan,
2658
2658
  onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
@@ -2778,7 +2778,7 @@ var AdminPanelUIModule = class {
2778
2778
  {
2779
2779
  uiTransform: { height: this.s(24) },
2780
2780
  uiBackground: { color: Color45.create(0, 0, 0, 0) },
2781
- value: `thestatic.tv/scene/${this.config.sceneId} \u2192`,
2781
+ value: `thestatic.tv/scene/${this.config.sceneId}`,
2782
2782
  fontSize: t.labelSmall,
2783
2783
  color: C.cyan,
2784
2784
  onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
@@ -3451,6 +3451,7 @@ function setupStaticUI(client) {
3451
3451
  // src/StaticTVClient.ts
3452
3452
  import { VideoPlayer, Material } from "@dcl/sdk/ecs";
3453
3453
  var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
3454
+ var DEFAULT_FALLBACK_VIDEO = "https://media.thestatic.tv/fallback-loop.mp4";
3454
3455
  var KEY_TYPE_CHANNEL = "channel";
3455
3456
  var KEY_TYPE_SCENE = "scene";
3456
3457
  var StaticTVClient = class {
@@ -3492,6 +3493,7 @@ var StaticTVClient = class {
3492
3493
  this._standardFeaturesEnabled = false;
3493
3494
  this._proFeaturesEnabled = false;
3494
3495
  this._pendingProConfig = null;
3496
+ this._featuresResolved = false;
3495
3497
  /** Guide module - fetch channel lineup (standard/pro tier) */
3496
3498
  this.guide = null;
3497
3499
  /** Session module - track visitor sessions (all tiers, null when disabled) */
@@ -3512,6 +3514,9 @@ var StaticTVClient = class {
3512
3514
  // --- VIDEO PLAYBACK (Internal handler for videoScreen) ---
3513
3515
  // =============================================================================
3514
3516
  this._currentVideoUrl = "";
3517
+ this._featuresReadyPromise = new Promise((resolve) => {
3518
+ this._featuresReadyResolve = resolve;
3519
+ });
3515
3520
  this.config = {
3516
3521
  autoStartSession: true,
3517
3522
  sessionHeartbeatInterval: 3e4,
@@ -3529,6 +3534,7 @@ var StaticTVClient = class {
3529
3534
  this.interactions = null;
3530
3535
  this.guideUI = null;
3531
3536
  this.chatUI = null;
3537
+ this._resolveFeaturesReady("free");
3532
3538
  return;
3533
3539
  }
3534
3540
  if (config.apiKey.startsWith("dclk_")) {
@@ -3539,12 +3545,14 @@ var StaticTVClient = class {
3539
3545
  console.warn("[TheStatic] Invalid API key format - get your key at thestatic.tv/dashboard");
3540
3546
  this._disabled = true;
3541
3547
  this._keyType = null;
3548
+ this._resolveFeaturesReady("free");
3542
3549
  return;
3543
3550
  }
3544
3551
  this.session = new SessionModule(this);
3545
3552
  if (this._keyType === KEY_TYPE_CHANNEL) {
3546
3553
  this._tier = "standard";
3547
3554
  this._initStandardModules();
3555
+ this._resolveFeaturesReady("standard");
3548
3556
  }
3549
3557
  if (this.config.autoStartSession) {
3550
3558
  fetchUserData().then(() => {
@@ -3595,6 +3603,35 @@ var StaticTVClient = class {
3595
3603
  get isLite() {
3596
3604
  return this.isFree;
3597
3605
  }
3606
+ /**
3607
+ * Wait for tier confirmation from the server.
3608
+ * Resolves when the SDK knows which features are available.
3609
+ *
3610
+ * Use this instead of polling `isLite` or `isFree` in a while loop.
3611
+ *
3612
+ * @returns Promise that resolves with the confirmed tier ('free', 'standard', or 'pro')
3613
+ *
3614
+ * @example
3615
+ * ```typescript
3616
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3617
+ *
3618
+ * // Wait for tier to be confirmed
3619
+ * const tier = await staticTV.onFeaturesReady()
3620
+ *
3621
+ * // Now modules are guaranteed to be initialized (or confirmed unavailable)
3622
+ * if (staticTV.guideUI) {
3623
+ * await staticTV.guideUI.init()
3624
+ * console.log('Guide ready!')
3625
+ * }
3626
+ *
3627
+ * if (tier === 'free') {
3628
+ * console.log('Upgrade at thestatic.tv for Guide & Chat!')
3629
+ * }
3630
+ * ```
3631
+ */
3632
+ async onFeaturesReady() {
3633
+ return this._featuresReadyPromise;
3634
+ }
3598
3635
  /**
3599
3636
  * Make an authenticated API request
3600
3637
  * @internal
@@ -3652,17 +3689,7 @@ var StaticTVClient = class {
3652
3689
  if (screen !== void 0) {
3653
3690
  this.log(`Playing video: ${url}`);
3654
3691
  this._currentVideoUrl = url;
3655
- if (VideoPlayer.has(screen)) {
3656
- VideoPlayer.deleteFrom(screen);
3657
- }
3658
- VideoPlayer.create(screen, {
3659
- src: url,
3660
- playing: true,
3661
- volume: 1
3662
- });
3663
- Material.setBasicMaterial(screen, {
3664
- texture: Material.Texture.Video({ videoPlayerEntity: screen })
3665
- });
3692
+ this._playVideoInternal(url, false);
3666
3693
  if (this.guideUI) {
3667
3694
  const videos = this.guideUI.getVideos();
3668
3695
  const video = videos.find((v) => v.src === url);
@@ -3677,22 +3704,55 @@ var StaticTVClient = class {
3677
3704
  }
3678
3705
  /**
3679
3706
  * Stop video playback on the configured videoScreen entity.
3707
+ * If fallbackVideoUrl is configured (default), plays the fallback loop.
3680
3708
  * Called by Admin Panel.
3681
3709
  */
3682
3710
  stopVideo() {
3683
3711
  const screen = this.config.videoScreen;
3684
- if (screen !== void 0 && VideoPlayer.has(screen)) {
3685
- this.log("Stopping video");
3686
- VideoPlayer.getMutable(screen).playing = false;
3687
- this._currentVideoUrl = "";
3712
+ if (screen !== void 0) {
3688
3713
  if (this.guideUI) {
3689
3714
  this.guideUI.currentVideoId = null;
3690
3715
  }
3716
+ const fallbackUrl = this.config.fallbackVideoUrl;
3717
+ const fallbackDisabled = fallbackUrl === "";
3718
+ if (fallbackDisabled) {
3719
+ if (VideoPlayer.has(screen)) {
3720
+ this.log("Stopping video (no fallback)");
3721
+ VideoPlayer.getMutable(screen).playing = false;
3722
+ }
3723
+ this._currentVideoUrl = "";
3724
+ } else {
3725
+ const url = fallbackUrl || DEFAULT_FALLBACK_VIDEO;
3726
+ this.log(`Playing fallback: ${url}`);
3727
+ this._currentVideoUrl = "";
3728
+ this._playVideoInternal(url, true);
3729
+ }
3691
3730
  }
3692
3731
  if (this.config.onVideoStop) {
3693
3732
  this.config.onVideoStop();
3694
3733
  }
3695
3734
  }
3735
+ /**
3736
+ * Internal video player - handles both regular videos and fallback
3737
+ * @internal
3738
+ */
3739
+ _playVideoInternal(url, isFallback = false) {
3740
+ const screen = this.config.videoScreen;
3741
+ if (screen === void 0) return;
3742
+ if (VideoPlayer.has(screen)) {
3743
+ VideoPlayer.deleteFrom(screen);
3744
+ }
3745
+ VideoPlayer.create(screen, {
3746
+ src: url,
3747
+ playing: true,
3748
+ loop: isFallback,
3749
+ // Loop fallback videos
3750
+ volume: 1
3751
+ });
3752
+ Material.setBasicMaterial(screen, {
3753
+ texture: Material.Texture.Video({ videoPlayerEntity: screen })
3754
+ });
3755
+ }
3696
3756
  /**
3697
3757
  * Get the currently playing video URL
3698
3758
  */
@@ -3775,6 +3835,16 @@ var StaticTVClient = class {
3775
3835
  });
3776
3836
  this.log(`Pro features enabled (admin panel) - sceneId: ${sceneId}`);
3777
3837
  }
3838
+ /**
3839
+ * Resolve the features ready promise (only once)
3840
+ * @internal
3841
+ */
3842
+ _resolveFeaturesReady(tier) {
3843
+ if (!this._featuresResolved) {
3844
+ this._featuresResolved = true;
3845
+ this._featuresReadyResolve(tier);
3846
+ }
3847
+ }
3778
3848
  /**
3779
3849
  * Called by SessionModule when server returns the tier
3780
3850
  * Enables modules based on tier level
@@ -3796,6 +3866,7 @@ var StaticTVClient = class {
3796
3866
  this.log("Pro tier detected but no video config - call enableProFeatures() or set videoScreen to enable admin panel");
3797
3867
  }
3798
3868
  }
3869
+ this._resolveFeaturesReady(tier);
3799
3870
  }
3800
3871
  /**
3801
3872
  * @deprecated Use `_enableFeaturesForTier` instead
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thestatic-tv/dcl-sdk",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Connect your Decentraland scene to thestatic.tv - full channel lineup, metrics tracking, and interactions",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",