@shortkitsdk/react-native 0.2.11 → 0.2.12

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 (32) hide show
  1. package/android/build.gradle.kts +13 -1
  2. package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +115 -55
  3. package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +67 -56
  4. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +71 -26
  5. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +160 -35
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +5 -0
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +43 -10
  8. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +9 -0
  9. package/ios/ReactOverlayHost.swift +13 -27
  10. package/ios/ShortKitBridge.swift +36 -2
  11. package/ios/ShortKitFeedView.swift +24 -3
  12. package/ios/ShortKitModule.mm +4 -1
  13. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +720 -144
  14. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +19 -5
  15. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  16. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +19 -5
  17. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  18. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +720 -144
  19. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +19 -5
  20. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  21. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +19 -5
  22. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  23. package/package.json +1 -1
  24. package/src/ShortKitContext.ts +2 -1
  25. package/src/ShortKitFeed.tsx +14 -0
  26. package/src/ShortKitOverlaySurface.tsx +153 -45
  27. package/src/ShortKitPlayer.tsx +25 -3
  28. package/src/ShortKitProvider.tsx +4 -2
  29. package/src/index.ts +4 -0
  30. package/src/serialization.ts +1 -0
  31. package/src/specs/NativeShortKitModule.ts +18 -1
  32. package/src/types.ts +6 -0
@@ -169,6 +169,15 @@ public enum FeedHeight : Swift.Codable, Swift.Equatable, Swift.Sendable {
169
169
  public func encode(to encoder: any Swift.Encoder) throws
170
170
  public init(from decoder: any Swift.Decoder) throws
171
171
  }
