@shortkitsdk/react-native 0.2.6 → 0.2.11

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 (75) hide show
  1. package/ShortKitReactNative.podspec +1 -0
  2. package/android/build.gradle.kts +5 -1
  3. package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +319 -0
  4. package/android/src/main/java/com/shortkit/reactnative/ReactLoadingHost.kt +40 -0
  5. package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +559 -0
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +984 -0
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +88 -220
  8. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +12 -3
  9. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +123 -741
  10. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +2 -2
  11. package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +2 -2
  12. package/ios/ReactCarouselOverlayHost.swift +177 -0
  13. package/ios/ReactLoadingHost.swift +38 -0
  14. package/ios/ReactOverlayHost.swift +458 -0
  15. package/ios/SKFabricSurfaceWrapper.h +18 -0
  16. package/ios/SKFabricSurfaceWrapper.mm +57 -0
  17. package/ios/ShortKitBridge.swift +186 -63
  18. package/ios/ShortKitFeedView.swift +62 -229
  19. package/ios/ShortKitFeedViewManager.mm +3 -2
  20. package/ios/ShortKitModule.mm +66 -37
  21. package/ios/ShortKitPlayerNativeView.swift +39 -8
  22. package/ios/ShortKitReactNative-Bridging-Header.h +2 -0
  23. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
  24. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +2380 -522
  25. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +39 -12
  26. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  27. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +39 -12
  28. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  29. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
  30. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +2380 -522
  31. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +39 -12
  32. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  33. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +39 -12
  34. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  35. package/ios/ShortKitSDK.xcframework.bak/Info.plist +43 -0
  36. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
  37. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
  38. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +28917 -0
  39. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +824 -0
  40. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  41. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +824 -0
  42. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  43. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  44. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
  45. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
  46. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +28917 -0
  47. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +824 -0
  48. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  49. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +824 -0
  50. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  51. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  52. package/ios/ShortKitWidgetNativeView.swift +3 -3
  53. package/package.json +1 -1
  54. package/src/ShortKitCarouselOverlaySurface.tsx +55 -0
  55. package/src/ShortKitCommands.ts +31 -0
  56. package/src/ShortKitContext.ts +6 -25
  57. package/src/ShortKitFeed.tsx +110 -41
  58. package/src/ShortKitLoadingSurface.tsx +24 -0
  59. package/src/ShortKitOverlaySurface.tsx +205 -0
  60. package/src/ShortKitPlayer.tsx +6 -7
  61. package/src/ShortKitProvider.tsx +27 -286
  62. package/src/index.ts +5 -3
  63. package/src/serialization.ts +19 -39
  64. package/src/specs/NativeShortKitModule.ts +58 -46
  65. package/src/specs/ShortKitFeedViewNativeComponent.ts +3 -2
  66. package/src/types.ts +78 -16
  67. package/src/useShortKit.ts +1 -3
  68. package/src/useShortKitPlayer.ts +7 -7
  69. package/android/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +0 -48
  70. package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +0 -128
  71. package/ios/ShortKitCarouselOverlayBridge.swift +0 -219
  72. package/ios/ShortKitOverlayBridge.swift +0 -111
  73. package/src/CarouselOverlayManager.tsx +0 -70
  74. package/src/OverlayManager.tsx +0 -87
  75. package/src/useShortKitCarousel.ts +0 -29
@@ -4,46 +4,28 @@ import ShortKitSDK
4
4
  /// Fabric native view that embeds `ShortKitFeedViewController` using
5
5
  /// UIViewController containment. Props are set by the view manager via
6
6
  /// `@objc` setters.
