@shortkitsdk/react-native 0.2.0 → 0.2.2

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 (34) hide show
  1. package/android/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +48 -0
  2. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +48 -6
  3. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +180 -2
  4. package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +28 -1
  5. package/android/src/main/java/com/shortkit/reactnative/ShortKitPackage.kt +5 -1
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +136 -0
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerViewManager.kt +35 -0
  8. package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +133 -0
  9. package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetViewManager.kt +30 -0
  10. package/ios/ShortKitBridge.swift +134 -2
  11. package/ios/ShortKitCarouselOverlayBridge.swift +54 -0
  12. package/ios/ShortKitFeedView.swift +46 -7
  13. package/ios/ShortKitModule.mm +42 -0
  14. package/ios/ShortKitOverlayBridge.swift +23 -1
  15. package/ios/ShortKitPlayerNativeView.swift +186 -0
  16. package/ios/ShortKitPlayerNativeViewManager.mm +28 -0
  17. package/ios/ShortKitWidgetNativeView.swift +168 -0
  18. package/ios/ShortKitWidgetNativeViewManager.mm +27 -0
  19. package/package.json +1 -1
  20. package/src/CarouselOverlayManager.tsx +71 -0
  21. package/src/ShortKitContext.ts +18 -0
  22. package/src/ShortKitFeed.tsx +13 -0
  23. package/src/ShortKitPlayer.tsx +61 -0
  24. package/src/ShortKitProvider.tsx +161 -2
  25. package/src/ShortKitWidget.tsx +63 -0
  26. package/src/index.ts +15 -1
  27. package/src/serialization.ts +16 -2
  28. package/src/specs/NativeShortKitModule.ts +37 -0
  29. package/src/specs/ShortKitPlayerViewNativeComponent.ts +13 -0
  30. package/src/specs/ShortKitWidgetViewNativeComponent.ts +12 -0
  31. package/src/types.ts +82 -3
  32. package/src/useShortKit.ts +5 -1
  33. package/src/useShortKitCarousel.ts +29 -0
  34. package/src/useShortKitPlayer.ts +10 -2
@@ -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.2",
4
4
  "description": "ShortKit React Native SDK — short-form video feed",
5
5
  "react-native": "src/index",
6
6
  "source": "src/index",
