@shortkitsdk/react-native 0.1.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.
Files changed (35) hide show
  1. package/ShortKitReactNative.podspec +19 -0
  2. package/android/build.gradle.kts +34 -0
  3. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +249 -0
  4. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +32 -0
  5. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +769 -0
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +101 -0
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitPackage.kt +40 -0
  8. package/app.plugin.js +1 -0
  9. package/ios/ShortKitBridge.swift +537 -0
  10. package/ios/ShortKitFeedView.swift +207 -0
  11. package/ios/ShortKitFeedViewManager.mm +29 -0
  12. package/ios/ShortKitModule.h +25 -0
  13. package/ios/ShortKitModule.mm +204 -0
  14. package/ios/ShortKitOverlayBridge.swift +91 -0
  15. package/ios/ShortKitReactNative-Bridging-Header.h +3 -0
  16. package/ios/ShortKitReactNative.podspec +19 -0
  17. package/package.json +50 -0
  18. package/plugin/build/index.d.ts +3 -0
  19. package/plugin/build/index.js +13 -0
  20. package/plugin/build/withShortKitAndroid.d.ts +8 -0
  21. package/plugin/build/withShortKitAndroid.js +32 -0
  22. package/plugin/build/withShortKitIOS.d.ts +8 -0
  23. package/plugin/build/withShortKitIOS.js +29 -0
  24. package/react-native.config.js +8 -0
  25. package/src/OverlayManager.tsx +87 -0
  26. package/src/ShortKitContext.ts +51 -0
  27. package/src/ShortKitFeed.tsx +203 -0
  28. package/src/ShortKitProvider.tsx +526 -0
  29. package/src/index.ts +26 -0
  30. package/src/serialization.ts +95 -0
  31. package/src/specs/NativeShortKitModule.ts +201 -0
  32. package/src/specs/ShortKitFeedViewNativeComponent.ts +13 -0
  33. package/src/types.ts +167 -0
  34. package/src/useShortKit.ts +20 -0
  35. package/src/useShortKitPlayer.ts +29 -0