7
- ///
8
- /// Also tracks the feed's scroll offset via KVO and applies a native
9
- /// transform to the sibling RN overlay view so it moves with the active
10
- /// cell during swipe transitions.
11
7
  @objc public class ShortKitFeedView: UIView {
12
8
 
13
9
  // MARK: - Props (set by RCTViewManager)
14
10
 
15
11
  @objc public var config: String? {
16
- didSet { /* reserved for future use */ }
12
+ didSet { /* config is read once at embed time; changes require remount */ }
17
13
  }
18
14
 
19
- @objc public var overlayType: String? {
20
- didSet { /* overlay mode is determined at ShortKit init time */ }
15
+ @objc public var startAtItemId: String? {
16
+ didSet { /* used at init time only */ }
21
17
  }
22
18
 
19
+ @objc public var preloadId: String? {
20
+ didSet { /* used at init time only */ }
21
+ }
22
+
23
+ @objc var feedId: String?
24
+
23
25
  // MARK: - Child VC
24
26
 
25
27
  private var feedViewController: ShortKitFeedViewController?
26
28
 
27
- // MARK: - Scroll Tracking
28
-
29
- private var scrollObservation: NSKeyValueObservation?
30
- /// Video overlay for the currently active cell (nativeID="overlay-current").
31
- private weak var currentOverlayView: UIView?
32
- /// Video overlay for the upcoming cell (nativeID="overlay-next").
33
- private weak var nextOverlayView: UIView?
34
- /// Carousel overlay for the currently active cell (nativeID="carousel-overlay-current").
35
- private weak var currentCarouselOverlayView: UIView?
36
- /// Carousel overlay for the upcoming cell (nativeID="carousel-overlay-next").
37
- private weak var nextCarouselOverlayView: UIView?
38
- /// The page index used for overlay transform calculations.
39
- private var currentPage: Int = 0
40
- /// Closure to execute the pending overlay transform swap once JS signals ready.
41
- private var pendingSwap: (() -> Void)?
42
- /// Fallback timer in case JS never calls notifyOverlayReady.
43
- private var swapFallbackWorkItem: DispatchWorkItem?
44
- /// Observer for the overlay-ready notification from the JS bridge.
45
- private var overlayReadyObserver: NSObjectProtocol?
46
-
47
29
  // MARK: - Lifecycle
48
30
 
49
31
  public override func didMoveToWindow() {
@@ -58,7 +40,7 @@ import ShortKitSDK
58
40
  super.willMove(toWindow: newWindow)
59
41
 
60
42
  if newWindow == nil {
61
- removeFeedViewController()
43
+ suspendFeedViewController()
62
44
  }
63
45
  }
64
46
 
@@ -67,237 +49,88 @@ import ShortKitSDK
67
49
  feedViewController?.view.frame = bounds
68
50
  }
69
51
 
52
+ deinit {
53
+ destroyFeedViewController()
54
+ }
55
+
70
56
  // MARK: - VC Containment
71
57
 
72
58
  private func embedFeedViewControllerIfNeeded() {
73
- // Already embedded
74
- guard feedViewController == nil else { return }
59
+ guard let parentVC = findParentViewController() else {
60
+ return
61
+ }
75
62
 
76
- guard let sdk = ShortKitBridge.shared?.sdk else {
77
- NSLog("[ShortKitFeedView] ShortKit SDK not initialized. Call ShortKitModule.initialize() first.")
63
+ // Re-attach an existing suspended VC (e.g. after native stack pop)
64
+ if let feedVC = feedViewController {
65
+ parentVC.addChild(feedVC)
66
+ feedVC.view.frame = bounds
67
+ addSubview(feedVC.view)
68
+ feedVC.didMove(toParent: parentVC)
69
+ if let feedId = self.feedId {
70
+ ShortKitBridge.shared?.registerFeed(id: feedId, viewController: feedVC)
71
+ }
78
72
  return
79
73
  }
80
74
 
81
- guard let parentVC = findParentViewController() else {
82
- NSLog("[ShortKitFeedView] Could not find parent UIViewController.")
75
+ guard let sdk = ShortKitBridge.shared?.sdk else {
83
76
  return
84
77
  }
85
78
 
86
- let feedVC = ShortKitFeedViewController(shortKit: sdk)
79
+ // Parse config from the Fabric view prop
80
+ var feedConfig = ShortKitBridge.parseFeedConfig(self.config ?? "{}")
81
+
82
+ // Consume preload handle if available
83
+ if let preloadId = self.preloadId,
84
+ let preload = ShortKitBridge.shared?.consumePreload(id: preloadId) {
85
+ feedConfig.preload = preload
86
+ }
87
+
88
+ let feedVC = ShortKitFeedViewController(shortKit: sdk, config: feedConfig, startAtItemId: startAtItemId)
89
+
90
+ feedVC.onDismiss = {
91
+ ShortKitBridge.shared?.emitDismiss()
92
+ }
93
+
87
94
  self.feedViewController = feedVC
95
+ if let feedId = self.feedId {
96
+ ShortKitBridge.shared?.registerFeed(id: feedId, viewController: feedVC)
97
+ }
88
98
 
89
99
  parentVC.addChild(feedVC)
90
100
  feedVC.view.frame = bounds
91
101
  feedVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
92
102
  addSubview(feedVC.view)
93
103
  feedVC.didMove(toParent: parentVC)
94
-
95
- setupScrollTracking(feedVC)
96
- setupOverlayReadyObserver()
97
104
  }
98
105
 
99
- private func removeFeedViewController() {
100
- pendingSwap = nil
101
- swapFallbackWorkItem?.cancel()
102
- swapFallbackWorkItem = nil
103
- if let observer = overlayReadyObserver {
104
- NotificationCenter.default.removeObserver(observer)
105
- overlayReadyObserver = nil
106
+ /// Detach the feedVC from the parent VC hierarchy without destroying it.
107
+ /// Called when the native stack temporarily removes this view from the window
108
+ /// (e.g. pushing a new screen on top). The feedVC and its state are preserved
109
+ /// so they can be re-attached when the view returns to the window.
110
+ private func suspendFeedViewController() {
111
+ if let feedId = self.feedId {
112
+ ShortKitBridge.shared?.unregisterFeed(id: feedId)
106
113
  }
107
- scrollObservation?.invalidate()
108
- scrollObservation = nil
109
- currentOverlayView?.transform = .identity
110
- nextOverlayView?.transform = .identity
111
- currentCarouselOverlayView?.transform = .identity
112
- nextCarouselOverlayView?.transform = .identity
113
- currentOverlayView = nil
114
- nextOverlayView = nil
115
- currentCarouselOverlayView = nil
116
- nextCarouselOverlayView = nil
117
-
118
114
  guard let feedVC = feedViewController else { return }
119
115
 
120
116
  feedVC.willMove(toParent: nil)
121
117
  feedVC.view.removeFromSuperview()
122
118
  feedVC.removeFromParent()
123
-
124
- self.feedViewController = nil
125
- }
126
-
127
- // MARK: - Scroll Tracking
128
-
129
- private func setupScrollTracking(_ feedVC: ShortKitFeedViewController) {
130
- guard let scrollView = findScrollView(in: feedVC.view) else {
131
- return
132
- }
133
-
134
- scrollObservation = scrollView.observe(\.contentOffset, options: [.new]) { [weak self] sv, _ in
135
- self?.handleScrollOffset(sv)
136
- }
119
+ // Keep feedViewController reference — re-attached in embedFeedViewControllerIfNeeded
137
120
  }
138
121
 
139
- private func handleScrollOffset(_ scrollView: UIScrollView) {
140
- let cellHeight = scrollView.bounds.height
141
- guard cellHeight > 0 else { return }
142
-
143
- let offset = scrollView.contentOffset.y
144
-
145
- // Detect page change, but DEFER updating currentPage until JS
146
- // signals that overlay-current has been re-rendered with new content.
147
- //
148
- // Why: when the scroll settles on a new page, overlay-current still
149
- // shows the OLD page's metadata. If we update currentPage immediately,
150
- // delta snaps to 0 and overlay-current becomes visible with stale data.
151
- //
152
- // Instead, we store a pending swap closure. JS calls notifyOverlayReady()
153
- // after processing OVERLAY_ACTIVATE and resetting overlay opacity, which
154
- // executes the swap deterministically. A fallback timer catches edge cases.
155
- let nearestPage = Int(round(offset / cellHeight))
156
- if abs(offset - CGFloat(nearestPage) * cellHeight) < 1.0 {
157
- if nearestPage != currentPage && pendingSwap == nil {
158
- let targetPage = nearestPage
159
- pendingSwap = { [weak self] in
160
- guard let self else { return }
161
- self.currentPage = targetPage
162
- self.pendingSwap = nil
163
- self.swapFallbackWorkItem?.cancel()
164
- self.swapFallbackWorkItem = nil
165
- // Reapply overlay transforms now that currentPage is updated.
166
- let h = self.bounds.height
167
- self.currentOverlayView?.transform = .identity
168
- self.nextOverlayView?.transform = CGAffineTransform(translationX: 0, y: h)
169
- self.currentCarouselOverlayView?.transform = .identity
170
- self.nextCarouselOverlayView?.transform = CGAffineTransform(translationX: 0, y: h)
171
- }
172
- // Fallback: if JS never signals (e.g. overlay config is 'none'),
173
- // execute after 500ms to avoid getting stuck.
174
- let fallback = DispatchWorkItem { [weak self] in
175
- self?.pendingSwap?()
176
- }
177
- swapFallbackWorkItem = fallback
178
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: fallback)
179
- }
180
- } else if pendingSwap != nil {
181
- // User is scrolling again — execute pending swap immediately
182
- // so transforms stay aligned for the new gesture.
183
- pendingSwap?()
122
+ /// Full teardown called from deinit when React unmounts the native view.
123
+ private func destroyFeedViewController() {
124
+ if let feedId = self.feedId {
125
+ ShortKitBridge.shared?.unregisterFeed(id: feedId)
184
126
  }
127
+ guard let feedVC = feedViewController else { return }
185
128
 
186
- let delta = offset - CGFloat(currentPage) * cellHeight
187
-
188
- // Find the overlay views if not cached
189
- if currentOverlayView == nil || nextOverlayView == nil {
190
- findOverlayViews()
191
- }
192
- if currentCarouselOverlayView == nil || nextCarouselOverlayView == nil {
193
- findCarouselOverlayViews()
194
- }
195
-
196
- let translateY = CGAffineTransform(translationX: 0, y: -delta)
197
-
198
- // Current overlays follow the active cell
199
- currentOverlayView?.transform = translateY
200
- currentCarouselOverlayView?.transform = translateY
201
-
202
- // Next overlays: positioned one page ahead in the scroll direction
203
- if delta >= 0 {
204
- // Forward scroll (or idle): next cell is below
205
- let nextTransform = CGAffineTransform(translationX: 0, y: cellHeight - delta)
206
- nextOverlayView?.transform = nextTransform
207
- nextCarouselOverlayView?.transform = nextTransform
208
- } else {
209
- // Backward scroll: next cell is above
210
- let nextTransform = CGAffineTransform(translationX: 0, y: -cellHeight - delta)
211
- nextOverlayView?.transform = nextTransform
212
- nextCarouselOverlayView?.transform = nextTransform
213
- }
214
- }
215
-
216
- // MARK: - Overlay Ready Handshake
217
-
218
- private func setupOverlayReadyObserver() {
219
- overlayReadyObserver = NotificationCenter.default.addObserver(
220
- forName: .shortKitOverlayReady,
221
- object: nil,
222
- queue: .main
223
- ) { [weak self] _ in
224
- self?.pendingSwap?()
225
- }
226
- }
227
-
228
- /// Find the sibling RN overlay views by nativeID.
229
- ///
230
- /// In Fabric interop, `ShortKitFeedView` (a Paper-style `RCTViewManager`
231
- /// view) is wrapped in an intermediate `RCTViewComponentView`. So
232
- /// `self.superview` is that wrapper — not the React `<View>` container
233
- /// rendered by ShortKitFeed.tsx. We walk up the ancestor chain until we
234
- /// find the overlays.
235
- private func findOverlayViews() {
236
- var ancestor: UIView? = superview
237
- while let container = ancestor {
238
- for child in container.subviews {
239
- // Skip subtrees that contain ourselves
240
- guard !self.isDescendant(of: child) else { continue }
241
-
242
- if currentOverlayView == nil {
243
- currentOverlayView = findView(withNativeID: "overlay-current", in: child)
244
- }
245
- if nextOverlayView == nil {
246
- nextOverlayView = findView(withNativeID: "overlay-next", in: child)
247
- }
248
- }
249
- if currentOverlayView != nil && nextOverlayView != nil { return }
250
- ancestor = container.superview
251
- }
252
- }
253
-
254
- /// Find the sibling RN carousel overlay views by nativeID.
255
- private func findCarouselOverlayViews() {
256
- var ancestor: UIView? = superview
257
- while let container = ancestor {
258
- for child in container.subviews {
259
- guard !self.isDescendant(of: child) else { continue }
260
-
261
- if currentCarouselOverlayView == nil {
262
- currentCarouselOverlayView = findView(withNativeID: "carousel-overlay-current", in: child)
263
- }
264
- if nextCarouselOverlayView == nil {
265
- nextCarouselOverlayView = findView(withNativeID: "carousel-overlay-next", in: child)
266
- }
267
- }
268
- if currentCarouselOverlayView != nil && nextCarouselOverlayView != nil { return }
269
- ancestor = container.superview
270
- }
271
- }
272
-
273
- /// Recursively find a view by its React Native `nativeID` prop.
274
- /// In Fabric, this is stored on `RCTViewComponentView.nativeId`,
275
- /// not `accessibilityIdentifier`.
276
- private func findView(withNativeID nativeID: String, in view: UIView) -> UIView? {
277
- if view.responds(to: Selector(("nativeId"))),
278
- let rnNativeId = view.value(forKey: "nativeId") as? String,
279
- rnNativeId == nativeID {
280
- return view
281
- }
282
- for subview in view.subviews {
283
- if let found = findView(withNativeID: nativeID, in: subview) {
284
- return found
285
- }
286
- }
287
- return nil
288
- }
129
+ feedVC.willMove(toParent: nil)
130
+ feedVC.view.removeFromSuperview()
131
+ feedVC.removeFromParent()
289
132
 
290
- /// Recursively find the first UIScrollView in a view hierarchy.
291
- private func findScrollView(in view: UIView) -> UIScrollView? {
292
- if let sv = view as? UIScrollView {
293
- return sv
294
- }
295
- for subview in view.subviews {
296
- if let sv = findScrollView(in: subview) {
297
- return sv
298
- }
299
- }
300
- return nil
133
+ self.feedViewController = nil
301
134
  }
302
135
 
303
136
  // MARK: - Helpers
@@ -23,7 +23,8 @@ RCT_EXPORT_MODULE(ShortKitFeedView)
23
23
  }
24
24
 
25
25
  RCT_EXPORT_VIEW_PROPERTY(config, NSString)
26
- RCT_EXPORT_VIEW_PROPERTY(overlayType, NSString)
27
- RCT_EXPORT_VIEW_PROPERTY(templateName, NSString)
26
+ RCT_EXPORT_VIEW_PROPERTY(startAtItemId, NSString)
27
+ RCT_EXPORT_VIEW_PROPERTY(preloadId, NSString)
28
+ RCT_EXPORT_VIEW_PROPERTY(feedId, NSString)
28
29
 
29
30
  @end
@@ -11,8 +11,11 @@
11
11
  @implementation ShortKitModule {
12
12
  ShortKitBridge *_shortKitBridge;
13
13
  BOOL _hasListeners;
14
- NSString *_pendingFeedItems;
15
- NSString *_pendingAppendItems;
14
+ id _surfacePresenter;
15
+ // Buffer feed ops that arrive before the bridge is initialized.
16
+ // React fires child effects (MainScreen) before parent effects (ShortKitProvider),
17
+ // so setFeedItems can be called before initialize completes.
18
+ NSMutableArray<void(^)(void)> *_pendingBridgeOps;
16
19
  }
17
20
 
18
21
  RCT_EXPORT_MODULE(ShortKitModule)
@@ -33,6 +36,13 @@ RCT_EXPORT_MODULE(ShortKitModule)
33
36
  return self;
34
37
  }
35
38
 
39
+ /// Called automatically by RCTInstance for modules that respond to this selector.
40
+ /// This is the recommended way to receive the surface presenter in the new architecture.
41
+ - (void)setSurfacePresenter:(id)surfacePresenter {
42
+ _surfacePresenter = surfacePresenter;
43
+ [_shortKitBridge updateSurfacePresenter:surfacePresenter];
44
+ }
45
+
36
46
  /// Called when the RN bridge tears down (hot reload, app shutdown).
37
47
  - (void)invalidate {
38
48
  [_shortKitBridge teardown];
@@ -58,19 +68,19 @@ RCT_EXPORT_MODULE(ShortKitModule)
58
68
  @"onFormatChange",
59
69
  @"onPrefetchedAheadCountChanged",
60
70
  @"onRemainingContentCountChanged",
61
- @"onError",
62
- @"onShareTapped",
63
71
  @"onSurveyResponse",
64
- @"onOverlayConfigure",
65
- @"onOverlayActivate",
66
- @"onOverlayReset",
67
- @"onOverlayTap",
68
- @"onOverlayDoubleTap",
69
72
  @"onContentTapped",
70
- @"onCarouselOverlayConfigure",
71
- @"onCarouselOverlayActivate",
72
- @"onCarouselOverlayReset",
73
- @"onCarouselPageChanged",
73
+ @"onDismiss",
74
+ @"onRefreshRequested",
75
+ @"onDidFetchContentItems",
76
+ @"onOverlayActiveChanged",
77
+ @"onOverlayPlayerStateChanged",
78
+ @"onOverlayMutedChanged",
79
+ @"onOverlayPlaybackRateChanged",
80
+ @"onOverlayCaptionsEnabledChanged",
81
+ @"onOverlayActiveCueChanged",
82
+ @"onOverlayFeedScrollPhaseChanged",
83
+ @"onOverlayTimeUpdate",
74
84
  ];
75
85
  }
76
86
 
@@ -108,7 +118,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
108
118
  // MARK: - Lifecycle
109
119
 
110
120
  RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
111
- config:(NSString *)config
121
+ hasLoadingView:(BOOL)hasLoadingView
112
122
  clientAppName:(NSString *)clientAppName
113
123
  clientAppVersion:(NSString *)clientAppVersion
114
124
  customDimensions:(NSString *)customDimensions) {
@@ -116,20 +126,19 @@ RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
116
126
  [_shortKitBridge teardown];
117
127
 
118
128
  _shortKitBridge = [[ShortKitBridge alloc] initWithApiKey:apiKey
119
- config:config
129
+ hasLoadingView:hasLoadingView
120
130
  clientAppName:clientAppName
121
131
  clientAppVersion:clientAppVersion
122
132
  customDimensions:customDimensions
123
- delegate:self];
133
+ delegate:self
134
+ surfacePresenter:_surfacePresenter];
124
135
 
125
- // Replay any feed items that arrived before initialization
126
- if (_pendingFeedItems) {
127
- [_shortKitBridge setFeedItems:_pendingFeedItems];
128
- _pendingFeedItems = nil;
129
- }
130
- if (_pendingAppendItems) {
131
- [_shortKitBridge appendFeedItems:_pendingAppendItems];
132
- _pendingAppendItems = nil;
136
+ // Replay any feed ops that arrived before the bridge was ready
137
+ if (_pendingBridgeOps) {
138
+ for (void(^op)(void) in _pendingBridgeOps) {
139
+ op();
140
+ }
141
+ _pendingBridgeOps = nil;
133
142
  }
134
143
  }
135
144
 
@@ -206,30 +215,56 @@ RCT_EXPORT_METHOD(setMaxBitrate:(double)bitrate) {
206
215
 
207
216
  // MARK: - Custom Feed
208
217
 
209
- RCT_EXPORT_METHOD(setFeedItems:(NSString *)items) {
218
+ RCT_EXPORT_METHOD(setFeedItems:(NSString *)feedId items:(NSString *)items) {
210
219
  if (_shortKitBridge) {
211
- [_shortKitBridge setFeedItems:items];
220
+ [_shortKitBridge setFeedItems:feedId items:items];
212
221
  } else {
213
- _pendingFeedItems = items;
222
+ if (!_pendingBridgeOps) _pendingBridgeOps = [NSMutableArray new];
223
+ [_pendingBridgeOps addObject:^{
224
+ [self->_shortKitBridge setFeedItems:feedId items:items];
225
+ }];
214
226
  }
215
227
  }
216
228
 
217
- RCT_EXPORT_METHOD(appendFeedItems:(NSString *)items) {
229
+ RCT_EXPORT_METHOD(appendFeedItems:(NSString *)feedId items:(NSString *)items) {
218
230
  if (_shortKitBridge) {
219
- [_shortKitBridge appendFeedItems:items];
231
+ [_shortKitBridge appendFeedItems:feedId items:items];
220
232
  } else {
221
- _pendingAppendItems = items;
233
+ if (!_pendingBridgeOps) _pendingBridgeOps = [NSMutableArray new];
234
+ [_pendingBridgeOps addObject:^{
235
+ [self->_shortKitBridge appendFeedItems:feedId items:items];
236
+ }];
222
237
  }
223
238
  }
224
239
 
225
240
  RCT_EXPORT_METHOD(fetchContent:(NSInteger)limit
241
+ filterJSON:(NSString *)filterJSON
226
242
  resolve:(RCTPromiseResolveBlock)resolve
227
243
  reject:(RCTPromiseRejectBlock)reject) {
228
- [_shortKitBridge fetchContent:limit completion:^(NSString *json) {
244
+ [_shortKitBridge fetchContent:limit filterJSON:filterJSON completion:^(NSString *json) {
229
245
  resolve(json);
230
246
  }];
231
247
  }
232
248
 
249
+ RCT_EXPORT_METHOD(applyFilter:(NSString *)feedId filterJSON:(NSString *)filterJSON) {
250
+ if (_shortKitBridge) {
251
+ [_shortKitBridge applyFilter:feedId filterJSON:filterJSON];
252
+ } else {
253
+ if (!_pendingBridgeOps) _pendingBridgeOps = [NSMutableArray new];
254
+ [_pendingBridgeOps addObject:^{
255
+ [self->_shortKitBridge applyFilter:feedId filterJSON:filterJSON];
256
+ }];
257
+ }
258
+ }
259
+
260
+ RCT_EXPORT_METHOD(preloadFeed:(NSString *)configJSON
261
+ resolve:(RCTPromiseResolveBlock)resolve
262
+ reject:(RCTPromiseRejectBlock)reject) {
263
+ [_shortKitBridge preloadFeed:configJSON completion:^(NSString *uuid) {
264
+ resolve(uuid);
265
+ }];
266
+ }
267
+
233
268
  // MARK: - Storyboard / Seek Thumbnails
234
269
 
235
270
  RCT_EXPORT_METHOD(prefetchStoryboard:(NSString *)playbackId) {
@@ -248,12 +283,6 @@ RCT_EXPORT_METHOD(getStoryboardData:(NSString *)playbackId
248
283
  }];
249
284
  }
250
285
 
251
- // MARK: - Overlay Lifecycle
252
-
253
- RCT_EXPORT_METHOD(notifyOverlayReady) {
254
- [_shortKitBridge notifyOverlayReady];
255
- }
256
-
257
286
  // MARK: - New Architecture (TurboModule)
258
287
 
259
288
  #ifdef RCT_NEW_ARCH_ENABLED
@@ -49,7 +49,7 @@ import ShortKitSDK
49
49
  public override func willMove(toWindow newWindow: UIWindow?) {
50
50
  super.willMove(toWindow: newWindow)
51
51
  if newWindow == nil {
52
- removePlayerVC()
52
+ suspendPlayerVC()
53
53
  }
54
54
  }
55
55
 
@@ -58,13 +58,30 @@ import ShortKitSDK
58
58
  playerVC?.view.frame = bounds
59
59
  }
60
60
 
61
+ deinit {
62
+ destroyPlayerVC()
63
+ }
64
+
61
65
  // MARK: - VC Containment
62
66
 
63
67
  private func embedPlayerVCIfNeeded() {
64
- guard playerVC == nil else { return }
65
- guard let sdk = ShortKitBridge.shared?.sdk else { return }
66
68
  guard let parentVC = findParentViewController() else { return }
67
69
 
70
+ // Re-attach a suspended VC (e.g. after native stack pop)
71
+ if let vc = playerVC {
72
+ parentVC.addChild(vc)
73
+ vc.view.frame = bounds
74
+ addSubview(vc.view)
75
+ vc.didMove(toParent: parentVC)
76
+ if active {
77
+ vc.activate()
78
+ }
79
+ return
80
+ }
81
+
82
+ // First mount — create a new VC
83
+ guard let sdk = ShortKitBridge.shared?.sdk else { return }
84
+
68
85
  let playerConfig = parsedConfig ?? PlayerConfig()
69
86
 
70
87
  let vc = ShortKitPlayerViewController(shortKit: sdk, config: playerConfig)
@@ -88,7 +105,21 @@ import ShortKitSDK
88
105
  }
89
106
  }
90
107
 
91
- private func removePlayerVC() {
108
+ /// Detach the playerVC from the parent VC hierarchy without destroying it.
109
+ /// Called when the native stack temporarily removes this view from the window
110
+ /// (e.g. pushing a new screen on top). The playerVC and its thumbnail are
111
+ /// preserved so they can be re-attached instantly when the view returns.
112
+ private func suspendPlayerVC() {
113
+ guard let vc = playerVC else { return }
114
+ vc.deactivate()
115
+ vc.willMove(toParent: nil)
116
+ vc.view.removeFromSuperview()
117
+ vc.removeFromParent()
118
+ // Keep playerVC reference — re-attached in embedPlayerVCIfNeeded
119
+ }
120
+
121
+ /// Full teardown — called from deinit when React unmounts the native view.
122
+ private func destroyPlayerVC() {
92
123
  guard let vc = playerVC else { return }
93
124
  vc.deactivate()
94
125
  vc.willMove(toParent: nil)
@@ -104,7 +135,7 @@ import ShortKitSDK
104
135
  parsedConfig = Self.parsePlayerConfig(json)
105
136
  // Config changes require re-embedding
106
137
  if playerVC != nil {
107
- removePlayerVC()
138
+ destroyPlayerVC()
108
139
  embedPlayerVCIfNeeded()
109
140
  }
110
141
  }
@@ -148,9 +179,9 @@ import ShortKitSDK
148
179
  if let overlayObj = obj["overlay"] as? [String: Any],
149
180
  overlayObj["type"] as? String == "custom" {
150
181
  overlayMode = .custom { @Sendable in
151
- let overlay = ShortKitOverlayBridge()
152
- overlay.bridge = ShortKitBridge.shared
153
- return overlay
182
+ let host = ReactOverlayHost()
183
+ host.surfacePresenter = ShortKitBridge.shared?.surfacePresenter
184
+ return host
154
185
  }
155
186
  } else {
156
187
  overlayMode = .none
@@ -1,3 +1,5 @@
1
1
  #import <React/RCTBridgeModule.h>
2
2
  #import <React/RCTEventEmitter.h>
3
+ #import <React-RCTFabric/React/RCTFabricSurface.h>
4
+ #import <React-RCTFabric/React/RCTSurfacePresenter.h>
3
5
  #import "ShortKitModule.h"
@@ -365,7 +365,7 @@ SWIFT_CLASS("_TtC11ShortKitSDK26ShortKitFeedViewController")
365
365
 
366
366
  /// Public single-video player view controller.
367
367
  /// Starts in thumbnail-only mode and borrows an AVPlayer from the shared
368
- /// <code>TilePlayerPool</code> when activated. Call <code>activate()</code> when the tile becomes
368
+ /// <code>PlayerPool</code> tile slots when activated. Call <code>activate()</code> when the tile becomes
369
369
  /// visible and <code>deactivate()</code> when it scrolls offscreen.
370
370
  SWIFT_CLASS("_TtC11ShortKitSDK28ShortKitPlayerViewController")
371
371
  @interface ShortKitPlayerViewController : UIViewController