@@ -0,0 +1,71 @@
1
+ import React, { useContext, useMemo } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import type { CarouselOverlayConfig } from './types';
4
+ import { ShortKitContext } from './ShortKitContext';
5
+ import type { ShortKitContextValue } from './ShortKitContext';
6
+
7
+ interface CarouselOverlayManagerProps {
8
+ carouselOverlay: CarouselOverlayConfig;
9
+ }
10
+
11
+ /**
12
+ * Internal component that renders TWO instances of the developer's custom
13
+ * carousel overlay component — one for the current cell and one for the next.
14
+ *
15
+ * Works identically to `OverlayManager` but for image carousel cells.
16
+ * The native side finds these views by their `nativeID` and applies
17
+ * scroll-tracking transforms so each overlay moves with its respective
18
+ * carousel cell during swipe transitions.
19
+ */
20
+ export function CarouselOverlayManager({ carouselOverlay }: CarouselOverlayManagerProps) {
21
+ if (
22
+ carouselOverlay === 'none' ||
23
+ typeof carouselOverlay === 'string' ||
24
+ carouselOverlay.type !== 'custom' ||
25
+ !('component' in carouselOverlay)
26
+ ) {
27
+ return null;
28
+ }
29
+
30
+ const CarouselComponent = carouselOverlay.component;
31
+
32
+ return (
33
+ <>
34
+ <View style={StyleSheet.absoluteFill} nativeID="carousel-overlay-current" pointerEvents="box-none">
35
+ <CarouselComponent />
36
+ </View>
37
+ <View style={StyleSheet.absoluteFill} nativeID="carousel-overlay-next" pointerEvents="box-none">
38
+ <NextCarouselOverlayProvider>
39
+ <CarouselComponent />
40
+ </NextCarouselOverlayProvider>
41
+ </View>
42
+ </>
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Wraps children with a modified ShortKitContext where `currentCarouselItem`
48
+ * is set to the provider's `nextCarouselItem`.
49
+ */
50
+ function NextCarouselOverlayProvider({ children }: { children: React.ReactNode }) {
51
+ const context = useContext(ShortKitContext);
52
+
53
+ const nextValue: ShortKitContextValue | null = useMemo(() => {
54
+ if (!context) return null;
55
+
56
+ return {
57
+ ...context,
58
+ currentCarouselItem: context.nextCarouselItem,
59
+ isCarouselActive: context.nextCarouselItem != null,
60
+ isCarouselTransitioning: false,
61
+ };
62
+ }, [context]);
63
+
64
+ if (!nextValue) return <>{children}</>;
65
+
66
+ return (
67
+ <ShortKitContext.Provider value={nextValue}>
68
+ {children}
69
+ </ShortKitContext.Provider>
70
+ );
71
+ }
@@ -1,11 +1,14 @@
1
1
  import { createContext } from 'react';
2
2
  import type {
3
3
  ContentItem,
4
+ ImageCarouselItem,
5
+ CustomFeedItem,
4
6
  PlayerTime,
5
7
  PlayerState,
6
8
  CaptionTrack,
7
9
  ContentSignal,
8
10
  OverlayConfig,
11
+ CarouselOverlayConfig,
9
12
  } from './types';
10
13
 
11
14
  export interface ShortKitContextValue {
@@ -20,6 +23,7 @@ export interface ShortKitContextValue {
20
23
  activeCaptionTrack: CaptionTrack | null;
21
24
  activeCue: { text: string; startTime: number; endTime: number } | null;
22
25
  prefetchedAheadCount: number;
26
+ remainingContentCount: number;
23
27
  isActive: boolean;
24
28
  isTransitioning: boolean;
25
29
  lastOverlayTap: number;
@@ -42,10 +46,24 @@ export interface ShortKitContextValue {
42
46
  // SDK operations
43
47
  setUserId: (id: string) => void;
44
48
  clearUserId: () => void;
49
+ setFeedItems: (items: CustomFeedItem[]) => void;
50
+ appendFeedItems: (items: CustomFeedItem[]) => void;
51
+ fetchContent: (limit?: number) => Promise<ContentItem[]>;
52
+
53
+ // Carousel overlay state
54
+ currentCarouselItem: ImageCarouselItem | null;
55
+ nextCarouselItem: ImageCarouselItem | null;
56
+ isCarouselActive: boolean;
57
+ isCarouselTransitioning: boolean;
58
+
59
+ // Active cell type — used by overlay managers to show/hide
60
+ activeCellType: 'video' | 'carousel' | null;
45
61
 
46
62
  // Internal — used by ShortKitFeed to render custom overlays
47
63
  /** @internal */
48
64
  _overlayConfig: OverlayConfig;
65
+ /** @internal */
66
+ _carouselOverlayConfig: CarouselOverlayConfig;
49
67
  }
50
68
 
51
69
  export const ShortKitContext = createContext<ShortKitContextValue | null>(null);
@@ -4,6 +4,7 @@ import type { ShortKitFeedProps } from './types';
4
4
  import ShortKitFeedView from './specs/ShortKitFeedViewNativeComponent';
5
5
  import NativeShortKitModule from './specs/NativeShortKitModule';
6
6
  import { OverlayManager } from './OverlayManager';
7
+ import { CarouselOverlayManager } from './CarouselOverlayManager';
7
8
  import { ShortKitContext } from './ShortKitContext';
8
9
  import { deserializeContentItem } from './serialization';
9
10
 
@@ -29,6 +30,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
29
30
  onLoop,
30
31
  onFeedTransition,
31
32
  onFormatChange,
33
+ onContentTapped,
32
34
  } = props;
33
35
 
34
36
  const context = useContext(ShortKitContext);
@@ -37,6 +39,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
37
39
  }
38
40
 
39
41
  const overlayConfig = context._overlayConfig;
42
+ const carouselOverlayConfig = context._carouselOverlayConfig;
40
43
 
41
44
  // ---------------------------------------------------------------------------
42
45
  // Subscribe to feed-level events and forward to callback props
@@ -109,6 +112,14 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
109
112
  );
110
113
  }
111
114
 
115
+ if (onContentTapped) {
116
+ subscriptions.push(
117
+ NativeShortKitModule.onContentTapped((event) => {
118
+ onContentTapped(event.contentId, event.index);
119
+ }),
120
+ );
121
+ }
122
+
112
123
  return () => {
113
124
  for (const sub of subscriptions) {
114
125
  sub.remove();
@@ -121,6 +132,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
121
132
  onLoop,
122
133
  onFeedTransition,
123
134
  onFormatChange,
135
+ onContentTapped,
124
136
  ]);
125
137
 
126
138
  // ---------------------------------------------------------------------------
@@ -133,6 +145,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
133
145
  config="{}"
134
146
  />
135
147
  <OverlayManager overlay={overlayConfig} />
148
+ <CarouselOverlayManager carouselOverlay={carouselOverlayConfig} />
136
149
  </View>
137
150
  );
138
151
  }
@@ -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
+ });