@@ -0,0 +1,526 @@
1
+ import React, { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
2
+ import { AppState } from 'react-native';
3
+ import type { AppStateStatus } from 'react-native';
4
+ import { ShortKitContext } from './ShortKitContext';
5
+ import type { ShortKitContextValue } from './ShortKitContext';
6
+ import type {
7
+ ShortKitProviderProps,
8
+ ContentItem,
9
+ PlayerTime,
10
+ PlayerState,
11
+ CaptionTrack,
12
+ ContentSignal,
13
+ } from './types';
14
+ import {
15
+ serializeFeedConfigForModule,
16
+ deserializePlayerState,
17
+ deserializeContentItem,
18
+ deserializePlayerTime,
19
+ } from './serialization';
20
+ import NativeShortKitModule from './specs/NativeShortKitModule';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // State & Reducer
24
+ // ---------------------------------------------------------------------------
25
+
26
+ interface State {
27
+ playerState: PlayerState;
28
+ currentItem: ContentItem | null;
29
+ nextItem: ContentItem | null;
30
+ time: PlayerTime;
31
+ isMuted: boolean;
32
+ playbackRate: number;
33
+ captionsEnabled: boolean;
34
+ activeCaptionTrack: CaptionTrack | null;
35
+ activeCue: { text: string; startTime: number; endTime: number } | null;
36
+ prefetchedAheadCount: number;
37
+ isActive: boolean;
38
+ isTransitioning: boolean;
39
+ lastOverlayTap: number;
40
+ lastOverlayDoubleTap: { x: number; y: number; id: number } | null;
41
+ }
42
+
43
+ const initialState: State = {
44
+ playerState: 'idle',
45
+ currentItem: null,
46
+ nextItem: null,
47
+ time: { current: 0, duration: 0, buffered: 0 },
48
+ isMuted: true,
49
+ playbackRate: 1.0,
50
+ captionsEnabled: false,
51
+ activeCaptionTrack: null,
52
+ activeCue: null,
53
+ prefetchedAheadCount: 0,
54
+ isActive: false,
55
+ isTransitioning: false,
56
+ lastOverlayTap: 0,
57
+ lastOverlayDoubleTap: null,
58
+ };
59
+
60
+ type Action =
61
+ | { type: 'PLAYER_STATE'; payload: PlayerState }
62
+ | { type: 'CURRENT_ITEM'; payload: ContentItem | null }
63
+ | { type: 'TIME'; payload: PlayerTime }
64
+ | { type: 'MUTED'; payload: boolean }
65
+ | { type: 'PLAYBACK_RATE'; payload: number }
66
+ | { type: 'CAPTIONS_ENABLED'; payload: boolean }
67
+ | { type: 'CAPTION_TRACK'; payload: CaptionTrack | null }
68
+ | { type: 'CUE'; payload: { text: string; startTime: number; endTime: number } | null }
69
+ | { type: 'PREFETCH_COUNT'; payload: number }
70
+ | { type: 'OVERLAY_CONFIGURE'; payload: ContentItem }
71
+ | { type: 'OVERLAY_ACTIVATE' }
72
+ | { type: 'OVERLAY_RESET' }
73
+ | { type: 'OVERLAY_FADE_OUT' }
74
+ | { type: 'OVERLAY_RESTORE' }
75
+ | { type: 'OVERLAY_TAP' }
76
+ | { type: 'OVERLAY_DOUBLE_TAP'; payload: { x: number; y: number } };
77
+
78
+ function reducer(state: State, action: Action): State {
79
+ switch (action.type) {
80
+ case 'PLAYER_STATE': {
81
+ // Only allow isActive to transition TO true from player state.
82
+ // Once active, the overlay stays mounted — hiding during transient
83
+ // states like "loading" between videos would cause a visible flash.
84
+ const isPlaybackActive =
85
+ action.payload === 'playing' ||
86
+ action.payload === 'paused' ||
87
+ action.payload === 'buffering' ||
88
+ action.payload === 'seeking';
89
+ return {
90
+ ...state,
91
+ playerState: action.payload,
92
+ isActive: state.isActive || isPlaybackActive,
93
+ };
94
+ }
95
+ case 'CURRENT_ITEM':
96
+ return { ...state, currentItem: action.payload };
97
+ case 'TIME':
98
+ return { ...state, time: action.payload };
99
+ case 'MUTED':
100
+ return { ...state, isMuted: action.payload };
101
+ case 'PLAYBACK_RATE':
102
+ return { ...state, playbackRate: action.payload };
103
+ case 'CAPTIONS_ENABLED':
104
+ return { ...state, captionsEnabled: action.payload };
105
+ case 'CAPTION_TRACK':
106
+ return { ...state, activeCaptionTrack: action.payload };
107
+ case 'CUE':
108
+ return { ...state, activeCue: action.payload };
109
+ case 'PREFETCH_COUNT':
110
+ return { ...state, prefetchedAheadCount: action.payload };
111
+ case 'OVERLAY_CONFIGURE':
112
+ return { ...state, nextItem: action.payload };
113
+ case 'OVERLAY_ACTIVATE':
114
+ return { ...state, currentItem: state.nextItem, isActive: true };
115
+ case 'OVERLAY_RESET':
116
+ // Don't set isActive = false — the overlay stays mounted during
117
+ // transitions. In the native SDK each cell has its own overlay
118
+ // instance, so there's no gap. We replicate this by keeping the
119
+ // single JS overlay mounted and updating its content on activate.
120
+ return { ...state, isTransitioning: false };
121
+ case 'OVERLAY_FADE_OUT':
122
+ return { ...state, isTransitioning: true };
123
+ case 'OVERLAY_RESTORE':
124
+ return { ...state, isTransitioning: false };
125
+ case 'OVERLAY_TAP':
126
+ return { ...state, lastOverlayTap: state.lastOverlayTap + 1 };
127
+ case 'OVERLAY_DOUBLE_TAP':
128
+ return {
129
+ ...state,
130
+ lastOverlayDoubleTap: {
131
+ x: action.payload.x,
132
+ y: action.payload.y,
133
+ id: (state.lastOverlayDoubleTap?.id ?? 0) + 1,
134
+ },
135
+ };
136
+ default:
137
+ return state;
138
+ }
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Provider Component
143
+ // ---------------------------------------------------------------------------
144
+
145
+ export function ShortKitProvider({
146
+ apiKey,
147
+ config,
148
+ userId,
149
+ clientAppName,
150
+ clientAppVersion,
151
+ customDimensions,
152
+ children,
153
+ }: ShortKitProviderProps): React.JSX.Element {
154
+ const [state, dispatch] = useReducer(reducer, initialState);
155
+ const appStateRef = useRef<AppStateStatus>(AppState.currentState);
156
+
157
+ // -----------------------------------------------------------------------
158
+ // Initialize / Release
159
+ // -----------------------------------------------------------------------
160
+ useEffect(() => {
161
+ if (!NativeShortKitModule) return;
162
+
163
+ const serializedConfig = serializeFeedConfigForModule(config);
164
+ const serializedDimensions = customDimensions
165
+ ? JSON.stringify(customDimensions)
166
+ : undefined;
167
+
168
+ NativeShortKitModule.initialize(
169
+ apiKey,
170
+ serializedConfig,
171
+ clientAppName,
172
+ clientAppVersion,
173
+ serializedDimensions,
174
+ );
175
+
176
+ return () => {
177
+ NativeShortKitModule.destroy();
178
+ };
179
+ // Intentionally only run on mount/unmount. Config changes require a
180
+ // remount (via a `key` prop on the provider) to take effect.
181
+ // eslint-disable-next-line react-hooks/exhaustive-deps
182
+ }, []);
183
+
184
+ // -----------------------------------------------------------------------
185
+ // User ID sync
186
+ // -----------------------------------------------------------------------
187
+ useEffect(() => {
188
+ if (!NativeShortKitModule) return;
189
+ if (userId) {
190
+ NativeShortKitModule.setUserId(userId);
191
+ } else {
192
+ NativeShortKitModule.clearUserId();
193
+ }
194
+ }, [userId]);
195
+
196
+ // -----------------------------------------------------------------------
197
+ // AppState handling (background / foreground)
198
+ // -----------------------------------------------------------------------
199
+ useEffect(() => {
200
+ if (!NativeShortKitModule) return;
201
+
202
+ const subscription = AppState.addEventListener(
203
+ 'change',
204
+ (nextState: AppStateStatus) => {
205
+ if (
206
+ appStateRef.current.match(/active/) &&
207
+ nextState === 'background'
208
+ ) {
209
+ NativeShortKitModule.onPause();
210
+ } else if (
211
+ appStateRef.current.match(/background/) &&
212
+ nextState === 'active'
213
+ ) {
214
+ NativeShortKitModule.onResume();
215
+ }
216
+ appStateRef.current = nextState;
217
+ },
218
+ );
219
+
220
+ return () => {
221
+ subscription.remove();
222
+ };
223
+ }, []);
224
+
225
+ // -----------------------------------------------------------------------
226
+ // Event subscriptions
227
+ // -----------------------------------------------------------------------
228
+ useEffect(() => {
229
+ if (!NativeShortKitModule) return;
230
+
231
+ const subscriptions: Array<{ remove: () => void }> = [];
232
+
233
+ // Player state
234
+ subscriptions.push(
235
+ NativeShortKitModule.onPlayerStateChanged((event) => {
236
+ dispatch({
237
+ type: 'PLAYER_STATE',
238
+ payload: deserializePlayerState(event),
239
+ });
240
+ }),
241
+ );
242
+
243
+ // Current item
244
+ subscriptions.push(
245
+ NativeShortKitModule.onCurrentItemChanged((event) => {
246
+ const item: ContentItem = {
247
+ id: event.id,
248
+ title: event.title,
249
+ description: event.description,
250
+ duration: event.duration,
251
+ streamingUrl: event.streamingUrl,
252
+ thumbnailUrl: event.thumbnailUrl,
253
+ captionTracks: event.captionTracks
254
+ ? JSON.parse(event.captionTracks)
255
+ : [],
256
+ customMetadata: event.customMetadata
257
+ ? JSON.parse(event.customMetadata)
258
+ : undefined,
259
+ author: event.author,
260
+ articleUrl: event.articleUrl,
261
+ commentCount: event.commentCount,
262
+ };
263
+ dispatch({ type: 'CURRENT_ITEM', payload: item });
264
+ }),
265
+ );
266
+
267
+ // Time updates
268
+ subscriptions.push(
269
+ NativeShortKitModule.onTimeUpdate((event) => {
270
+ dispatch({ type: 'TIME', payload: deserializePlayerTime(event) });
271
+ }),
272
+ );
273
+
274
+ // Muted
275
+ subscriptions.push(
276
+ NativeShortKitModule.onMutedChanged((event) => {
277
+ dispatch({ type: 'MUTED', payload: event.isMuted });
278
+ }),
279
+ );
280
+
281
+ // Playback rate
282
+ subscriptions.push(
283
+ NativeShortKitModule.onPlaybackRateChanged((event) => {
284
+ dispatch({ type: 'PLAYBACK_RATE', payload: event.rate });
285
+ }),
286
+ );
287
+
288
+ // Captions enabled
289
+ subscriptions.push(
290
+ NativeShortKitModule.onCaptionsEnabledChanged((event) => {
291
+ dispatch({ type: 'CAPTIONS_ENABLED', payload: event.enabled });
292
+ }),
293
+ );
294
+
295
+ // Active caption track
296
+ subscriptions.push(
297
+ NativeShortKitModule.onActiveCaptionTrackChanged((event) => {
298
+ dispatch({
299
+ type: 'CAPTION_TRACK',
300
+ payload: {
301
+ language: event.language,
302
+ label: event.label,
303
+ sourceUrl: event.sourceUrl,
304
+ },
305
+ });
306
+ }),
307
+ );
308
+
309
+ // Active cue
310
+ subscriptions.push(
311
+ NativeShortKitModule.onActiveCueChanged((event) => {
312
+ dispatch({
313
+ type: 'CUE',
314
+ payload: {
315
+ text: event.text,
316
+ startTime: event.startTime,
317
+ endTime: event.endTime,
318
+ },
319
+ });
320
+ }),
321
+ );
322
+
323
+ // Prefetch count
324
+ subscriptions.push(
325
+ NativeShortKitModule.onPrefetchedAheadCountChanged((event) => {
326
+ dispatch({ type: 'PREFETCH_COUNT', payload: event.count });
327
+ }),
328
+ );
329
+
330
+ // Overlay lifecycle events
331
+ subscriptions.push(
332
+ NativeShortKitModule.onOverlayConfigure((event) => {
333
+ const item = deserializeContentItem(event.item);
334
+ if (item) {
335
+ dispatch({ type: 'OVERLAY_CONFIGURE', payload: item });
336
+ }
337
+ }),
338
+ );
339
+
340
+ subscriptions.push(
341
+ NativeShortKitModule.onOverlayActivate((_event) => {
342
+ dispatch({ type: 'OVERLAY_ACTIVATE' });
343
+ }),
344
+ );
345
+
346
+ subscriptions.push(
347
+ NativeShortKitModule.onOverlayReset((_event) => {
348
+ dispatch({ type: 'OVERLAY_RESET' });
349
+ }),
350
+ );
351
+
352
+ subscriptions.push(
353
+ NativeShortKitModule.onOverlayFadeOut((_event) => {
354
+ dispatch({ type: 'OVERLAY_FADE_OUT' });
355
+ }),
356
+ );
357
+
358
+ subscriptions.push(
359
+ NativeShortKitModule.onOverlayRestore((_event) => {
360
+ dispatch({ type: 'OVERLAY_RESTORE' });
361
+ }),
362
+ );
363
+
364
+ // Overlay tap events
365
+ subscriptions.push(
366
+ NativeShortKitModule.onOverlayTap((_event) => {
367
+ dispatch({ type: 'OVERLAY_TAP' });
368
+ }),
369
+ );
370
+
371
+ subscriptions.push(
372
+ NativeShortKitModule.onOverlayDoubleTap((event) => {
373
+ dispatch({
374
+ type: 'OVERLAY_DOUBLE_TAP',
375
+ payload: { x: event.x, y: event.y },
376
+ });
377
+ }),
378
+ );
379
+
380
+ // Note: Feed-level callback events (onDidLoop, onFeedTransition,
381
+ // onShareTapped, etc.) are subscribed by the ShortKitFeed component
382
+ // (Task 11), not here. The provider only manages state-driving events.
383
+
384
+ return () => {
385
+ for (const sub of subscriptions) {
386
+ sub.remove();
387
+ }
388
+ };
389
+ }, []);
390
+
391
+ // -----------------------------------------------------------------------
392
+ // Commands (stable refs)
393
+ // -----------------------------------------------------------------------
394
+ const play = useCallback(() => {
395
+ NativeShortKitModule?.play();
396
+ }, []);
397
+
398
+ const pause = useCallback(() => {
399
+ NativeShortKitModule?.pause();
400
+ }, []);
401
+
402
+ const seek = useCallback((seconds: number) => {
403
+ NativeShortKitModule?.seek(seconds);
404
+ }, []);
405
+
406
+ const seekAndPlay = useCallback((seconds: number) => {
407
+ NativeShortKitModule?.seekAndPlay(seconds);
408
+ }, []);
409
+
410
+ const skipToNext = useCallback(() => {
411
+ NativeShortKitModule?.skipToNext();
412
+ }, []);
413
+
414
+ const skipToPrevious = useCallback(() => {
415
+ NativeShortKitModule?.skipToPrevious();
416
+ }, []);
417
+
418
+ const setMuted = useCallback((muted: boolean) => {
419
+ NativeShortKitModule?.setMuted(muted);
420
+ }, []);
421
+
422
+ const setPlaybackRate = useCallback((rate: number) => {
423
+ NativeShortKitModule?.setPlaybackRate(rate);
424
+ }, []);
425
+
426
+ const setCaptionsEnabled = useCallback((enabled: boolean) => {
427
+ NativeShortKitModule?.setCaptionsEnabled(enabled);
428
+ }, []);
429
+
430
+ const selectCaptionTrack = useCallback((language: string) => {
431
+ NativeShortKitModule?.selectCaptionTrack(language);
432
+ }, []);
433
+
434
+ const sendContentSignal = useCallback((signal: ContentSignal) => {
435
+ NativeShortKitModule?.sendContentSignal(signal);
436
+ }, []);
437
+
438
+ const setMaxBitrate = useCallback((bitrate: number) => {
439
+ NativeShortKitModule?.setMaxBitrate(bitrate);
440
+ }, []);
441
+
442
+ const setUserIdCmd = useCallback((id: string) => {
443
+ NativeShortKitModule?.setUserId(id);
444
+ }, []);
445
+
446
+ const clearUserIdCmd = useCallback(() => {
447
+ NativeShortKitModule?.clearUserId();
448
+ }, []);
449
+
450
+ // -----------------------------------------------------------------------
451
+ // Context value (memoized to avoid unnecessary re-renders)
452
+ // -----------------------------------------------------------------------
453
+ const value: ShortKitContextValue = useMemo(
454
+ () => ({
455
+ // State
456
+ playerState: state.playerState,
457
+ currentItem: state.currentItem,
458
+ nextItem: state.nextItem,
459
+ time: state.time,
460
+ isMuted: state.isMuted,
461
+ playbackRate: state.playbackRate,
462
+ captionsEnabled: state.captionsEnabled,
463
+ activeCaptionTrack: state.activeCaptionTrack,
464
+ activeCue: state.activeCue,
465
+ prefetchedAheadCount: state.prefetchedAheadCount,
466
+ isActive: state.isActive,
467
+ isTransitioning: state.isTransitioning,
468
+ lastOverlayTap: state.lastOverlayTap,
469
+ lastOverlayDoubleTap: state.lastOverlayDoubleTap,
470
+
471
+ // Commands
472
+ play,
473
+ pause,
474
+ seek,
475
+ seekAndPlay,
476
+ skipToNext,
477
+ skipToPrevious,
478
+ setMuted,
479
+ setPlaybackRate,
480
+ setCaptionsEnabled,
481
+ selectCaptionTrack,
482
+ sendContentSignal,
483
+ setMaxBitrate,
484
+ setUserId: setUserIdCmd,
485
+ clearUserId: clearUserIdCmd,
486
+
487
+ // Internal — consumed by ShortKitFeed to pass to OverlayManager
488
+ _overlayConfig: config.overlay ?? 'none',
489
+ }),
490
+ [
491
+ state.playerState,
492
+ state.currentItem,
493
+ state.nextItem,
494
+ state.time,
495
+ state.isMuted,
496
+ state.playbackRate,
497
+ state.captionsEnabled,
498
+ state.activeCaptionTrack,
499
+ state.activeCue,
500
+ state.prefetchedAheadCount,
501
+ state.isActive,
502
+ state.isTransitioning,
503
+ state.lastOverlayTap,
504
+ state.lastOverlayDoubleTap,
505
+ play,
506
+ pause,
507
+ seek,
508
+ seekAndPlay,
509
+ skipToNext,
510
+ skipToPrevious,
511
+ setMuted,
512
+ setPlaybackRate,
513
+ setCaptionsEnabled,
514
+ selectCaptionTrack,
515
+ sendContentSignal,
516
+ setMaxBitrate,
517
+ setUserIdCmd,
518
+ clearUserIdCmd,
519
+ config.overlay,
520
+ ],
521
+ );
522
+
523
+ return (
524
+ <ShortKitContext.Provider value={value}>{children}</ShortKitContext.Provider>
525
+ );
526
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ export { ShortKitProvider } from './ShortKitProvider';
2
+ export { ShortKitFeed } from './ShortKitFeed';
3
+ export { useShortKitPlayer } from './useShortKitPlayer';
4
+ export { useShortKit } from './useShortKit';
5
+ export type {
6
+ FeedConfig,
7
+ FeedHeight,
8
+ OverlayConfig,
9
+ CarouselMode,
10
+ SurveyMode,
11
+
12
+ ContentItem,
13
+ JSONValue,
14
+ CaptionTrack,
15
+ PlayerTime,
16
+ PlayerState,
17
+ LoopEvent,
18
+ FeedTransitionEvent,
19
+ FormatChangeEvent,
20
+ ContentSignal,
21
+ SurveyOption,
22
+ ShortKitError,
23
+ ShortKitProviderProps,
24
+ ShortKitFeedProps,
25
+ ShortKitPlayerState,
26
+ } from './types';
@@ -0,0 +1,95 @@
1
+ import type {
2
+ FeedConfig,
3
+ ContentItem,
4
+ PlayerState,
5
+ PlayerTime,
6
+ } from './types';
7
+
8
+ /**
9
+ * Serialized FeedConfig with JSON strings for complex fields.
10
+ * Used by ShortKitFeedView native component props.
11
+ */
12
+ export interface SerializedFeedConfig {
13
+ feedHeight: string;
14
+ overlay: string;
15
+ carouselMode: string;
16
+ surveyMode: string;
17
+ muteOnStart: boolean;
18
+ }
19
+
20
+ /**
21
+ * Serialize FeedConfig for the bridge.
22
+ * Complex union types are JSON-stringified; the `component` field on custom
23
+ * overlays is stripped because React component references cannot cross the bridge.
24
+ */
25
+ export function serializeFeedConfig(config: FeedConfig): SerializedFeedConfig {
26
+ let overlay: string;
27
+ const rawOverlay = config.overlay ?? 'none';
28
+ if (typeof rawOverlay === 'string') {
29
+ overlay = JSON.stringify(rawOverlay);
30
+ } else {
31
+ // Strip the component ref — it stays on the JS side for OverlayManager
32
+ overlay = JSON.stringify({ type: 'custom' });
33
+ }
34
+
35
+ return {
36
+ feedHeight: JSON.stringify(config.feedHeight ?? { type: 'fullscreen' }),
37
+ overlay,
38
+ carouselMode: JSON.stringify(config.carouselMode ?? 'none'),
39
+ surveyMode: JSON.stringify(config.surveyMode ?? 'none'),
40
+ muteOnStart: config.muteOnStart ?? true,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Serialize a FeedConfig into a single JSON string for the TurboModule
46
+ * `initialize(config:)` parameter.
47
+ */
48
+ export function serializeFeedConfigForModule(config: FeedConfig): string {
49
+ const serialized = serializeFeedConfig(config);
50
+ return JSON.stringify(serialized);
51
+ }
52
+
53
+ /**
54
+ * Deserialize PlayerState from a bridge event.
55
+ * The native side sends `{ state: string; errorMessage?: string }`.
56
+ */
57
+ export function deserializePlayerState(event: {
58
+ state: string;
59
+ errorMessage?: string;
60
+ }): PlayerState {
61
+ if (event.state === 'error' && event.errorMessage) {
62
+ return { error: event.errorMessage };
63
+ }
64
+ return event.state as Exclude<PlayerState, { error: string }>;
65
+ }
66
+
67
+ /**
68
+ * Deserialize a ContentItem from a bridge JSON string.
69
+ * Returns null for null/undefined input or malformed JSON.
70
+ */
71
+ export function deserializeContentItem(
72
+ json: string | null | undefined,
73
+ ): ContentItem | null {
74
+ if (!json) return null;
75
+ try {
76
+ return JSON.parse(json) as ContentItem;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Deserialize PlayerTime from a bridge event.
84
+ */
85
+ export function deserializePlayerTime(event: {
86
+ current: number;
87
+ duration: number;
88
+ buffered: number;
89
+ }): PlayerTime {
90
+ return {
91
+ current: event.current,
92
+ duration: event.duration,
93
+ buffered: event.buffered,
94
+ };
95
+ }