@shortkitsdk/react-native 0.1.0 → 0.2.1

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.
@@ -6,6 +6,7 @@ import type { ShortKitContextValue } from './ShortKitContext';
6
6
  import type {
7
7
  ShortKitProviderProps,
8
8
  ContentItem,
9
+ CustomFeedItem,
9
10
  PlayerTime,
10
11
  PlayerState,
11
12
  CaptionTrack,
@@ -13,6 +14,7 @@ import type {
13
14
  } from './types';
14
15
  import {
15
16
  serializeFeedConfigForModule,
17
+ serializeCustomFeedItems,
16
18
  deserializePlayerState,
17
19
  deserializeContentItem,
18
20
  deserializePlayerTime,
@@ -34,6 +36,7 @@ interface State {
34
36
  activeCaptionTrack: CaptionTrack | null;
35
37
  activeCue: { text: string; startTime: number; endTime: number } | null;
36
38
  prefetchedAheadCount: number;
39
+ remainingContentCount: number;
37
40
  isActive: boolean;
38
41
  isTransitioning: boolean;
39
42
  lastOverlayTap: number;
@@ -51,6 +54,7 @@ const initialState: State = {
51
54
  activeCaptionTrack: null,
52
55
  activeCue: null,
53
56
  prefetchedAheadCount: 0,
57
+ remainingContentCount: 0,
54
58
  isActive: false,
55
59
  isTransitioning: false,
56
60
  lastOverlayTap: 0,
@@ -67,8 +71,9 @@ type Action =
67
71
  | { type: 'CAPTION_TRACK'; payload: CaptionTrack | null }
68
72
  | { type: 'CUE'; payload: { text: string; startTime: number; endTime: number } | null }
69
73
  | { type: 'PREFETCH_COUNT'; payload: number }
74
+ | { type: 'REMAINING_COUNT'; payload: number }
70
75
  | { type: 'OVERLAY_CONFIGURE'; payload: ContentItem }
71
- | { type: 'OVERLAY_ACTIVATE' }
76
+ | { type: 'OVERLAY_ACTIVATE'; payload: ContentItem }
72
77
  | { type: 'OVERLAY_RESET' }
73
78
  | { type: 'OVERLAY_FADE_OUT' }
74
79
  | { type: 'OVERLAY_RESTORE' }
@@ -108,10 +113,12 @@ function reducer(state: State, action: Action): State {
108
113
  return { ...state, activeCue: action.payload };
109
114
  case 'PREFETCH_COUNT':
110
115
  return { ...state, prefetchedAheadCount: action.payload };
116
+ case 'REMAINING_COUNT':
117
+ return { ...state, remainingContentCount: action.payload };
111
118
  case 'OVERLAY_CONFIGURE':
112
119
  return { ...state, nextItem: action.payload };
113
120
  case 'OVERLAY_ACTIVATE':
114
- return { ...state, currentItem: state.nextItem, isActive: true };
121
+ return { ...state, currentItem: action.payload, isActive: true };
115
122
  case 'OVERLAY_RESET':
116
123
  // Don't set isActive = false — the overlay stays mounted during
117
124
  // transitions. In the native SDK each cell has its own overlay
@@ -145,6 +152,7 @@ function reducer(state: State, action: Action): State {
145
152
  export function ShortKitProvider({
146
153
  apiKey,
147
154
  config,
155
+ embedId,
148
156
  userId,
149
157
  clientAppName,
150
158
  clientAppVersion,
@@ -168,6 +176,7 @@ export function ShortKitProvider({
168
176
  NativeShortKitModule.initialize(
169
177
  apiKey,
170
178
  serializedConfig,
179
+ embedId,
171
180
  clientAppName,
172
181
  clientAppVersion,
173
182
  serializedDimensions,
@@ -245,6 +254,7 @@ export function ShortKitProvider({
245
254
  NativeShortKitModule.onCurrentItemChanged((event) => {
246
255
  const item: ContentItem = {
247
256
  id: event.id,
257
+ playbackId: event.playbackId,
248
258
  title: event.title,
249
259
  description: event.description,
250
260
  duration: event.duration,
@@ -327,6 +337,13 @@ export function ShortKitProvider({
327
337
  }),
328
338
  );
329
339
 
340
+ // Remaining content count
341
+ subscriptions.push(
342
+ NativeShortKitModule.onRemainingContentCountChanged((event) => {
343
+ dispatch({ type: 'REMAINING_COUNT', payload: event.count });
344
+ }),
345
+ );
346
+
330
347
  // Overlay lifecycle events
331
348
  subscriptions.push(
332
349
  NativeShortKitModule.onOverlayConfigure((event) => {
@@ -338,8 +355,11 @@ export function ShortKitProvider({
338
355
  );
339
356
 
340
357
  subscriptions.push(
341
- NativeShortKitModule.onOverlayActivate((_event) => {
342
- dispatch({ type: 'OVERLAY_ACTIVATE' });
358
+ NativeShortKitModule.onOverlayActivate((event) => {
359
+ const item = deserializeContentItem(event.item);
360
+ if (item) {
361
+ dispatch({ type: 'OVERLAY_ACTIVATE', payload: item });
362
+ }
343
363
  }),
344
364
  );
345
365
 
@@ -447,6 +467,24 @@ export function ShortKitProvider({
447
467
  NativeShortKitModule?.clearUserId();
448
468
  }, []);
449
469
 
470
+ const setFeedItemsCmd = useCallback((items: CustomFeedItem[]) => {
471
+ NativeShortKitModule?.setFeedItems(serializeCustomFeedItems(items));
472
+ }, []);
473
+
474
+ const appendFeedItemsCmd = useCallback((items: CustomFeedItem[]) => {
475
+ NativeShortKitModule?.appendFeedItems(serializeCustomFeedItems(items));
476
+ }, []);
477
+
478
+ const fetchContentCmd = useCallback(async (limit: number = 10): Promise<ContentItem[]> => {
479
+ if (!NativeShortKitModule) return [];
480
+ const json = await NativeShortKitModule.fetchContent(limit);
481
+ try {
482
+ return JSON.parse(json) as ContentItem[];
483
+ } catch {
484
+ return [];
485
+ }
486
+ }, []);
487
+
450
488
  // -----------------------------------------------------------------------
451
489
  // Context value (memoized to avoid unnecessary re-renders)
452
490
  // -----------------------------------------------------------------------
@@ -463,6 +501,7 @@ export function ShortKitProvider({
463
501
  activeCaptionTrack: state.activeCaptionTrack,
464
502
  activeCue: state.activeCue,
465
503
  prefetchedAheadCount: state.prefetchedAheadCount,
504
+ remainingContentCount: state.remainingContentCount,
466
505
  isActive: state.isActive,
467
506
  isTransitioning: state.isTransitioning,
468
507
  lastOverlayTap: state.lastOverlayTap,
@@ -483,6 +522,9 @@ export function ShortKitProvider({
483
522
  setMaxBitrate,
484
523
  setUserId: setUserIdCmd,
485
524
  clearUserId: clearUserIdCmd,
525
+ setFeedItems: setFeedItemsCmd,
526
+ appendFeedItems: appendFeedItemsCmd,
527
+ fetchContent: fetchContentCmd,
486
528
 
487
529
  // Internal — consumed by ShortKitFeed to pass to OverlayManager
488
530
  _overlayConfig: config.overlay ?? 'none',
@@ -498,6 +540,7 @@ export function ShortKitProvider({
498
540
  state.activeCaptionTrack,
499
541
  state.activeCue,
500
542
  state.prefetchedAheadCount,
543
+ state.remainingContentCount,
501
544
  state.isActive,
502
545
  state.isTransitioning,
503
546
  state.lastOverlayTap,
@@ -516,6 +559,9 @@ export function ShortKitProvider({
516
559
  setMaxBitrate,
517
560
  setUserIdCmd,
518
561
  clearUserIdCmd,
562
+ setFeedItemsCmd,
563
+ appendFeedItemsCmd,
564
+ fetchContentCmd,
519
565
  config.overlay,
520
566
  ],
521
567
  );
@@ -0,0 +1,63 @@
1
+ import React, { useContext, useMemo } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import type { ShortKitWidgetProps } from './types';
4
+ import ShortKitWidgetView from './specs/ShortKitWidgetViewNativeComponent';
5
+ import { ShortKitContext } from './ShortKitContext';
6
+
7
+ /**
8
+ * Horizontal carousel widget component. Displays a row of video cards
9
+ * with automatic rotation. Wraps a native Fabric view.
10
+ *
11
+ * Must be rendered inside a `<ShortKitProvider>`.
12
+ */
13
+ export function ShortKitWidget(props: ShortKitWidgetProps) {
14
+ const { config, items, style } = props;
15
+
16
+ const context = useContext(ShortKitContext);
17
+ if (!context) {
18
+ throw new Error('ShortKitWidget must be used within a ShortKitProvider');
19
+ }
20
+
21
+ const serializedConfig = useMemo(() => {
22
+ const cfg = config ?? {};
23
+ return JSON.stringify({
24
+ cardCount: cfg.cardCount ?? 3,
25
+ cardSpacing: cfg.cardSpacing ?? 8,
26
+ cornerRadius: cfg.cornerRadius ?? 12,
27
+ autoplay: cfg.autoplay ?? true,
28
+ muteOnStart: cfg.muteOnStart ?? true,
29
+ loop: cfg.loop ?? true,
30
+ rotationInterval: cfg.rotationInterval ?? 10000,
31
+ clickAction: cfg.clickAction ?? 'feed',
32
+ overlay: cfg.overlay
33
+ ? typeof cfg.overlay === 'string'
34
+ ? cfg.overlay
35
+ : { type: 'custom' }
36
+ : 'none',
37
+ });
38
+ }, [config]);
39
+
40
+ const serializedItems = useMemo(() => {
41
+ if (!items || items.length === 0) return undefined;
42
+ return JSON.stringify(items);
43
+ }, [items]);
44
+
45
+ return (
46
+ <View style={[styles.container, style]}>
47
+ <ShortKitWidgetView
48
+ style={styles.widget}
49
+ config={serializedConfig}
50
+ items={serializedItems}
51
+ />
52
+ </View>
53
+ );
54
+ }
55
+
56
+ const styles = StyleSheet.create({
57
+ container: {
58
+ overflow: 'hidden',
59
+ },
60
+ widget: {
61
+ flex: 1,
62
+ },
63
+ });
package/src/index.ts CHANGED
@@ -1,15 +1,25 @@
1
1
  export { ShortKitProvider } from './ShortKitProvider';
2
2
  export { ShortKitFeed } from './ShortKitFeed';
3
+ export { ShortKitPlayer } from './ShortKitPlayer';
4
+ export { ShortKitWidget } from './ShortKitWidget';
3
5
  export { useShortKitPlayer } from './useShortKitPlayer';
4
6
  export { useShortKit } from './useShortKit';
5
7
  export type {
6
8
  FeedConfig,
7
9
  FeedHeight,
10
+ FeedSource,
8
11
  OverlayConfig,
9
12
  CarouselMode,
10
13
  SurveyMode,
11
14
 
15
+ PlayerConfig,
16
+ PlayerClickAction,
17
+ WidgetConfig,
18
+
12
19
  ContentItem,
20
+ CarouselImage,
21
+ ImageCarouselItem,
22
+ CustomFeedItem,
13
23
  JSONValue,
14
24
  CaptionTrack,
15
25
  PlayerTime,
@@ -22,5 +32,7 @@ export type {
22
32
  ShortKitError,
23
33
  ShortKitProviderProps,
24
34
  ShortKitFeedProps,
35
+ ShortKitPlayerProps,
36
+ ShortKitWidgetProps,
25
37
  ShortKitPlayerState,
26
38
  } from './types';
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  FeedConfig,
3
3
  ContentItem,
4
+ CustomFeedItem,
4
5
  PlayerState,
5
6
  PlayerTime,
6
7
  } from './types';
@@ -15,6 +16,7 @@ export interface SerializedFeedConfig {
15
16
  carouselMode: string;
16
17
  surveyMode: string;
17
18
  muteOnStart: boolean;
19
+ feedSource: string;
18
20
  }
19
21
 
20
22
  /**
@@ -38,6 +40,7 @@ export function serializeFeedConfig(config: FeedConfig): SerializedFeedConfig {
38
40
  carouselMode: JSON.stringify(config.carouselMode ?? 'none'),
39
41
  surveyMode: JSON.stringify(config.surveyMode ?? 'none'),
40
42
  muteOnStart: config.muteOnStart ?? true,
43
+ feedSource: config.feedSource ?? 'algorithmic',
41
44
  };
42
45
  }
43
46
 
@@ -93,3 +96,10 @@ export function deserializePlayerTime(event: {
93
96
  buffered: event.buffered,
94
97
  };
95
98
  }
99
+
100
+ /**
101
+ * Serialize CustomFeedItem[] to a JSON string for the bridge.
102
+ */
103
+ export function serializeCustomFeedItems(items: CustomFeedItem[]): string {
104
+ return JSON.stringify(items);
105
+ }
@@ -11,6 +11,7 @@ type PlayerStateEvent = Readonly<{
11
11
 
12
12
  type CurrentItemEvent = Readonly<{
13
13
  id: string;
14
+ playbackId?: string;
14
15
  title: string;
15
16
  description?: string;
16
17
  duration: Double;
@@ -77,6 +78,10 @@ type PrefetchedAheadCountEvent = Readonly<{
77
78
  count: Int32;
78
79
  }>;
79
80
 
81
+ type RemainingContentCountEvent = Readonly<{
82
+ count: Int32;
83
+ }>;
84
+
80
85
  type ErrorEvent = Readonly<{
81
86
  code: string;
82
87
  message: string;
@@ -92,26 +97,6 @@ type SurveyResponseEvent = Readonly<{
92
97
  optionText: string;
93
98
  }>;
94
99
 
95
- type ArticleTappedEvent = Readonly<{
96
- item: string; // JSON-serialized ContentItem
97
- }>;
98
-
99
- type CommentTappedEvent = Readonly<{
100
- item: string; // JSON-serialized ContentItem
101
- }>;
102
-
103
- type OverlayShareTappedEvent = Readonly<{
104
- item: string; // JSON-serialized ContentItem
105
- }>;
106
-
107
- type SaveTappedEvent = Readonly<{
108
- item: string; // JSON-serialized ContentItem
109
- }>;
110
-
111
- type LikeTappedEvent = Readonly<{
112
- item: string; // JSON-serialized ContentItem
113
- }>;
114
-
115
100
  type OverlayConfigureEvent = Readonly<{
116
101
  item: string; // JSON-serialized ContentItem
117
102
  }>;
@@ -132,6 +117,11 @@ type OverlayRestoreEvent = Readonly<{
132
117
  item: string; // JSON-serialized ContentItem
133
118
  }>;
134
119
 
120
+ type ContentTappedEvent = Readonly<{
121
+ contentId: string;
122
+ index: Int32;
123
+ }>;
124
+
135
125
  type OverlayTapEvent = Readonly<{}>;
136
126
 
137
127
  type OverlayDoubleTapEvent = Readonly<{
@@ -144,6 +134,7 @@ export interface Spec extends TurboModule {
144
134
  initialize(
145
135
  apiKey: string,
146
136
  config: string, // JSON-serialized FeedConfig
137
+ embedId?: string,
147
138
  clientAppName?: string,
148
139
  clientAppVersion?: string,
149
140
  customDimensions?: string, // JSON-serialized Record<string, string>
@@ -168,6 +159,11 @@ export interface Spec extends TurboModule {
168
159
  sendContentSignal(signal: string): void;
169
160
  setMaxBitrate(bitrate: Double): void;
170
161
 
162
+ // --- Custom feed ---
163
+ setFeedItems(items: string): void;
164
+ appendFeedItems(items: string): void;
165
+ fetchContent(limit: Int32): Promise<string>;
166
+
171
167
  // --- Event emitters ---
172
168
  readonly onPlayerStateChanged: EventEmitter<PlayerStateEvent>;
173
169
  readonly onCurrentItemChanged: EventEmitter<CurrentItemEvent>;
@@ -181,14 +177,10 @@ export interface Spec extends TurboModule {
181
177
  readonly onFeedTransition: EventEmitter<FeedTransitionEvent>;
182
178
  readonly onFormatChange: EventEmitter<FormatChangeEvent>;
183
179
  readonly onPrefetchedAheadCountChanged: EventEmitter<PrefetchedAheadCountEvent>;
180
+ readonly onRemainingContentCountChanged: EventEmitter<RemainingContentCountEvent>;
184
181
  readonly onError: EventEmitter<ErrorEvent>;
185
182
  readonly onShareTapped: EventEmitter<ShareTappedEvent>;
186
183
  readonly onSurveyResponse: EventEmitter<SurveyResponseEvent>;
187
- readonly onArticleTapped: EventEmitter<ArticleTappedEvent>;
188
- readonly onCommentTapped: EventEmitter<CommentTappedEvent>;
189
- readonly onOverlayShareTapped: EventEmitter<OverlayShareTappedEvent>;
190
- readonly onSaveTapped: EventEmitter<SaveTappedEvent>;
191
- readonly onLikeTapped: EventEmitter<LikeTappedEvent>;
192
184
  readonly onOverlayConfigure: EventEmitter<OverlayConfigureEvent>;
193
185
  readonly onOverlayActivate: EventEmitter<OverlayActivateEvent>;
194
186
  readonly onOverlayReset: EventEmitter<OverlayResetEvent>;
@@ -196,6 +188,7 @@ export interface Spec extends TurboModule {
196
188
  readonly onOverlayRestore: EventEmitter<OverlayRestoreEvent>;
197
189
  readonly onOverlayTap: EventEmitter<OverlayTapEvent>;
198
190
  readonly onOverlayDoubleTap: EventEmitter<OverlayDoubleTapEvent>;
191
+ readonly onContentTapped: EventEmitter<ContentTappedEvent>;
199
192
  }
200
193
 
201
194
  export default TurboModuleRegistry.getEnforcing<Spec>('ShortKitModule');
@@ -0,0 +1,13 @@
1
+ import type { HostComponent, ViewProps } from 'react-native';
2
+ import { codegenNativeComponent } from 'react-native';
3
+
4
+ export interface NativeProps extends ViewProps {
5
+ config: string;
6
+ contentItem?: string;
7
+ active?: boolean;
8
+ }
9
+
10
+ export default codegenNativeComponent<NativeProps>(
11
+ 'ShortKitPlayerView',
12
+ {},
13
+ ) as HostComponent<NativeProps>;
@@ -0,0 +1,12 @@
1
+ import type { HostComponent, ViewProps } from 'react-native';
2
+ import { codegenNativeComponent } from 'react-native';
3
+
4
+ export interface NativeProps extends ViewProps {
5
+ config: string;
6
+ items?: string;
7
+ }
8
+
9
+ export default codegenNativeComponent<NativeProps>(
10
+ 'ShortKitWidgetView',
11
+ {},
12
+ ) as HostComponent<NativeProps>;
package/src/types.ts CHANGED
@@ -2,12 +2,15 @@ import type { ViewStyle } from 'react-native';
2
2
 
3
3
  // --- Configuration ---
4
4
 
5
+ export type FeedSource = 'algorithmic' | 'custom';
6
+
5
7
  export interface FeedConfig {
6
8
  feedHeight?: FeedHeight;
7
9
  overlay?: OverlayConfig;
8
10
  carouselMode?: CarouselMode;
9
11
  surveyMode?: SurveyMode;
10
12
  muteOnStart?: boolean;
13
+ feedSource?: FeedSource;
11
14
  }
12
15
 
13
16
  export type FeedHeight =
@@ -30,6 +33,7 @@ export type SurveyMode =
30
33
 
31
34
  export interface ContentItem {
32
35
  id: string;
36
+ playbackId?: string;
33
37
  title: string;
34
38
  description?: string;
35
39
  duration: number;
@@ -42,6 +46,29 @@ export interface ContentItem {
42
46
  commentCount?: number;
43
47
  }
44
48
 
49
+ // --- Custom Feed Types ---
50
+
51
+ export interface CarouselImage {
52
+ url: string;
53
+ alt?: string;
54
+ }
55
+
56
+ export interface ImageCarouselItem {
57
+ id: string;
58
+ images: CarouselImage[];
59
+ autoScrollInterval?: number;
60
+ caption?: string;
61
+ title?: string;
62
+ description?: string;
63
+ author?: string;
64
+ section?: string;
65
+ articleUrl?: string;
66
+ }
67
+
68
+ export type CustomFeedItem =
69
+ | { type: 'video'; playbackId: string }
70
+ | { type: 'imageCarousel'; item: ImageCarouselItem };
71
+
45
72
  export type JSONValue =
46
73
  | string
47
74
  | number
@@ -109,6 +136,7 @@ export interface ShortKitError {
109
136
  export interface ShortKitProviderProps {
110
137
  apiKey: string;
111
138
  config: FeedConfig;
139
+ embedId?: string;
112
140
  userId?: string;
113
141
 
114
142
  clientAppName?: string;
@@ -127,11 +155,55 @@ export interface ShortKitFeedProps {
127
155
  onLoop?: (event: LoopEvent) => void;
128
156
  onFeedTransition?: (event: FeedTransitionEvent) => void;
129
157
  onFormatChange?: (event: FormatChangeEvent) => void;
130
- onArticleTapped?: (item: ContentItem) => void;
131
- onCommentTapped?: (item: ContentItem) => void;
132
- onOverlayShareTapped?: (item: ContentItem) => void;
133
- onSaveTapped?: (item: ContentItem) => void;
134
- onLikeTapped?: (item: ContentItem) => void;
158
+ onContentTapped?: (contentId: string, index: number) => void;
159
+ }
160
+
161
+ // --- Single Player Config ---
162
+
163
+ export type PlayerClickAction = 'feed' | 'mute' | 'none';
164
+
165
+ export interface PlayerConfig {
166
+ cornerRadius?: number;
167
+ clickAction?: PlayerClickAction;
168
+ autoplay?: boolean;
169
+ loop?: boolean;
170
+ muteOnStart?: boolean;
171
+ overlay?: OverlayConfig;
172
+ }
173
+
174
+ // --- Widget Config ---
175
+
176
+ export interface WidgetConfig {
177
+ cardCount?: number;
178
+ cardSpacing?: number;
179
+ cornerRadius?: number;
180
+ autoplay?: boolean;
181
+ muteOnStart?: boolean;
182
+ loop?: boolean;
183
+ rotationInterval?: number;
184
+ clickAction?: PlayerClickAction;
185
+ overlay?: OverlayConfig;
186
+ }
187
+
188
+ // --- Single Player Props ---
189
+
190
+ export interface ShortKitPlayerProps {
191
+ config?: PlayerConfig;
192
+ contentItem?: ContentItem;
193
+ /** Controls whether the player is actively playing. When `false` the player
194
+ * returns to thumbnail-only mode without tearing down the view. Defaults to
195
+ * the value of `config.autoplay` (which itself defaults to `true`). */
196
+ active?: boolean;
197
+ style?: ViewStyle;
198
+ onTap?: () => void;
199
+ }
200
+
201
+ // --- Widget Props ---
202
+
203
+ export interface ShortKitWidgetProps {
204
+ config?: WidgetConfig;
205
+ items?: ContentItem[];
206
+ style?: ViewStyle;
135
207
  }
136
208
 
137
209
  // --- Hook Return Types ---
@@ -147,6 +219,7 @@ export interface ShortKitPlayerState {
147
219
  activeCaptionTrack: CaptionTrack | null;
148
220
  activeCue: { text: string; startTime: number; endTime: number } | null;
149
221
  prefetchedAheadCount: number;
222
+ remainingContentCount: number;
150
223
  isActive: boolean;
151
224
  isTransitioning: boolean;
152
225
  lastOverlayTap: number;
@@ -6,7 +6,7 @@ import { ShortKitContext } from './ShortKitContext';
6
6
  *
7
7
  * Must be used within a `<ShortKitProvider>`.
8
8
  *
9
- * @returns SDK operations: `setUserId` and `clearUserId`.
9
+ * @returns SDK operations and state.
10
10
  */
11
11
  export function useShortKit() {
12
12
  const context = useContext(ShortKitContext);
@@ -16,5 +16,9 @@ export function useShortKit() {
16
16
  return {
17
17
  setUserId: context.setUserId,
18
18
  clearUserId: context.clearUserId,
19
+ setFeedItems: context.setFeedItems,
20
+ appendFeedItems: context.appendFeedItems,
21
+ fetchContent: context.fetchContent,
22
+ remainingContentCount: context.remainingContentCount,
19
23
  };
20
24
  }