172
+ public enum ScrollAxis : Swift.String, Swift.Codable, Swift.Equatable, Swift.Sendable {
173
+ case vertical
174
+ case horizontal
175
+ public init?(rawValue: Swift.String)
176
+ public typealias RawValue = Swift.String
177
+ public var rawValue: Swift.String {
178
+ get
179
+ }
180
+ }
172
181
  public enum FeedSource : Swift.String, Swift.Codable, Swift.Equatable, Swift.Sendable {
173
182
  case algorithmic
174
183
  case custom
@@ -191,6 +200,7 @@ public struct FeedFilter : Swift.Codable, Swift.Equatable, Swift.Sendable {
191
200
  }
192
201
  public struct FeedConfig : Swift.Codable {
193
202
  public var feedHeight: ShortKitSDK.FeedHeight
203
+ public var scrollAxis: ShortKitSDK.ScrollAxis
194
204
  public var videoOverlay: ShortKitSDK.VideoOverlayMode
195
205
  public var carouselOverlay: ShortKitSDK.CarouselOverlayMode
196
206
  public var surveyOverlay: ShortKitSDK.SurveyOverlayMode
@@ -201,7 +211,7 @@ public struct FeedConfig : Swift.Codable {
201
211
  public var coldStartEnabled: Swift.Bool
202
212
  public var filter: ShortKitSDK.FeedFilter?
203
213
  public var preload: ShortKitSDK.FeedPreload?
204
- public init(feedHeight: ShortKitSDK.FeedHeight = .fullscreen, videoOverlay: ShortKitSDK.VideoOverlayMode = .none, carouselOverlay: ShortKitSDK.CarouselOverlayMode = .none, surveyOverlay: ShortKitSDK.SurveyOverlayMode = .none, adOverlay: ShortKitSDK.AdOverlayMode = .none, muteOnStart: Swift.Bool = true, autoplay: Swift.Bool = true, feedSource: ShortKitSDK.FeedSource = .algorithmic, coldStartEnabled: Swift.Bool = false, filter: ShortKitSDK.FeedFilter? = nil, preload: ShortKitSDK.FeedPreload? = nil)
214
+ public init(feedHeight: ShortKitSDK.FeedHeight = .fullscreen, scrollAxis: ShortKitSDK.ScrollAxis = .vertical, videoOverlay: ShortKitSDK.VideoOverlayMode = .none, carouselOverlay: ShortKitSDK.CarouselOverlayMode = .none, surveyOverlay: ShortKitSDK.SurveyOverlayMode = .none, adOverlay: ShortKitSDK.AdOverlayMode = .none, muteOnStart: Swift.Bool = true, autoplay: Swift.Bool = true, feedSource: ShortKitSDK.FeedSource = .algorithmic, coldStartEnabled: Swift.Bool = false, filter: ShortKitSDK.FeedFilter? = nil, preload: ShortKitSDK.FeedPreload? = nil)
205
215
  public func encode(to encoder: any Swift.Encoder) throws
206
216
  public init(from decoder: any Swift.Decoder) throws
207
217
  }
@@ -266,7 +276,7 @@ extension ShortKitSDK.ShortKitDelegate {
266
276
  @objc deinit
267
277
  }
268
278
  @_Concurrency.MainActor @preconcurrency public struct ShortKitFeedView : SwiftUI.UIViewControllerRepresentable {
269
- @_Concurrency.MainActor @preconcurrency public init(shortKit: ShortKitSDK.ShortKit, config: ShortKitSDK.FeedConfig)
279
+ @_Concurrency.MainActor @preconcurrency public init(shortKit: ShortKitSDK.ShortKit, config: ShortKitSDK.FeedConfig = FeedConfig(), onFeedReady: (() -> Swift.Void)? = nil)
270
280
  @_Concurrency.MainActor @preconcurrency public func makeCoordinator() -> ShortKitSDK.ShortKitFeedView.Coordinator
271
281
  @_Concurrency.MainActor @preconcurrency public func makeUIViewController(context: ShortKitSDK.ShortKitFeedView.Context) -> ShortKitSDK.ShortKitFeedViewController
272
282
  @_Concurrency.MainActor @preconcurrency public func updateUIViewController(_ uiViewController: ShortKitSDK.ShortKitFeedViewController, context: ShortKitSDK.ShortKitFeedView.Context)
@@ -280,6 +290,7 @@ extension ShortKitSDK.ShortKitDelegate {
280
290
  @objc @_hasMissingDesignatedInitializers @_Concurrency.MainActor @preconcurrency public class ShortKitFeedViewController : UIKit.UIViewController {
281
291
  @_Concurrency.MainActor @preconcurrency public var onDismiss: (() -> Swift.Void)?
282
292
  @_Concurrency.MainActor @preconcurrency public var onRemainingContentCountChange: ((Swift.Int) -> Swift.Void)?
293
+ @_Concurrency.MainActor @preconcurrency public var onFeedReady: (() -> Swift.Void)?
283
294
  @_Concurrency.MainActor @preconcurrency public init(shortKit: ShortKitSDK.ShortKit, config: ShortKitSDK.FeedConfig, startAtItemId: Swift.String? = nil)
284
295
  @_Concurrency.MainActor public func setFeedItems(_ items: [ShortKitSDK.FeedInput])
285
296
  @_Concurrency.MainActor public func appendFeedItems(_ items: [ShortKitSDK.FeedInput])
@@ -289,6 +300,9 @@ extension ShortKitSDK.ShortKitDelegate {
289
300
  @_Concurrency.MainActor @preconcurrency @objc override dynamic public func viewWillAppear(_ animated: Swift.Bool)
290
301
  @_Concurrency.MainActor @preconcurrency @objc override dynamic public func viewDidAppear(_ animated: Swift.Bool)
291
302
  @_Concurrency.MainActor @preconcurrency @objc override dynamic public func viewWillDisappear(_ animated: Swift.Bool)
303
+ @_Concurrency.MainActor @preconcurrency public func setBridgeManaged()
304
+ @_Concurrency.MainActor @preconcurrency public func activate()
305
+ @_Concurrency.MainActor @preconcurrency public func deactivate()
292
306
  @_Concurrency.MainActor @preconcurrency @objc override dynamic public func viewDidDisappear(_ animated: Swift.Bool)
293
307
  @_Concurrency.MainActor @preconcurrency @objc override dynamic public var supportedInterfaceOrientations: UIKit.UIInterfaceOrientationMask {
294
308
  @objc get
@@ -689,9 +703,6 @@ public enum ContentSignal : Swift.Equatable, Swift.Sendable {
689
703
  final public var remainingContentCount: Combine.AnyPublisher<Swift.Int, Swift.Never> {
690
704
  get
691
705
  }
692
- final public var feedReady: Combine.AnyPublisher<Swift.Void, Swift.Never> {
693
- get
694
- }
695
706
  final public var currentItemValue: ShortKitSDK.ContentItem? {
696
707
  get
697
708
  }
@@ -754,6 +765,7 @@ final public class ShortKit {
754
765
  final public var loadingViewProvider: (() -> UIKit.UIView)?
755
766
  public init(apiKey: Swift.String, userId: Swift.String? = nil, adProvider: (any ShortKitSDK.ShortKitAdProvider)? = nil, clientAppName: Swift.String? = nil, clientAppVersion: Swift.String? = nil, customDimensions: [Swift.String : Swift.String]? = nil, loadingViewProvider: (() -> UIKit.UIView)? = nil)
756
767
  final public func preloadFeed(filter: ShortKitSDK.FeedFilter? = nil, limit: Swift.Int = 10) -> ShortKitSDK.FeedPreload
768
+ final public func preloadFeed(items: [ShortKitSDK.FeedInput]) -> ShortKitSDK.FeedPreload
757
769
  final public func fetchContent(limit: Swift.Int = 10, filter: ShortKitSDK.FeedFilter? = nil) async throws -> [ShortKitSDK.ContentItem]
758
770
  final public func setUserId(_ id: Swift.String)
759
771
  final public func clearUserId()
@@ -835,6 +847,8 @@ public struct WidgetConfig {
835
847
  }
836
848
  extension ShortKitSDK.AdQuartile : Swift.Equatable {}
837
849
  extension ShortKitSDK.AdQuartile : Swift.Hashable {}
850
+ extension ShortKitSDK.ScrollAxis : Swift.Hashable {}
851
+ extension ShortKitSDK.ScrollAxis : Swift.RawRepresentable {}
838
852
  extension ShortKitSDK.FeedSource : Swift.Hashable {}
839
853
  extension ShortKitSDK.FeedSource : Swift.RawRepresentable {}
840
854
  extension ShortKitSDK.ShortKitFeedView : Swift.Sendable {}
@@ -169,6 +169,15 @@ public enum FeedHeight : Swift.Codable, Swift.Equatable, Swift.Sendable {
169
169
  public func encode(to encoder: any Swift.Encoder) throws
170
170
  public init(from decoder: any Swift.Decoder) throws
171
171
  }
172
+ public enum ScrollAxis : Swift.String, Swift.Codable, Swift.Equatable, Swift.Sendable {
173
+ case vertical
174
+ case horizontal
175
+ public init?(rawValue: Swift.String)
176
+ public typealias RawValue = Swift.String
177
+ public var rawValue: Swift.String {
178
+ get
179
+ }
180
+ }
172
181
  public enum FeedSource : Swift.String, Swift.Codable, Swift.Equatable, Swift.Sendable {
173
182
  case algorithmic
174
183
  case custom
@@ -191,6 +200,7 @@ public struct FeedFilter : Swift.Codable, Swift.Equatable, Swift.Sendable {
191
200
  }
192
201
  public struct FeedConfig : Swift.Codable {
193
202
  public var feedHeight: ShortKitSDK.FeedHeight
203
+ public var scrollAxis: ShortKitSDK.ScrollAxis
194
204
  public var videoOverlay: ShortKitSDK.VideoOverlayMode
195
205
  public var carouselOverlay: ShortKitSDK.CarouselOverlayMode
196
206
  public var surveyOverlay: ShortKitSDK.SurveyOverlayMode
@@ -201,7 +211,7 @@ public struct FeedConfig : Swift.Codable {
201
211
  public var coldStartEnabled: Swift.Bool
202
212
  public var filter: ShortKitSDK.FeedFilter?
203
213
  public var preload: ShortKitSDK.FeedPreload?
204
- public init(feedHeight: ShortKitSDK.FeedHeight = .fullscreen, videoOverlay: ShortKitSDK.VideoOverlayMode = .none, carouselOverlay: ShortKitSDK.CarouselOverlayMode = .none, surveyOverlay: ShortKitSDK.SurveyOverlayMode = .none, adOverlay: ShortKitSDK.AdOverlayMode = .none, muteOnStart: Swift.Bool = true, autoplay: Swift.Bool = true, feedSource: ShortKitSDK.FeedSource = .algorithmic, coldStartEnabled: Swift.Bool = false, filter: ShortKitSDK.FeedFilter? = nil, preload: ShortKitSDK.FeedPreload? = nil)
214
+ public init(feedHeight: ShortKitSDK.FeedHeight = .fullscreen, scrollAxis: ShortKitSDK.ScrollAxis = .vertical, videoOverlay: ShortKitSDK.VideoOverlayMode = .none, carouselOverlay: ShortKitSDK.CarouselOverlayMode = .none, surveyOverlay: ShortKitSDK.SurveyOverlayMode = .none, adOverlay: ShortKitSDK.AdOverlayMode = .none, muteOnStart: Swift.Bool = true, autoplay: Swift.Bool = true, feedSource: ShortKitSDK.FeedSource = .algorithmic, coldStartEnabled: Swift.Bool = false, filter: ShortKitSDK.FeedFilter? = nil, preload: ShortKitSDK.FeedPreload? = nil)
205
215
  public func encode(to encoder: any Swift.Encoder) throws
206
216
  public init(from decoder: any Swift.Decoder) throws
207
217
  }
@@ -266,7 +276,7 @@ extension ShortKitSDK.ShortKitDelegate {
266
276
  @objc deinit
267
277
  }
268
278
  @_Concurrency.MainActor @preconcurrency public struct ShortKitFeedView : SwiftUI.UIViewControllerRepresentable {
269
- @_Concurrency.MainActor @preconcurrency public init(shortKit: ShortKitSDK.ShortKit, config: ShortKitSDK.FeedConfig)
279
+ @_Concurrency.MainActor @preconcurrency public init(shortKit: ShortKitSDK.ShortKit, config: ShortKitSDK.FeedConfig = FeedConfig(), onFeedReady: (() -> Swift.Void)? = nil)
270
280
  @_Concurrency.MainActor @preconcurrency public func makeCoordinator() -> ShortKitSDK.ShortKitFeedView.Coordinator
271
281
  @_Concurrency.MainActor @preconcurrency public func makeUIViewController(context: ShortKitSDK.ShortKitFeedView.Context) -> ShortKitSDK.ShortKitFeedViewController
272
282
  @_Concurrency.MainActor @preconcurrency public func updateUIViewController(_ uiViewController: ShortKitSDK.ShortKitFeedViewController, context: ShortKitSDK.ShortKitFeedView.Context)
@@ -280,6 +290,7 @@ extension ShortKitSDK.ShortKitDelegate {
280
290
  @objc @_hasMissingDesignatedInitializers @_Concurrency.MainActor @preconcurrency public class ShortKitFeedViewController : UIKit.UIViewController {
281
291
  @_Concurrency.MainActor @preconcurrency public var onDismiss: (() -> Swift.Void)?
282
292
  @_Concurrency.MainActor @preconcurrency public var onRemainingContentCountChange: ((Swift.Int) -> Swift.Void)?
293
+ @_Concurrency.MainActor @preconcurrency public var onFeedReady: (() -> Swift.Void)?
283
294
  @_Concurrency.MainActor @preconcurrency public init(shortKit: ShortKitSDK.ShortKit, config: ShortKitSDK.FeedConfig, startAtItemId: Swift.String? = nil)
284
295
  @_Concurrency.MainActor public func setFeedItems(_ items: [ShortKitSDK.FeedInput])
285
296
  @_Concurrency.MainActor public func appendFeedItems(_ items: [ShortKitSDK.FeedInput])
@@ -289,6 +300,9 @@ extension ShortKitSDK.ShortKitDelegate {
289
300
  @_Concurrency.MainActor @preconcurrency @objc override dynamic public func viewWillAppear(_ animated: Swift.Bool)
290
301
  @_Concurrency.MainActor @preconcurrency @objc override dynamic public func viewDidAppear(_ animated: Swift.Bool)
291
302
  @_Concurrency.MainActor @preconcurrency @objc override dynamic public func viewWillDisappear(_ animated: Swift.Bool)
303
+ @_Concurrency.MainActor @preconcurrency public func setBridgeManaged()
304
+ @_Concurrency.MainActor @preconcurrency public func activate()
305
+ @_Concurrency.MainActor @preconcurrency public func deactivate()
292
306
  @_Concurrency.MainActor @preconcurrency @objc override dynamic public func viewDidDisappear(_ animated: Swift.Bool)
293
307
  @_Concurrency.MainActor @preconcurrency @objc override dynamic public var supportedInterfaceOrientations: UIKit.UIInterfaceOrientationMask {
294
308
  @objc get
@@ -689,9 +703,6 @@ public enum ContentSignal : Swift.Equatable, Swift.Sendable {
689
703
  final public var remainingContentCount: Combine.AnyPublisher<Swift.Int, Swift.Never> {
690
704
  get
691
705
  }
692
- final public var feedReady: Combine.AnyPublisher<Swift.Void, Swift.Never> {
693
- get
694
- }
695
706
  final public var currentItemValue: ShortKitSDK.ContentItem? {
696
707
  get
697
708
  }
@@ -754,6 +765,7 @@ final public class ShortKit {
754
765
  final public var loadingViewProvider: (() -> UIKit.UIView)?
755
766
  public init(apiKey: Swift.String, userId: Swift.String? = nil, adProvider: (any ShortKitSDK.ShortKitAdProvider)? = nil, clientAppName: Swift.String? = nil, clientAppVersion: Swift.String? = nil, customDimensions: [Swift.String : Swift.String]? = nil, loadingViewProvider: (() -> UIKit.UIView)? = nil)
756
767
  final public func preloadFeed(filter: ShortKitSDK.FeedFilter? = nil, limit: Swift.Int = 10) -> ShortKitSDK.FeedPreload
768
+ final public func preloadFeed(items: [ShortKitSDK.FeedInput]) -> ShortKitSDK.FeedPreload
757
769
  final public func fetchContent(limit: Swift.Int = 10, filter: ShortKitSDK.FeedFilter? = nil) async throws -> [ShortKitSDK.ContentItem]
758
770
  final public func setUserId(_ id: Swift.String)
759
771
  final public func clearUserId()
@@ -835,6 +847,8 @@ public struct WidgetConfig {
835
847
  }
836
848
  extension ShortKitSDK.AdQuartile : Swift.Equatable {}
837
849
  extension ShortKitSDK.AdQuartile : Swift.Hashable {}
850
+ extension ShortKitSDK.ScrollAxis : Swift.Hashable {}
851
+ extension ShortKitSDK.ScrollAxis : Swift.RawRepresentable {}
838
852
  extension ShortKitSDK.FeedSource : Swift.Hashable {}
839
853
  extension ShortKitSDK.FeedSource : Swift.RawRepresentable {}
840
854
  extension ShortKitSDK.ShortKitFeedView : Swift.Sendable {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shortkitsdk/react-native",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "ShortKit React Native SDK — short-form video feed",
5
5
  "react-native": "src/index",
6
6
  "source": "src/index",
@@ -3,6 +3,7 @@ import type {
3
3
  ContentItem,
4
4
  FeedConfig,
5
5
  FeedFilter,
6
+ FeedInput,
6
7
  PlayerTime,
7
8
  PlayerState,
8
9
  CaptionTrack,
@@ -45,7 +46,7 @@ export interface ShortKitContextValue {
45
46
  fetchContent: (limit?: number, filter?: FeedFilter) => Promise<ContentItem[]>;
46
47
 
47
48
  // Preload
48
- preloadFeed: (config?: Partial<FeedConfig>) => Promise<string>;
49
+ preloadFeed: (config?: Partial<FeedConfig>, items?: FeedInput[]) => Promise<string>;
49
50
 
50
51
  // Storyboard / seek thumbnails
51
52
  prefetchStoryboard: (playbackId: string) => void;
@@ -41,6 +41,7 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
41
41
  onRefreshRequested,
42
42
  onDidFetchContentItems,
43
43
  onRemainingContentCountChange,
44
+ onFeedReady,
44
45
  } = props;
45
46
 
46
47
  const context = useContext(ShortKitContext);
@@ -79,6 +80,19 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
79
80
  return () => subscription.remove();
80
81
  }, [feedId, onRemainingContentCountChange]);
81
82
 
83
+ // Subscribe to per-feed ready event
84
+ useEffect(() => {
85
+ if (!NativeShortKitModule || !onFeedReady) return;
86
+
87
+ const subscription = NativeShortKitModule.onFeedReady((event) => {
88
+ if (event.feedId === feedId) {
89
+ onFeedReady();
90
+ }
91
+ });
92
+
93
+ return () => subscription.remove();
94
+ }, [feedId, onFeedReady]);
95
+
82
96
  // Register overlay components before native view mounts.
83
97
  // useLayoutEffect fires after commit but before paint — before
84
98
  // didMoveToWindow/onAttachedToWindow on the native view.
@@ -1,9 +1,11 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
2
- import { AppRegistry, DeviceEventEmitter, Platform } from 'react-native';
1
+ import React, { useEffect, useState } from 'react';
2
+ import { AppRegistry, View, Text } from 'react-native';
3
3
  import type { OverlayProps, PlayerState, PlayerTime, FeedScrollPhase } from './types';
4
4
  import { deserializeContentItem } from './serialization';
5
5
  import NativeShortKitModule from './specs/NativeShortKitModule';
6
6
 
7
+ const SK_OVERLAY_TAG = '[ShortKit:OverlaySurface]';
8
+
7
9
  // Named registry — supports different overlay components per feed
8
10
  const _overlayRegistry = new Map<string, React.ComponentType<OverlayProps>>();
9
11
 
@@ -16,7 +18,9 @@ export function registerOverlayComponent(
16
18
  name: string,
17
19
  component: React.ComponentType<OverlayProps>,
18
20
  ) {
19
- if (_overlayRegistry.has(name)) return;
21
+ if (_overlayRegistry.has(name)) {
22
+ return;
23
+ }
20
24
  _overlayRegistry.set(name, component);
21
25
 
22
26
  const moduleName = `ShortKitOverlay_${name}`;
@@ -27,6 +31,47 @@ export function registerOverlayComponent(
27
31
  });
28
32
  }
29
33
 
34
+ /**
35
+ * Error boundary that catches crashes inside the overlay component.
36
+ * Without this, a crash in an isolated React surface is silent on Android.
37
+ */
38
+ class OverlayErrorBoundary extends React.Component<
39
+ { surfaceId?: string; overlayName: string; children: React.ReactNode },
40
+ { error: Error | null }
41
+ > {
42
+ state: { error: Error | null } = { error: null };
43
+
44
+ static getDerivedStateFromError(error: Error) {
45
+ return { error };
46
+ }
47
+
48
+ componentDidCatch(error: Error, info: React.ErrorInfo) {
49
+ console.error(
50
+ `${SK_OVERLAY_TAG} CRASH in overlay '${this.props.overlayName}' (surfaceId=${this.props.surfaceId}):`,
51
+ error.message,
52
+ '\nComponent stack:',
53
+ info.componentStack,
54
+ );
55
+ }
56
+
57
+ render() {
58
+ if (this.state.error) {
59
+ // Render a visible red indicator in __DEV__ so it's obvious
60
+ if (__DEV__) {
61
+ return (
62
+ <View style={{ flex: 1, backgroundColor: 'rgba(255,0,0,0.3)', justifyContent: 'center', alignItems: 'center', padding: 20 }}>
63
+ <Text style={{ color: 'white', fontSize: 14, textAlign: 'center' }}>
64
+ {`Overlay '${this.props.overlayName}' crashed:\n${this.state.error.message}`}
65
+ </Text>
66
+ </View>
67
+ );
68
+ }
69
+ return null;
70
+ }
71
+ return this.props.children;
72
+ }
73
+ }
74
+
30
75
  /** Raw props received from native appProperties (set once per item in configure()). */
31
76
  interface RawOverlaySurfaceProps {
32
77
  surfaceId?: string;
@@ -47,14 +92,9 @@ interface InnerProps extends RawOverlaySurfaceProps {
47
92
  /**
48
93
  * Subscribe to a native overlay event, filtered by surfaceId.
49
94
  *
50
- * iOS and Android new arch emit events through different paths:
51
- * - iOS: _eventEmitterCallback (codegen path) only the codegen EventEmitter
52
- * subscriptions (NativeShortKitModule.onXxx) receive these events.
53
- * - Android: getJSModule(RCTDeviceEventEmitter).emit() — only
54
- * DeviceEventEmitter.addListener() receives these events (codegen
55
- * EventEmitter subscriptions do NOT receive them on Android new arch).
56
- *
57
- * We branch per platform to use the path that actually delivers events.
95
+ * Both iOS and Android (new arch) emit events through the codegen
96
+ * EventEmitter path (mEventEmitterCallback / JSI direct channel).
97
+ * We subscribe via the codegen emitter method on the TurboModule.
58
98
  */
59
99
  function useOverlayEvent<T extends { surfaceId: string }>(
60
100
  eventName: string,
@@ -66,19 +106,11 @@ function useOverlayEvent<T extends { surfaceId: string }>(
66
106
 
67
107
  let sub: { remove: () => void } | undefined;
68
108
 
69
- if (Platform.OS === 'ios') {
70
- // iOS new arch: events come through the codegen EventEmitterCallback.
71
- // Access the codegen emitter method by name on the TurboModule.
72
- const emitter = NativeShortKitModule?.[eventName as keyof typeof NativeShortKitModule];
73
- if (typeof emitter === 'function') {
74
- sub = (emitter as (cb: (e: T) => void) => { remove: () => void })((e: T) => {
75
- if (e.surfaceId !== surfaceId) return;
76
- handler(e);
77
- });
78
- }
79
- } else {
80
- // Android new arch: events come through RCTDeviceEventEmitter.emit().
81
- sub = DeviceEventEmitter.addListener(eventName, (e: T) => {
109
+ // Both platforms: events come through the codegen EventEmitterCallback.
110
+ // Access the codegen emitter method by name on the TurboModule.
111
+ const emitter = NativeShortKitModule?.[eventName as keyof typeof NativeShortKitModule];
112
+ if (typeof emitter === 'function') {
113
+ sub = (emitter as (cb: (e: T) => void) => { remove: () => void })((e: T) => {
82
114
  if (e.surfaceId !== surfaceId) return;
83
115
  handler(e);
84
116
  });
@@ -90,13 +122,24 @@ function useOverlayEvent<T extends { surfaceId: string }>(
90
122
 
91
123
  function ShortKitOverlaySurfaceInner(props: InnerProps) {
92
124
  const Component = _overlayRegistry.get(props.overlayName);
93
- if (!Component) return null;
125
+ if (!Component) {
126
+ console.error(`${SK_OVERLAY_TAG} ShortKitOverlaySurfaceInner: Component NOT FOUND in registry for '${props.overlayName}'. Registered names: [${Array.from(_overlayRegistry.keys()).join(', ')}]`);
127
+ return null;
128
+ }
94
129
 
95
- const item = useMemo(
96
- () => (props.item ? deserializeContentItem(props.item) : null),
97
- [props.item],
130
+ const [item, setItem] = useState(() =>
131
+ props.item ? deserializeContentItem(props.item) : null,
98
132
  );
99
133
 
134
+ // Sync item from props when surface is first mounted or remounted via updateInitProps
135
+ const prevPropsItemRef = React.useRef(props.item);
136
+ if (props.item !== prevPropsItemRef.current) {
137
+ prevPropsItemRef.current = props.item;
138
+ // Synchronous state update during render — React restarts with new state
139
+ // before committing, so stale frame is never painted.
140
+ setItem(props.item ? deserializeContentItem(props.item) : null);
141
+ }
142
+
100
143
  const sid = props.surfaceId;
101
144
 
102
145
  // Initialize state from surface properties (set once in configure()).
@@ -141,6 +184,24 @@ function ShortKitOverlaySurfaceInner(props: InnerProps) {
141
184
 
142
185
  // --- Event subscriptions (filtered by surfaceId) ---
143
186
 
187
+ // Item changed via event (not updateInitProps) — triggers React DIFF
188
+ // instead of full remount. Uses DeviceEventEmitter because this event
189
+ // is not in the TurboModule spec (avoids native codegen changes).
190
+ useEffect(() => {
191
+ if (!sid) return;
192
+ const { DeviceEventEmitter } = require('react-native');
193
+ const sub = DeviceEventEmitter.addListener('onOverlayItemChanged', (e: { surfaceId: string; item: string }) => {
194
+ if (e.surfaceId !== sid) return;
195
+ const newItem = e.item ? deserializeContentItem(e.item) : null;
196
+ if (newItem) setItem(newItem);
197
+ // Reset playback state for the new item
198
+ setIsActive(false);
199
+ setPlayerState('idle' as PlayerState);
200
+ setTime({ current: 0, duration: 0, buffered: 0 });
201
+ });
202
+ return () => sub.remove();
203
+ }, [sid]);
204
+
144
205
  useOverlayEvent<{ surfaceId: string; isActive: boolean }>(
145
206
  'onOverlayActiveChanged', sid,
146
207
  (e) => setIsActive(e.isActive),
@@ -175,31 +236,78 @@ function ShortKitOverlaySurfaceInner(props: InnerProps) {
175
236
  'onOverlayFeedScrollPhaseChanged', sid,
176
237
  (e) => {
177
238
  try {
178
- setFeedScrollPhase(e.feedScrollPhase ? JSON.parse(e.feedScrollPhase) : null);
239
+ const next = e.feedScrollPhase ? JSON.parse(e.feedScrollPhase) : null;
240
+ // Stabilize object reference: only update state if the phase actually
241
+ // changed. Prevents unnecessary re-renders of memo'd overlay components
242
+ // that receive feedScrollPhase as a prop (new JSON.parse object !== old).
243
+ setFeedScrollPhase(prev => {
244
+ if (prev?.phase === next?.phase && prev?.fromId === next?.fromId) {
245
+ return prev;
246
+ }
247
+ return next;
248
+ });
179
249
  } catch {
180
250
  setFeedScrollPhase(null);
181
251
  }
182
252
  },
183
253
  );
184
254
 
185
- useOverlayEvent<{ surfaceId: string; current: number; duration: number; buffered: number }>(
186
- 'onOverlayTimeUpdate', sid,
187
- (e) => setTime({ current: e.current, duration: e.duration, buffered: e.buffered }),
188
- );
255
+ // EXPERIMENT: time updates disabled to measure impact on swipe perf
256
+ // useOverlayEvent<{ surfaceId: string; current: number; duration: number; buffered: number }>(
257
+ // 'onOverlayTimeUpdate', sid,
258
+ // (e) => setTime({ current: e.current, duration: e.duration, buffered: e.buffered }),
259
+ // );
260
+
261
+ // Batched full-state sync — replaces 7 individual events on swipe settle.
262
+ // All setState calls within one handler = one React render (auto-batched).
263
+ useOverlayEvent<{
264
+ surfaceId: string;
265
+ isActive: boolean;
266
+ playerState: string;
267
+ isMuted: boolean;
268
+ playbackRate: number;
269
+ captionsEnabled: boolean;
270
+ activeCue: string | null;
271
+ feedScrollPhase: string | null;
272
+ }>('onOverlayFullState', sid, (e) => {
273
+ setIsActive(e.isActive);
274
+ setPlayerState(e.playerState as PlayerState);
275
+ setIsMuted(e.isMuted);
276
+ setPlaybackRate(e.playbackRate);
277
+ setCaptionsEnabled(e.captionsEnabled);
278
+ try {
279
+ setActiveCue(e.activeCue ? JSON.parse(e.activeCue) : null);
280
+ } catch {
281
+ setActiveCue(null);
282
+ }
283
+ try {
284
+ const next = e.feedScrollPhase ? JSON.parse(e.feedScrollPhase) : null;
285
+ setFeedScrollPhase(prev => {
286
+ if (prev?.phase === next?.phase && prev?.fromId === next?.fromId) return prev;
287
+ return next;
288
+ });
289
+ } catch {
290
+ setFeedScrollPhase(null);
291
+ }
292
+ });
189
293
 
190
- if (!item) return null;
294
+ if (!item) {
295
+ return null;
296
+ }
191
297
 
192
298
  return (
193
- <Component
194
- item={item}
195
- isActive={isActive}
196
- playerState={playerState}
197
- time={time}
198
- isMuted={isMuted}
199
- playbackRate={playbackRate}
200
- captionsEnabled={captionsEnabled}
201
- activeCue={activeCue}
202
- feedScrollPhase={feedScrollPhase}
203
- />
299
+ <OverlayErrorBoundary surfaceId={sid} overlayName={props.overlayName}>
300
+ <Component
301
+ item={item}
302
+ isActive={isActive}
303
+ playerState={playerState}
304
+ time={time}
305
+ isMuted={isMuted}
306
+ playbackRate={playbackRate}
307
+ captionsEnabled={captionsEnabled}
308
+ activeCue={activeCue}
309
+ feedScrollPhase={feedScrollPhase}
310
+ />
311
+ </OverlayErrorBoundary>
204
312
  );
205
313
  }
@@ -17,11 +17,13 @@ import ShortKitPlayerView from './specs/ShortKitPlayerViewNativeComponent';
17
17
  export function ShortKitPlayer(props: ShortKitPlayerProps) {
18
18
  const { config, contentItem, active, style } = props;
19
19
 
20
+ const clickAction = config?.clickAction ?? 'feed';
21
+
20
22
  const serializedConfig = useMemo(() => {
21
23
  const cfg = config ?? {};
22
24
  return JSON.stringify({
23
25
  cornerRadius: cfg.cornerRadius ?? 12,
24
- clickAction: cfg.clickAction ?? 'feed',
26
+ clickAction: clickAction,
25
27
  autoplay: cfg.autoplay ?? true,
26
28
  loop: cfg.loop ?? true,
27
29
  muteOnStart: cfg.muteOnStart ?? true,
@@ -31,20 +33,40 @@ export function ShortKitPlayer(props: ShortKitPlayerProps) {
31
33
  : { type: 'custom' }
32
34
  : 'none',
33
35
  });
34
- }, [config]);
36
+ }, [config, clickAction]);
35
37
 
36
38
  const serializedItem = useMemo(() => {
37
39
  if (!contentItem) return undefined;
38
40
  return JSON.stringify(contentItem);
39
41
  }, [contentItem]);
40
42
 
43
+ // When clickAction is "none", the native view should not participate in
44
+ // Fabric's JS-side hit testing. Without this, Fabric on Android sees the
45
+ // native component as the touch target and never routes the touch to a
46
+ // parent Pressable — even though the native view returns false from
47
+ // dispatchTouchEvent. iOS doesn't need this because UIKit's responder
48
+ // chain bubbles unhandled touches automatically.
49
+ //
50
+ // "box-none" on the container means the container View itself is
51
+ // transparent to touches (passes through to parent), but children can
52
+ // still receive touches if needed. "none" on the native view means it
53
+ // is fully invisible to hit testing.
54
+ const nativeTouchPassthrough = clickAction === 'none';
55
+
41
56
  return (
42
- <View style={[styles.container, style]}>
57
+ <View
58
+ style={[styles.container, style]}
59
+ pointerEvents={nativeTouchPassthrough ? 'box-none' : 'auto'}
60
+ onStartShouldSetResponder={() => {
61
+ return false;
62
+ }}
63
+ >
43
64
  <ShortKitPlayerView
44
65
  style={styles.player}
45
66
  config={serializedConfig}
46
67
  contentItem={serializedItem}
47
68
  active={active}
69
+ pointerEvents={nativeTouchPassthrough ? 'none' : 'auto'}
48
70
  />
49
71
  </View>
50
72
  );
@@ -8,6 +8,7 @@ import type {
8
8
  ContentItem,
9
9
  FeedConfig,
10
10
  FeedFilter,
11
+ FeedInput,
11
12
  PlayerTime,
12
13
  PlayerState,
13
14
  CaptionTrack,
@@ -420,10 +421,11 @@ export function ShortKitProvider({
420
421
  }
421
422
  }, []);
422
423
 
423
- const preloadFeedCmd = useCallback(async (config?: Partial<FeedConfig>): Promise<string> => {
424
+ const preloadFeedCmd = useCallback(async (config?: Partial<FeedConfig>, items?: FeedInput[]): Promise<string> => {
424
425
  if (!NativeShortKitModule) return '';
425
426
  const configJSON = config ? serializeFeedConfig(config as FeedConfig) : '{}';
426
- return NativeShortKitModule.preloadFeed(configJSON);
427
+ const itemsJSON = items ? JSON.stringify(items) : null;
428
+ return NativeShortKitModule.preloadFeed(configJSON, itemsJSON);
427
429
  }, []);
428
430
 
429
431
  // -----------------------------------------------------------------------
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export { useShortKit } from './useShortKit';
7
7
  export type {
8
8
  FeedConfig,
9
9
  FeedHeight,
10
+ ScrollAxis,
10
11
  FeedSource,
11
12
  OverlayConfig,
12
13
  CarouselOverlayConfig,
@@ -42,4 +43,7 @@ export type {
42
43
  StoryboardTile,
43
44
  } from './types';
44
45
  export { ShortKitCommands } from './ShortKitCommands';
46
+ export { default as NativeShortKitModule } from './specs/NativeShortKitModule';
47
+ export { registerOverlayComponent } from './ShortKitOverlaySurface';
48
+ export { registerCarouselOverlayComponent } from './ShortKitCarouselOverlaySurface';
45
49
  export type { OverlayProps, CarouselOverlayProps } from './types';
@@ -27,6 +27,7 @@ export function serializeFeedConfig(config: FeedConfig): string {
27
27
 
28
28
  return JSON.stringify({
29
29
  feedHeight: JSON.stringify(config.feedHeight ?? { type: 'fullscreen' }),
30
+ scrollAxis: config.scrollAxis ?? 'vertical',
30
31
  overlay,
31
32
  carouselOverlay,
32
33
  surveyMode: JSON.stringify(config.surveyMode ?? 'none'),
@@ -108,6 +108,10 @@ type ContentTappedEvent = Readonly<{
108
108
  index: Int32;
109
109
  }>;
110
110
 
111
+ type FeedReadyEvent = Readonly<{
112
+ feedId: string;
113
+ }>;
114
+
111
115
  // --- Overlay per-surface event payload types ---
112
116
 
113
117
  type OverlayActiveEvent = Readonly<{
@@ -152,6 +156,17 @@ type OverlayTimeUpdateEvent = Readonly<{
152
156
  buffered: Double;
153
157
  }>;
154
158
 
159
+ type OverlayFullStateEvent = Readonly<{
160
+ surfaceId: string;
161
+ isActive: boolean;
162
+ playerState: string;
163
+ isMuted: boolean;
164
+ playbackRate: Double;
165
+ captionsEnabled: boolean;
166
+ activeCue: string;
167
+ feedScrollPhase: string;
168
+ }>;
169
+
155
170
  export interface Spec extends TurboModule {
156
171
  // --- Lifecycle ---
157
172
  initialize(
@@ -186,7 +201,7 @@ export interface Spec extends TurboModule {
186
201
  appendFeedItems(feedId: string, items: string): void;
187
202
  fetchContent(limit: Int32, filterJSON: string | null): Promise<string>;
188
203
  applyFilter(feedId: string, filterJSON: string | null): void;
189
- preloadFeed(configJSON: string): Promise<string>;
204
+ preloadFeed(configJSON: string, itemsJSON: string | null): Promise<string>;
190
205
 
191
206
  // --- Storyboard / seek thumbnails ---
192
207
  prefetchStoryboard(playbackId: string): void;
@@ -212,6 +227,7 @@ export interface Spec extends TurboModule {
212
227
  readonly onDismiss: EventEmitter<DismissEvent>;
213
228
  readonly onRefreshRequested: EventEmitter<RefreshRequestedEvent>;
214
229
  readonly onDidFetchContentItems: EventEmitter<DidFetchContentItemsEvent>;
230
+ readonly onFeedReady: EventEmitter<FeedReadyEvent>;
215
231
 
216
232
  // --- Overlay per-surface events ---
217
233
  readonly onOverlayActiveChanged: EventEmitter<OverlayActiveEvent>;
@@ -222,6 +238,7 @@ export interface Spec extends TurboModule {
222
238
  readonly onOverlayActiveCueChanged: EventEmitter<OverlayActiveCueEvent>;
223
239
  readonly onOverlayFeedScrollPhaseChanged: EventEmitter<OverlayFeedScrollPhaseEvent>;
224
240
  readonly onOverlayTimeUpdate: EventEmitter<OverlayTimeUpdateEvent>;
241
+ readonly onOverlayFullState: EventEmitter<OverlayFullStateEvent>;
225
242
  }
226
243
 
227
244
  export default TurboModuleRegistry.getEnforcing<Spec>('ShortKitModule');