@shortkitsdk/react-native 0.2.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.
@@ -54,6 +54,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
54
54
  @"onFeedTransition",
55
55
  @"onFormatChange",
56
56
  @"onPrefetchedAheadCountChanged",
57
+ @"onRemainingContentCountChanged",
57
58
  @"onError",
58
59
  @"onShareTapped",
59
60
  @"onSurveyResponse",
@@ -64,6 +65,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
64
65
  @"onOverlayRestore",
65
66
  @"onOverlayTap",
66
67
  @"onOverlayDoubleTap",
68
+ @"onContentTapped",
67
69
  ];
68
70
  }
69
71
 
@@ -102,6 +104,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
102
104
 
103
105
  RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
104
106
  config:(NSString *)config
107
+ embedId:(NSString *)embedId
105
108
  clientAppName:(NSString *)clientAppName
106
109
  clientAppVersion:(NSString *)clientAppVersion
107
110
  customDimensions:(NSString *)customDimensions) {
@@ -110,6 +113,7 @@ RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
110
113
 
111
114
  _shortKitBridge = [[ShortKitBridge alloc] initWithApiKey:apiKey
112
115
  config:config
116
+ embedId:embedId
113
117
  clientAppName:clientAppName
114
118
  clientAppVersion:clientAppVersion
115
119
  customDimensions:customDimensions
@@ -187,6 +191,24 @@ RCT_EXPORT_METHOD(setMaxBitrate:(double)bitrate) {
187
191
  [_shortKitBridge setMaxBitrate:bitrate];
188
192
  }
189
193
 
194
+ // MARK: - Custom Feed
195
+
196
+ RCT_EXPORT_METHOD(setFeedItems:(NSString *)items) {
197
+ [_shortKitBridge setFeedItems:items];
198
+ }
199
+
200
+ RCT_EXPORT_METHOD(appendFeedItems:(NSString *)items) {
201
+ [_shortKitBridge appendFeedItems:items];
202
+ }
203
+
204
+ RCT_EXPORT_METHOD(fetchContent:(NSInteger)limit
205
+ resolve:(RCTPromiseResolveBlock)resolve
206
+ reject:(RCTPromiseRejectBlock)reject) {
207
+ [_shortKitBridge fetchContent:limit completion:^(NSString *json) {
208
+ resolve(json);
209
+ }];
210
+ }
211
+
190
212
  // MARK: - New Architecture (TurboModule)
191
213
 
192
214
  #ifdef RCT_NEW_ARCH_ENABLED
@@ -0,0 +1,186 @@
1
+ import UIKit
2
+ import ShortKit
3
+
4
+ /// Fabric native view wrapping `ShortKitPlayerViewController` for use as
5
+ /// a single-video player in React Native.
6
+ ///
7
+ /// Props (set by RCTViewManager):
8
+ /// - `config`: JSON string with PlayerConfig values
9
+ /// - `contentItem`: JSON string with ContentItem data
10
+ @objc public class ShortKitPlayerNativeView: UIView {
11
+
12
+ // MARK: - Props
13
+
14
+ @objc public var config: String? {
15
+ didSet {
16
+ guard config != oldValue else { return }
17
+ applyConfig()
18
+ }
19
+ }
20
+
21
+ @objc public var contentItem: String? {
22
+ didSet {
23
+ guard contentItem != oldValue else { return }
24
+ applyContentItem()
25
+ }
26
+ }
27
+
28
+ @objc public var active: Bool = true {
29
+ didSet {
30
+ guard active != oldValue else { return }
31
+ applyActive()
32
+ }
33
+ }
34
+
35
+ // MARK: - Child VC
36
+
37
+ private var playerVC: ShortKitPlayerViewController?
38
+ private var parsedConfig: PlayerConfig?
39
+
40
+ // MARK: - Lifecycle
41
+
42
+ public override func didMoveToWindow() {
43
+ super.didMoveToWindow()
44
+ if window != nil {
45
+ embedPlayerVCIfNeeded()
46
+ }
47
+ }
48
+
49
+ public override func willMove(toWindow newWindow: UIWindow?) {
50
+ super.willMove(toWindow: newWindow)
51
+ if newWindow == nil {
52
+ removePlayerVC()
53
+ }
54
+ }
55
+
56
+ public override func layoutSubviews() {
57
+ super.layoutSubviews()
58
+ playerVC?.view.frame = bounds
59
+ }
60
+
61
+ // MARK: - VC Containment
62
+
63
+ private func embedPlayerVCIfNeeded() {
64
+ guard playerVC == nil else { return }
65
+ guard let sdk = ShortKitBridge.shared?.sdk else { return }
66
+ guard let parentVC = findParentViewController() else { return }
67
+
68
+ let playerConfig = parsedConfig ?? PlayerConfig()
69
+
70
+ let vc = ShortKitPlayerViewController(shortKit: sdk, config: playerConfig)
71
+ self.playerVC = vc
72
+
73
+ // Inject content BEFORE the view loads so loadData() skips its own
74
+ // fetch. Accessing vc.view triggers viewDidLoad, so configure first.
75
+ if let json = contentItem, let item = Self.parseContentItem(json) {
76
+ vc.configure(with: item)
77
+ }
78
+
79
+ parentVC.addChild(vc)
80
+ vc.view.frame = bounds
81
+ vc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
82
+ addSubview(vc.view)
83
+ vc.didMove(toParent: parentVC)
84
+
85
+ // Activate based on the `active` prop (defaults to true)
86
+ if active {
87
+ vc.activate()
88
+ }
89
+ }
90
+
91
+ private func removePlayerVC() {
92
+ guard let vc = playerVC else { return }
93
+ vc.deactivate()
94
+ vc.willMove(toParent: nil)
95
+ vc.view.removeFromSuperview()
96
+ vc.removeFromParent()
97
+ self.playerVC = nil
98
+ }
99
+
100
+ // MARK: - Prop Application
101
+
102
+ private func applyConfig() {
103
+ guard let json = config else { return }
104
+ parsedConfig = Self.parsePlayerConfig(json)
105
+ // Config changes require re-embedding
106
+ if playerVC != nil {
107
+ removePlayerVC()
108
+ embedPlayerVCIfNeeded()
109
+ }
110
+ }
111
+
112
+ private func applyContentItem() {
113
+ guard let json = contentItem, let item = Self.parseContentItem(json) else { return }
114
+ playerVC?.configure(with: item)
115
+ }
116
+
117
+ private func applyActive() {
118
+ guard let vc = playerVC else { return }
119
+ if active {
120
+ vc.activate()
121
+ } else {
122
+ vc.deactivate()
123
+ }
124
+ }
125
+
126
+ // MARK: - Parsing
127
+
128
+ private static func parsePlayerConfig(_ json: String) -> PlayerConfig {
129
+ guard let data = json.data(using: .utf8),
130
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
131
+ return PlayerConfig()
132
+ }
133
+
134
+ let cornerRadius = obj["cornerRadius"] as? CGFloat ?? 12
135
+ let autoplay = obj["autoplay"] as? Bool ?? true
136
+ let loop = obj["loop"] as? Bool ?? true
137
+ let muteOnStart = obj["muteOnStart"] as? Bool ?? true
138
+
139
+ let clickAction: PlayerClickAction
140
+ switch obj["clickAction"] as? String {
141
+ case "feed": clickAction = .feed
142
+ case "mute": clickAction = .mute
143
+ case "none": clickAction = .none
144
+ default: clickAction = .feed
145
+ }
146
+
147
+ let overlayMode: VideoOverlayMode
148
+ if let overlayObj = obj["overlay"] as? [String: Any],
149
+ overlayObj["type"] as? String == "custom" {
150
+ overlayMode = .custom { @Sendable in
151
+ let overlay = ShortKitOverlayBridge()
152
+ overlay.bridge = ShortKitBridge.shared
153
+ return overlay
154
+ }
155
+ } else {
156
+ overlayMode = .none
157
+ }
158
+
159
+ return PlayerConfig(
160
+ cornerRadius: cornerRadius,
161
+ clickAction: clickAction,
162
+ autoplay: autoplay,
163
+ loop: loop,
164
+ muteOnStart: muteOnStart,
165
+ videoOverlay: overlayMode
166
+ )
167
+ }
168
+
169
+ private static func parseContentItem(_ json: String) -> ContentItem? {
170
+ guard let data = json.data(using: .utf8) else { return nil }
171
+ return try? JSONDecoder().decode(ContentItem.self, from: data)
172
+ }
173
+
174
+ // MARK: - Helpers
175
+
176
+ private func findParentViewController() -> UIViewController? {
177
+ var responder: UIResponder? = self
178
+ while let next = responder?.next {
179
+ if let vc = next as? UIViewController {
180
+ return vc
181
+ }
182
+ responder = next
183
+ }
184
+ return nil
185
+ }
186
+ }
@@ -0,0 +1,28 @@
1
+ #import <React/RCTViewManager.h>
2
+
3
+ #if __has_include(<ShortKitReactNative/ShortKitReactNative-Swift.h>)
4
+ #import <ShortKitReactNative/ShortKitReactNative-Swift.h>
5
+ #else
6
+ #import "ShortKitReactNative-Swift.h"
7
+ #endif
8
+
9
+ @interface ShortKitPlayerViewManager : RCTViewManager
10
+ @end
11
+
12
+ @implementation ShortKitPlayerViewManager
13
+
14
+ RCT_EXPORT_MODULE(ShortKitPlayerView)
15
+
16
+ + (BOOL)requiresMainQueueSetup {
17
+ return YES;
18
+ }
19
+
20
+ - (UIView *)view {
21
+ return [[ShortKitPlayerNativeView alloc] init];
22
+ }
23
+
24
+ RCT_EXPORT_VIEW_PROPERTY(config, NSString)
25
+ RCT_EXPORT_VIEW_PROPERTY(contentItem, NSString)
26
+ RCT_EXPORT_VIEW_PROPERTY(active, BOOL)
27
+
28
+ @end
@@ -0,0 +1,168 @@
1
+ import UIKit
2
+ import ShortKit
3
+
4
+ /// Fabric native view wrapping `ShortKitWidgetViewController` for use as
5
+ /// a horizontal video carousel in React Native.
6
+ ///
7
+ /// Props (set by RCTViewManager):
8
+ /// - `config`: JSON string with WidgetConfig values
9
+ /// - `items`: JSON string with ContentItem array
10
+ @objc public class ShortKitWidgetNativeView: UIView {
11
+
12
+ // MARK: - Props
13
+
14
+ @objc public var config: String? {
15
+ didSet {
16
+ guard config != oldValue else { return }
17
+ applyConfig()
18
+ }
19
+ }
20
+
21
+ @objc public var items: String? {
22
+ didSet {
23
+ guard items != oldValue else { return }
24
+ applyItems()
25
+ }
26
+ }
27
+
28
+ // MARK: - Child VC
29
+
30
+ private var widgetVC: ShortKitWidgetViewController?
31
+ private var parsedConfig: WidgetConfig?
32
+
33
+ // MARK: - Lifecycle
34
+
35
+ public override func didMoveToWindow() {
36
+ super.didMoveToWindow()
37
+ if window != nil {
38
+ embedWidgetVCIfNeeded()
39
+ }
40
+ }
41
+
42
+ public override func willMove(toWindow newWindow: UIWindow?) {
43
+ super.willMove(toWindow: newWindow)
44
+ if newWindow == nil {
45
+ removeWidgetVC()
46
+ }
47
+ }
48
+
49
+ public override func layoutSubviews() {
50
+ super.layoutSubviews()
51
+ widgetVC?.view.frame = bounds
52
+ }
53
+
54
+ // MARK: - VC Containment
55
+
56
+ private func embedWidgetVCIfNeeded() {
57
+ guard widgetVC == nil else { return }
58
+ guard let sdk = ShortKitBridge.shared?.sdk else { return }
59
+ guard let parentVC = findParentViewController() else { return }
60
+
61
+ let widgetConfig = parsedConfig ?? WidgetConfig()
62
+
63
+ let vc = ShortKitWidgetViewController(shortKit: sdk, config: widgetConfig)
64
+ self.widgetVC = vc
65
+
66
+ parentVC.addChild(vc)
67
+ vc.view.frame = bounds
68
+ vc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
69
+ addSubview(vc.view)
70
+ vc.didMove(toParent: parentVC)
71
+
72
+ // If items were already set, apply them
73
+ if let json = items, let contentItems = Self.parseContentItems(json) {
74
+ vc.configure(with: contentItems)
75
+ }
76
+ }
77
+
78
+ private func removeWidgetVC() {
79
+ guard let vc = widgetVC else { return }
80
+ vc.willMove(toParent: nil)
81
+ vc.view.removeFromSuperview()
82
+ vc.removeFromParent()
83
+ self.widgetVC = nil
84
+ }
85
+
86
+ // MARK: - Prop Application
87
+
88
+ private func applyConfig() {
89
+ guard let json = config else { return }
90
+ parsedConfig = Self.parseWidgetConfig(json)
91
+ if widgetVC != nil {
92
+ removeWidgetVC()
93
+ embedWidgetVCIfNeeded()
94
+ }
95
+ }
96
+
97
+ private func applyItems() {
98
+ guard let json = items, let contentItems = Self.parseContentItems(json) else { return }
99
+ widgetVC?.configure(with: contentItems)
100
+ }
101
+
102
+ // MARK: - Parsing
103
+
104
+ private static func parseWidgetConfig(_ json: String) -> WidgetConfig {
105
+ guard let data = json.data(using: .utf8),
106
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
107
+ return WidgetConfig()
108
+ }
109
+
110
+ let cardCount = obj["cardCount"] as? Int ?? 3
111
+ let cardSpacing = obj["cardSpacing"] as? CGFloat ?? 8
112
+ let cornerRadius = obj["cornerRadius"] as? CGFloat ?? 12
113
+ let autoplay = obj["autoplay"] as? Bool ?? true
114
+ let muteOnStart = obj["muteOnStart"] as? Bool ?? true
115
+ let loop = obj["loop"] as? Bool ?? true
116
+ let rotationInterval = obj["rotationInterval"] as? TimeInterval ?? 10000
117
+
118
+ let clickAction: PlayerClickAction
119
+ switch obj["clickAction"] as? String {
120
+ case "feed": clickAction = .feed
121
+ case "mute": clickAction = .mute
122
+ case "none": clickAction = .none
123
+ default: clickAction = .feed
124
+ }
125
+
126
+ let overlayMode: VideoOverlayMode
127
+ if let overlayObj = obj["overlay"] as? [String: Any],
128
+ overlayObj["type"] as? String == "custom" {
129
+ overlayMode = .custom { @Sendable in
130
+ let overlay = ShortKitOverlayBridge()
131
+ overlay.bridge = ShortKitBridge.shared
132
+ return overlay
133
+ }
134
+ } else {
135
+ overlayMode = .none
136
+ }
137
+
138
+ return WidgetConfig(
139
+ cardCount: cardCount,
140
+ cardSpacing: cardSpacing,
141
+ cornerRadius: cornerRadius,
142
+ autoplay: autoplay,
143
+ muteOnStart: muteOnStart,
144
+ loop: loop,
145
+ rotationInterval: rotationInterval / 1000.0, // JS sends ms, iOS expects seconds
146
+ clickAction: clickAction,
147
+ cardOverlay: overlayMode
148
+ )
149
+ }
150
+
151
+ private static func parseContentItems(_ json: String) -> [ContentItem]? {
152
+ guard let data = json.data(using: .utf8) else { return nil }
153
+ return try? JSONDecoder().decode([ContentItem].self, from: data)
154
+ }
155
+
156
+ // MARK: - Helpers
157
+
158
+ private func findParentViewController() -> UIViewController? {
159
+ var responder: UIResponder? = self
160
+ while let next = responder?.next {
161
+ if let vc = next as? UIViewController {
162
+ return vc
163
+ }
164
+ responder = next
165
+ }
166
+ return nil
167
+ }
168
+ }
@@ -0,0 +1,27 @@
1
+ #import <React/RCTViewManager.h>
2
+
3
+ #if __has_include(<ShortKitReactNative/ShortKitReactNative-Swift.h>)
4
+ #import <ShortKitReactNative/ShortKitReactNative-Swift.h>
5
+ #else
6
+ #import "ShortKitReactNative-Swift.h"
7
+ #endif
8
+
9
+ @interface ShortKitWidgetViewManager : RCTViewManager
10
+ @end
11
+
12
+ @implementation ShortKitWidgetViewManager
13
+
14
+ RCT_EXPORT_MODULE(ShortKitWidgetView)
15
+
16
+ + (BOOL)requiresMainQueueSetup {
17
+ return YES;
18
+ }
19
+
20
+ - (UIView *)view {
21
+ return [[ShortKitWidgetNativeView alloc] init];
22
+ }
23
+
24
+ RCT_EXPORT_VIEW_PROPERTY(config, NSString)
25
+ RCT_EXPORT_VIEW_PROPERTY(items, NSString)
26
+
27
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shortkitsdk/react-native",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "ShortKit React Native SDK — short-form video feed",
5
5
  "react-native": "src/index",
