@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.
@@ -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.1.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,11 +29,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
29
29
  onLoop,
30
30
  onFeedTransition,
31
31
  onFormatChange,
32
- onArticleTapped,
33
- onCommentTapped,
34
- onOverlayShareTapped,
35
- onSaveTapped,
36
- onLikeTapped,
32
+ onContentTapped,
37
33
  } = props;
38
34
 
39
35
  const context = useContext(ShortKitContext);
@@ -114,47 +110,10 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
114
110
  );
115
111
  }
116
112
 
117
- if (onArticleTapped) {
113
+ if (onContentTapped) {
118
114
  subscriptions.push(
119
- NativeShortKitModule.onArticleTapped((event) => {
120
- const item = deserializeContentItem(event.item);
121
- if (item) onArticleTapped(item);
122
- }),
123
- );
124
- }
125
-
126
- if (onCommentTapped) {
127
- subscriptions.push(
128
- NativeShortKitModule.onCommentTapped((event) => {
129
- const item = deserializeContentItem(event.item);
130
- if (item) onCommentTapped(item);
131
- }),
132
- );
133
- }
134
-
135
- if (onOverlayShareTapped) {
136
- subscriptions.push(
137
- NativeShortKitModule.onOverlayShareTapped((event) => {
138
- const item = deserializeContentItem(event.item);
139
- if (item) onOverlayShareTapped(item);
140
- }),
141
- );
142
- }
143
-
144
- if (onSaveTapped) {
145
- subscriptions.push(
146
- NativeShortKitModule.onSaveTapped((event) => {
147
- const item = deserializeContentItem(event.item);
148
- if (item) onSaveTapped(item);
149
- }),
150
- );
151
- }
152
-
153
- if (onLikeTapped) {
154
- subscriptions.push(
155
- NativeShortKitModule.onLikeTapped((event) => {
156
- const item = deserializeContentItem(event.item);
157
- if (item) onLikeTapped(item);
115
+ NativeShortKitModule.onContentTapped((event) => {
116
+ onContentTapped(event.contentId, event.index);
158
117
  }),
159
118
  );
160
119
  }
@@ -171,11 +130,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
171
130
  onLoop,
172
131
  onFeedTransition,
173
132
  onFormatChange,
174
- onArticleTapped,
175
- onCommentTapped,
176
- onOverlayShareTapped,
177
- onSaveTapped,
178
- onLikeTapped,
133
+ onContentTapped,
179
134
  ]);
180
135
 
181
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
+ });