@thestatic-tv/dcl-sdk 2.3.0 → 2.4.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
@@ -863,6 +863,9 @@ declare class StaticTVClient {
863
863
  private _standardFeaturesEnabled;
864
864
  private _proFeaturesEnabled;
865
865
  private _pendingProConfig;
866
+ private _featuresReadyPromise;
867
+ private _featuresReadyResolve;
868
+ private _featuresResolved;
866
869
  /** Guide module - fetch channel lineup (standard/pro tier) */
867
870
  guide: GuideModule | null;
868
871
  /** Session module - track visitor sessions (all tiers, null when disabled) */
@@ -933,6 +936,33 @@ declare class StaticTVClient {
933
936
  * @deprecated Use `isFree` instead. Kept for backward compatibility.
934
937
  */
935
938
  get isLite(): boolean;
939
+ /**
940
+ * Wait for tier confirmation from the server.
941
+ * Resolves when the SDK knows which features are available.
942
+ *
943
+ * Use this instead of polling `isLite` or `isFree` in a while loop.
944
+ *
945
+ * @returns Promise that resolves with the confirmed tier ('free', 'standard', or 'pro')
946
+ *
947
+ * @example
948
+ * ```typescript
949
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
950
+ *
951
+ * // Wait for tier to be confirmed
952
+ * const tier = await staticTV.onFeaturesReady()
953
+ *
954
+ * // Now modules are guaranteed to be initialized (or confirmed unavailable)
955
+ * if (staticTV.guideUI) {
956
+ * await staticTV.guideUI.init()
957
+ * console.log('Guide ready!')
958
+ * }
959
+ *
960
+ * if (tier === 'free') {
961
+ * console.log('Upgrade at thestatic.tv for Guide & Chat!')
962
+ * }
963
+ * ```
964
+ */
965
+ onFeaturesReady(): Promise<SDKTier>;
936
966
  /**
937
967
  * Make an authenticated API request
938
968
  * @internal
@@ -995,6 +1025,11 @@ declare class StaticTVClient {
995
1025
  * @internal
996
1026
  */
997
1027
  private _initProModules;
1028
+ /**
1029
+ * Resolve the features ready promise (only once)
1030
+ * @internal
1031
+ */
1032
+ private _resolveFeaturesReady;
998
1033
  /**
999
1034
  * Called by SessionModule when server returns the tier
1000
1035
  * Enables modules based on tier level
package/dist/index.d.ts CHANGED
@@ -863,6 +863,9 @@ declare class StaticTVClient {
863
863
  private _standardFeaturesEnabled;
864
864
  private _proFeaturesEnabled;
865
865
  private _pendingProConfig;
866
+ private _featuresReadyPromise;
867
+ private _featuresReadyResolve;
868
+ private _featuresResolved;
866
869
  /** Guide module - fetch channel lineup (standard/pro tier) */
867
870
  guide: GuideModule | null;
868
871
  /** Session module - track visitor sessions (all tiers, null when disabled) */
@@ -933,6 +936,33 @@ declare class StaticTVClient {
933
936
  * @deprecated Use `isFree` instead. Kept for backward compatibility.
934
937
  */
935
938
  get isLite(): boolean;
939
+ /**
940
+ * Wait for tier confirmation from the server.
941
+ * Resolves when the SDK knows which features are available.
942
+ *
943
+ * Use this instead of polling `isLite` or `isFree` in a while loop.
944
+ *
945
+ * @returns Promise that resolves with the confirmed tier ('free', 'standard', or 'pro')
946
+ *
947
+ * @example
948
+ * ```typescript
949
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
950
+ *
951
+ * // Wait for tier to be confirmed
952
+ * const tier = await staticTV.onFeaturesReady()
953
+ *
954
+ * // Now modules are guaranteed to be initialized (or confirmed unavailable)
955
+ * if (staticTV.guideUI) {
956
+ * await staticTV.guideUI.init()
957
+ * console.log('Guide ready!')
958
+ * }
959
+ *
960
+ * if (tier === 'free') {
961
+ * console.log('Upgrade at thestatic.tv for Guide & Chat!')
962
+ * }
963
+ * ```
964
+ */
965
+ onFeaturesReady(): Promise<SDKTier>;
936
966
  /**
937
967
  * Make an authenticated API request
938
968
  * @internal
@@ -995,6 +1025,11 @@ declare class StaticTVClient {
995
1025
  * @internal
996
1026
  */
997
1027
  private _initProModules;
1028
+ /**
1029
+ * Resolve the features ready promise (only once)
1030
+ * @internal
1031
+ */
1032
+ private _resolveFeaturesReady;
998
1033
  /**
999
1034
  * Called by SessionModule when server returns the tier
1000
1035
  * 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}` })
@@ -3535,6 +3535,7 @@ var StaticTVClient = class {
3535
3535
  this._standardFeaturesEnabled = false;
3536
3536
  this._proFeaturesEnabled = false;
3537
3537
  this._pendingProConfig = null;
3538
+ this._featuresResolved = false;
3538
3539
  /** Guide module - fetch channel lineup (standard/pro tier) */
3539
3540
  this.guide = null;
3540
3541
  /** Session module - track visitor sessions (all tiers, null when disabled) */
@@ -3555,6 +3556,9 @@ var StaticTVClient = class {
3555
3556
  // --- VIDEO PLAYBACK (Internal handler for videoScreen) ---
3556
3557
  // =============================================================================
3557
3558
  this._currentVideoUrl = "";
3559
+ this._featuresReadyPromise = new Promise((resolve) => {
3560
+ this._featuresReadyResolve = resolve;
3561
+ });
3558
3562
  this.config = {
3559
3563
  autoStartSession: true,
3560
3564
  sessionHeartbeatInterval: 3e4,
@@ -3572,6 +3576,7 @@ var StaticTVClient = class {
3572
3576
  this.interactions = null;
3573
3577
  this.guideUI = null;
3574
3578
  this.chatUI = null;
3579
+ this._resolveFeaturesReady("free");
3575
3580
  return;
3576
3581
  }
3577
3582
  if (config.apiKey.startsWith("dclk_")) {
@@ -3582,12 +3587,14 @@ var StaticTVClient = class {
3582
3587
  console.warn("[TheStatic] Invalid API key format - get your key at thestatic.tv/dashboard");
3583
3588
  this._disabled = true;
3584
3589
  this._keyType = null;
3590
+ this._resolveFeaturesReady("free");
3585
3591
  return;
3586
3592
  }
3587
3593
  this.session = new SessionModule(this);
3588
3594
  if (this._keyType === KEY_TYPE_CHANNEL) {
3589
3595
  this._tier = "standard";
3590
3596
  this._initStandardModules();
3597
+ this._resolveFeaturesReady("standard");
3591
3598
  }
3592
3599
  if (this.config.autoStartSession) {
3593
3600
  fetchUserData().then(() => {
@@ -3638,6 +3645,35 @@ var StaticTVClient = class {
3638
3645
  get isLite() {
3639
3646
  return this.isFree;
3640
3647
  }
3648
+ /**
3649
+ * Wait for tier confirmation from the server.
3650
+ * Resolves when the SDK knows which features are available.
3651
+ *
3652
+ * Use this instead of polling `isLite` or `isFree` in a while loop.
3653
+ *
3654
+ * @returns Promise that resolves with the confirmed tier ('free', 'standard', or 'pro')
3655
+ *
3656
+ * @example
3657
+ * ```typescript
3658
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3659
+ *
3660
+ * // Wait for tier to be confirmed
3661
+ * const tier = await staticTV.onFeaturesReady()
3662
+ *
3663
+ * // Now modules are guaranteed to be initialized (or confirmed unavailable)
3664
+ * if (staticTV.guideUI) {
3665
+ * await staticTV.guideUI.init()
3666
+ * console.log('Guide ready!')
3667
+ * }
3668
+ *
3669
+ * if (tier === 'free') {
3670
+ * console.log('Upgrade at thestatic.tv for Guide & Chat!')
3671
+ * }
3672
+ * ```
3673
+ */
3674
+ async onFeaturesReady() {
3675
+ return this._featuresReadyPromise;
3676
+ }
3641
3677
  /**
3642
3678
  * Make an authenticated API request
3643
3679
  * @internal
@@ -3818,6 +3854,16 @@ var StaticTVClient = class {
3818
3854
  });
3819
3855
  this.log(`Pro features enabled (admin panel) - sceneId: ${sceneId}`);
3820
3856
  }
3857
+ /**
3858
+ * Resolve the features ready promise (only once)
3859
+ * @internal
3860
+ */
3861
+ _resolveFeaturesReady(tier) {
3862
+ if (!this._featuresResolved) {
3863
+ this._featuresResolved = true;
3864
+ this._featuresReadyResolve(tier);
3865
+ }
3866
+ }
3821
3867
  /**
3822
3868
  * Called by SessionModule when server returns the tier
3823
3869
  * Enables modules based on tier level
@@ -3839,6 +3885,7 @@ var StaticTVClient = class {
3839
3885
  this.log("Pro tier detected but no video config - call enableProFeatures() or set videoScreen to enable admin panel");
3840
3886
  }
3841
3887
  }
3888
+ this._resolveFeaturesReady(tier);
3842
3889
  }
3843
3890
  /**
3844
3891
  * @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}` })
@@ -3492,6 +3492,7 @@ var StaticTVClient = class {
3492
3492
  this._standardFeaturesEnabled = false;
3493
3493
  this._proFeaturesEnabled = false;
3494
3494
  this._pendingProConfig = null;
3495
+ this._featuresResolved = false;
3495
3496
  /** Guide module - fetch channel lineup (standard/pro tier) */
3496
3497
  this.guide = null;
3497
3498
  /** Session module - track visitor sessions (all tiers, null when disabled) */
@@ -3512,6 +3513,9 @@ var StaticTVClient = class {
3512
3513
  // --- VIDEO PLAYBACK (Internal handler for videoScreen) ---
3513
3514
  // =============================================================================
3514
3515
  this._currentVideoUrl = "";
3516
+ this._featuresReadyPromise = new Promise((resolve) => {
3517
+ this._featuresReadyResolve = resolve;
3518
+ });
3515
3519
  this.config = {
3516
3520
  autoStartSession: true,
3517
3521
  sessionHeartbeatInterval: 3e4,
@@ -3529,6 +3533,7 @@ var StaticTVClient = class {
3529
3533
  this.interactions = null;
3530
3534
  this.guideUI = null;
3531
3535
  this.chatUI = null;
3536
+ this._resolveFeaturesReady("free");
3532
3537
  return;
3533
3538
  }
3534
3539
  if (config.apiKey.startsWith("dclk_")) {
@@ -3539,12 +3544,14 @@ var StaticTVClient = class {
3539
3544
  console.warn("[TheStatic] Invalid API key format - get your key at thestatic.tv/dashboard");
3540
3545
  this._disabled = true;
3541
3546
  this._keyType = null;
3547
+ this._resolveFeaturesReady("free");
3542
3548
  return;
3543
3549
  }
3544
3550
  this.session = new SessionModule(this);
3545
3551
  if (this._keyType === KEY_TYPE_CHANNEL) {
3546
3552
  this._tier = "standard";
3547
3553
  this._initStandardModules();
3554
+ this._resolveFeaturesReady("standard");
3548
3555
  }
3549
3556
  if (this.config.autoStartSession) {
3550
3557
  fetchUserData().then(() => {
@@ -3595,6 +3602,35 @@ var StaticTVClient = class {
3595
3602
  get isLite() {
3596
3603
  return this.isFree;
3597
3604
  }
3605
+ /**
3606
+ * Wait for tier confirmation from the server.
3607
+ * Resolves when the SDK knows which features are available.
3608
+ *
3609
+ * Use this instead of polling `isLite` or `isFree` in a while loop.
3610
+ *
3611
+ * @returns Promise that resolves with the confirmed tier ('free', 'standard', or 'pro')
3612
+ *
3613
+ * @example
3614
+ * ```typescript
3615
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3616
+ *
3617
+ * // Wait for tier to be confirmed
3618
+ * const tier = await staticTV.onFeaturesReady()
3619
+ *
3620
+ * // Now modules are guaranteed to be initialized (or confirmed unavailable)
3621
+ * if (staticTV.guideUI) {
3622
+ * await staticTV.guideUI.init()
3623
+ * console.log('Guide ready!')
3624
+ * }
3625
+ *
3626
+ * if (tier === 'free') {
3627
+ * console.log('Upgrade at thestatic.tv for Guide & Chat!')
3628
+ * }
3629
+ * ```
3630
+ */
3631
+ async onFeaturesReady() {
3632
+ return this._featuresReadyPromise;
3633
+ }
3598
3634
  /**
3599
3635
  * Make an authenticated API request
3600
3636
  * @internal
@@ -3775,6 +3811,16 @@ var StaticTVClient = class {
3775
3811
  });
3776
3812
  this.log(`Pro features enabled (admin panel) - sceneId: ${sceneId}`);
3777
3813
  }
3814
+ /**
3815
+ * Resolve the features ready promise (only once)
3816
+ * @internal
3817
+ */
3818
+ _resolveFeaturesReady(tier) {
3819
+ if (!this._featuresResolved) {
3820
+ this._featuresResolved = true;
3821
+ this._featuresReadyResolve(tier);
3822
+ }
3823
+ }
3778
3824
  /**
3779
3825
  * Called by SessionModule when server returns the tier
3780
3826
  * Enables modules based on tier level
@@ -3796,6 +3842,7 @@ var StaticTVClient = class {
3796
3842
  this.log("Pro tier detected but no video config - call enableProFeatures() or set videoScreen to enable admin panel");
3797
3843
  }
3798
3844
  }
3845
+ this._resolveFeaturesReady(tier);
3799
3846
  }
3800
3847
  /**
3801
3848
  * @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.4.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",