6
6
  "source": "src/index",
@@ -1,6 +1,7 @@
1
1
  import { createContext } from 'react';
2
2
  import type {
3
3
  ContentItem,
4
+ CustomFeedItem,
4
5
  PlayerTime,
5
6
  PlayerState,
6
7
  CaptionTrack,
@@ -20,6 +21,7 @@ export interface ShortKitContextValue {
20
21
  activeCaptionTrack: CaptionTrack | null;
21
22
  activeCue: { text: string; startTime: number; endTime: number } | null;
22
23
  prefetchedAheadCount: number;
24
+ remainingContentCount: number;
23
25
  isActive: boolean;
24
26
  isTransitioning: boolean;
25
27
  lastOverlayTap: number;
@@ -42,6 +44,9 @@ export interface ShortKitContextValue {
42
44
  // SDK operations
43
45
  setUserId: (id: string) => void;
44
46
  clearUserId: () => void;
47
+ setFeedItems: (items: CustomFeedItem[]) => void;
48
+ appendFeedItems: (items: CustomFeedItem[]) => void;
49
+ fetchContent: (limit?: number) => Promise<ContentItem[]>;
45
50
 
46
51
  // Internal — used by ShortKitFeed to render custom overlays
47
52
  /** @internal */
@@ -29,6 +29,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
29
29
  onLoop,
30
30
  onFeedTransition,
31
31
  onFormatChange,
32
+ onContentTapped,
32
33
  } = props;
33
34
 
34
35
  const context = useContext(ShortKitContext);
@@ -109,6 +110,14 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
109
110
  );
110
111
  }
111
112
 
113
+ if (onContentTapped) {
114
+ subscriptions.push(
115
+ NativeShortKitModule.onContentTapped((event) => {
116
+ onContentTapped(event.contentId, event.index);
117
+ }),
118
+ );
119
+ }
120
+
112
121
  return () => {
113
122
  for (const sub of subscriptions) {
114
123
  sub.remove();
@@ -121,6 +130,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
121
130
  onLoop,
122
131
  onFeedTransition,
123
132
  onFormatChange,
133
+ onContentTapped,
124
134
  ]);
125
135
 
126
136
  // ---------------------------------------------------------------------------
@@ -0,0 +1,61 @@
1
+ import React, { useContext, useMemo } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import type { ShortKitPlayerProps } from './types';
4
+ import ShortKitPlayerView from './specs/ShortKitPlayerViewNativeComponent';
5
+ import { ShortKitContext } from './ShortKitContext';
6
+
7
+ /**
8
+ * Single-video player component. Displays one video with thumbnail fallback
9
+ * and optional overlay. Wraps a native Fabric view.
10
+ *
11
+ * Must be rendered inside a `<ShortKitProvider>`.
12
+ */
13
+ export function ShortKitPlayer(props: ShortKitPlayerProps) {
14
+ const { config, contentItem, active, style } = props;
15
+
16
+ const context = useContext(ShortKitContext);
17
+ if (!context) {
18
+ throw new Error('ShortKitPlayer must be used within a ShortKitProvider');
19
+ }
20
+
21
+ const serializedConfig = useMemo(() => {
22
+ const cfg = config ?? {};
23
+ return JSON.stringify({
24
+ cornerRadius: cfg.cornerRadius ?? 12,
25
+ clickAction: cfg.clickAction ?? 'feed',
26
+ autoplay: cfg.autoplay ?? true,
27
+ loop: cfg.loop ?? true,
28
+ muteOnStart: cfg.muteOnStart ?? true,
29
+ overlay: cfg.overlay
30
+ ? typeof cfg.overlay === 'string'
31
+ ? cfg.overlay
32
+ : { type: 'custom' }
33
+ : 'none',
34
+ });
35
+ }, [config]);
36
+
37
+ const serializedItem = useMemo(() => {
38
+ if (!contentItem) return undefined;
39
+ return JSON.stringify(contentItem);
40
+ }, [contentItem]);
41
+
42
+ return (
43
+ <View style={[styles.container, style]}>
44
+ <ShortKitPlayerView
45
+ style={styles.player}
46
+ config={serializedConfig}
47
+ contentItem={serializedItem}
48
+ active={active}
49
+ />
50
+ </View>
51
+ );
52
+ }
53
+
54
+ const styles = StyleSheet.create({
55
+ container: {
56
+ overflow: 'hidden',
57
+ },
58
+ player: {
59
+ flex: 1,
60
+ },
61
+ });