@shortkitsdk/react-native 0.2.6 → 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 (75) hide show
  1. package/ShortKitReactNative.podspec +1 -0
  2. package/android/build.gradle.kts +17 -1
  3. package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +379 -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 +570 -0
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +1029 -0
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +212 -219
  8. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +17 -3
  9. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +157 -742
  10. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +11 -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 +444 -0
  15. package/ios/SKFabricSurfaceWrapper.h +18 -0
  16. package/ios/SKFabricSurfaceWrapper.mm +57 -0
  17. package/ios/ShortKitBridge.swift +220 -63
  18. package/ios/ShortKitFeedView.swift +82 -228
  19. package/ios/ShortKitFeedViewManager.mm +3 -2
  20. package/ios/ShortKitModule.mm +69 -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 +3683 -1249
  25. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +56 -15
  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 +56 -15
  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 +3683 -1249
  31. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +56 -15
  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 +56 -15
  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 -24
  57. package/src/ShortKitFeed.tsx +124 -41
  58. package/src/ShortKitLoadingSurface.tsx +24 -0
  59. package/src/ShortKitOverlaySurface.tsx +313 -0
  60. package/src/ShortKitPlayer.tsx +30 -9
  61. package/src/ShortKitProvider.tsx +28 -285
  62. package/src/index.ts +9 -3
  63. package/src/serialization.ts +20 -39
  64. package/src/specs/NativeShortKitModule.ts +74 -45
  65. package/src/specs/ShortKitFeedViewNativeComponent.ts +3 -2
  66. package/src/types.ts +84 -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,109 @@ 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 }
75
-
76
- guard let sdk = ShortKitBridge.shared?.sdk else {
77
- NSLog("[ShortKitFeedView] ShortKit SDK not initialized. Call ShortKitModule.initialize() first.")
78
- return
79
- }
80
-
81
59
  guard let parentVC = findParentViewController() else {
82
- NSLog("[ShortKitFeedView] Could not find parent UIViewController.")
83
60
  return
84
61
  }
85
62
 
86
- let feedVC = ShortKitFeedViewController(shortKit: sdk)
87
- self.feedViewController = feedVC
88
-
89
- parentVC.addChild(feedVC)
90
- feedVC.view.frame = bounds
91
- feedVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
92
- addSubview(feedVC.view)
93
- feedVC.didMove(toParent: parentVC)
94
-
95
- setupScrollTracking(feedVC)
96
- setupOverlayReadyObserver()
97
- }
98
-
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
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
+ feedVC.activate()
70
+ if let feedId = self.feedId {
71
+ ShortKitBridge.shared?.registerFeed(id: feedId, viewController: feedVC)
72
+ }
73
+ return
106
74
  }
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
- guard let feedVC = feedViewController else { return }
119
-
120
- feedVC.willMove(toParent: nil)
121
- feedVC.view.removeFromSuperview()
122
- feedVC.removeFromParent()
123
-
124
- self.feedViewController = nil
125
- }
126
75
 
127
- // MARK: - Scroll Tracking
128
-
129
- private func setupScrollTracking(_ feedVC: ShortKitFeedViewController) {
130
- guard let scrollView = findScrollView(in: feedVC.view) else {
76
+ guard let sdk = ShortKitBridge.shared?.sdk else {
131
77
  return
132
78
  }
133
79
 
134
- scrollObservation = scrollView.observe(\.contentOffset, options: [.new]) { [weak self] sv, _ in
135
- self?.handleScrollOffset(sv)
136
- }
137
- }
80
+ // Parse config from the Fabric view prop
81
+ var feedConfig = ShortKitBridge.parseFeedConfig(self.config ?? "{}")
138
82
 
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)
83
+ // Consume preload handle if available
84
+ if let preloadId = self.preloadId {
85
+ NSLog("[ShortKit FeedView] preloadId prop: %@", preloadId)
86
+ if let preload = ShortKitBridge.shared?.consumePreload(id: preloadId) {
87
+ feedConfig.preload = preload
88
+ NSLog("[ShortKit FeedView] ✅ Preload handle consumed for feedId: %@", preloadId)
89
+ } else {
90
+ NSLog("[ShortKit FeedView] No preload handle found for feedId: %@", preloadId)
91
+ NSLog("[ShortKit FeedView] Available preload handles: %@", ShortKitBridge.shared?.preloadHandles.keys.joined(separator: ", ") ?? "none")
179
92
  }
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?()
93
+ } else {
94
+ NSLog("[ShortKit FeedView] No preloadId prop set")
184
95
  }
185
96
 
186
- let delta = offset - CGFloat(currentPage) * cellHeight
97
+ NSLog("[ShortKit FeedView] feedConfig.preload is %@", feedConfig.preload != nil ? "SET" : "NIL")
98
+ NSLog("[ShortKit FeedView] feedConfig.feedSource: %@", feedConfig.feedSource == .custom ? "custom" : "algorithmic")
187
99
 
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
- }
100
+ NSLog("[ShortKit FeedView] Creating ShortKitFeedViewController with config.preload=%@, config.feedSource=%@",
101
+ feedConfig.preload != nil ? "SET" : "NIL",
102
+ feedConfig.feedSource == .custom ? "custom" : "algorithmic")
195
103
 
196
- let translateY = CGAffineTransform(translationX: 0, y: -delta)
104
+ let feedVC = ShortKitFeedViewController(shortKit: sdk, config: feedConfig, startAtItemId: startAtItemId)
105
+ feedVC.setBridgeManaged()
197
106
 
198
- // Current overlays follow the active cell
199
- currentOverlayView?.transform = translateY
200
- currentCarouselOverlayView?.transform = translateY
107
+ NSLog("[ShortKit FeedView] VC created successfully (bridge-managed)")
201
108
 
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
109
+ feedVC.onDismiss = {
110
+ ShortKitBridge.shared?.emitDismiss()
213
111
  }
214
- }
215
112
 
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?()
113
+ self.feedViewController = feedVC
114
+ if let feedId = self.feedId {
115
+ ShortKitBridge.shared?.registerFeed(id: feedId, viewController: feedVC)
225
116
  }
226
- }
227
117
 
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
- }
118
+ parentVC.addChild(feedVC)
119
+ feedVC.view.frame = bounds
120
+ feedVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
121
+ addSubview(feedVC.view)
122
+ feedVC.didMove(toParent: parentVC)
252
123
  }
253
124
 
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
125
+ /// Detach the feedVC from the parent VC hierarchy without destroying it.
126
+ /// Called when the native stack temporarily removes this view from the window
127
+ /// (e.g. pushing a new screen on top). The feedVC and its state are preserved
128
+ /// so they can be re-attached when the view returns to the window.
129
+ private func suspendFeedViewController() {
130
+ if let feedId = self.feedId {
131
+ ShortKitBridge.shared?.unregisterFeed(id: feedId)
270
132
  }
271
- }
133
+ guard let feedVC = feedViewController else { return }
272
134
 
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
135
+ feedVC.deactivate()
136
+ feedVC.willMove(toParent: nil)
137
+ feedVC.view.removeFromSuperview()
138
+ feedVC.removeFromParent()
139
+ // Keep feedViewController reference — re-attached in embedFeedViewControllerIfNeeded
288
140
  }
289
141
 
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
- }
142
+ /// Full teardown called from deinit when React unmounts the native view.
143
+ private func destroyFeedViewController() {
144
+ if let feedId = self.feedId {
145
+ ShortKitBridge.shared?.unregisterFeed(id: feedId)
299
146
  }
300
- return nil
147
+ guard let feedVC = feedViewController else { return }
148
+
149
+ feedVC.deactivate()
150
+ feedVC.willMove(toParent: nil)
151
+ feedVC.view.removeFromSuperview()
152
+ feedVC.removeFromParent()
153
+
154
+ self.feedViewController = nil
301
155
  }
302
156
 
303
157
  // 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,21 @@ 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
+ @"onFeedReady",
75
+ @"onRefreshRequested",
76
+ @"onDidFetchContentItems",
77
+ @"onOverlayActiveChanged",
78
+ @"onOverlayPlayerStateChanged",
79
+ @"onOverlayMutedChanged",
80
+ @"onOverlayPlaybackRateChanged",
81
+ @"onOverlayCaptionsEnabledChanged",
82
+ @"onOverlayActiveCueChanged",
83
+ @"onOverlayFeedScrollPhaseChanged",
84
+ @"onOverlayTimeUpdate",
85
+ @"onOverlayFullState",
74
86
  ];
75
87
  }
76
88
 
@@ -108,7 +120,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
108
120
  // MARK: - Lifecycle
109
121
 
110
122
  RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
111
- config:(NSString *)config
123
+ hasLoadingView:(BOOL)hasLoadingView
112
124
  clientAppName:(NSString *)clientAppName
113
125
  clientAppVersion:(NSString *)clientAppVersion
114
126
  customDimensions:(NSString *)customDimensions) {
@@ -116,20 +128,19 @@ RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
116
128
  [_shortKitBridge teardown];
117
129
 
118
130
  _shortKitBridge = [[ShortKitBridge alloc] initWithApiKey:apiKey
119
- config:config
131
+ hasLoadingView:hasLoadingView
120
132
  clientAppName:clientAppName
121
133
  clientAppVersion:clientAppVersion
122
134
  customDimensions:customDimensions
123
- delegate:self];
135
+ delegate:self
136
+ surfacePresenter:_surfacePresenter];
124
137
 
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;
138
+ // Replay any feed ops that arrived before the bridge was ready
139
+ if (_pendingBridgeOps) {
140
+ for (void(^op)(void) in _pendingBridgeOps) {
141
+ op();
142
+ }
143
+ _pendingBridgeOps = nil;
133
144
  }
134
145
  }
135
146
 
@@ -206,30 +217,57 @@ RCT_EXPORT_METHOD(setMaxBitrate:(double)bitrate) {
206
217
 
207
218
  // MARK: - Custom Feed
208
219
 
209
- RCT_EXPORT_METHOD(setFeedItems:(NSString *)items) {
220
+ RCT_EXPORT_METHOD(setFeedItems:(NSString *)feedId items:(NSString *)items) {
210
221
  if (_shortKitBridge) {
211
- [_shortKitBridge setFeedItems:items];
222
+ [_shortKitBridge setFeedItems:feedId items:items];
212
223
  } else {
213
- _pendingFeedItems = items;
224
+ if (!_pendingBridgeOps) _pendingBridgeOps = [NSMutableArray new];
225
+ [_pendingBridgeOps addObject:^{
226
+ [self->_shortKitBridge setFeedItems:feedId items:items];
227
+ }];
214
228
  }
215
229
  }
216
230
 
217
- RCT_EXPORT_METHOD(appendFeedItems:(NSString *)items) {
231
+ RCT_EXPORT_METHOD(appendFeedItems:(NSString *)feedId items:(NSString *)items) {
218
232
  if (_shortKitBridge) {
219
- [_shortKitBridge appendFeedItems:items];
233
+ [_shortKitBridge appendFeedItems:feedId items:items];
220
234
  } else {
221
- _pendingAppendItems = items;
235
+ if (!_pendingBridgeOps) _pendingBridgeOps = [NSMutableArray new];
236
+ [_pendingBridgeOps addObject:^{
237
+ [self->_shortKitBridge appendFeedItems:feedId items:items];
238
+ }];
222
239
  }
223
240
  }
224
241
 
225
242
  RCT_EXPORT_METHOD(fetchContent:(NSInteger)limit
243
+ filterJSON:(NSString *)filterJSON
226
244
  resolve:(RCTPromiseResolveBlock)resolve
227
245
  reject:(RCTPromiseRejectBlock)reject) {
228
- [_shortKitBridge fetchContent:limit completion:^(NSString *json) {
246
+ [_shortKitBridge fetchContent:limit filterJSON:filterJSON completion:^(NSString *json) {
229
247
  resolve(json);
230
248
  }];
231
249
  }
232
250
 
251
+ RCT_EXPORT_METHOD(applyFilter:(NSString *)feedId filterJSON:(NSString *)filterJSON) {
252
+ if (_shortKitBridge) {
253
+ [_shortKitBridge applyFilter:feedId filterJSON:filterJSON];
254
+ } else {
255
+ if (!_pendingBridgeOps) _pendingBridgeOps = [NSMutableArray new];
256
+ [_pendingBridgeOps addObject:^{
257
+ [self->_shortKitBridge applyFilter:feedId filterJSON:filterJSON];
258
+ }];
259
+ }
260
+ }
261
+
262
+ RCT_EXPORT_METHOD(preloadFeed:(NSString *)configJSON
263
+ itemsJSON:(NSString *)itemsJSON
264
+ resolve:(RCTPromiseResolveBlock)resolve
265
+ reject:(RCTPromiseRejectBlock)reject) {
266
+ [_shortKitBridge preloadFeed:configJSON itemsJSON:itemsJSON completion:^(NSString *uuid) {
267
+ resolve(uuid);
268
+ }];
269
+ }
270
+
233
271
  // MARK: - Storyboard / Seek Thumbnails
234
272
 
235
273
  RCT_EXPORT_METHOD(prefetchStoryboard:(NSString *)playbackId) {
@@ -248,12 +286,6 @@ RCT_EXPORT_METHOD(getStoryboardData:(NSString *)playbackId
248
286
  }];
249
287
  }
250
288
 
251
- // MARK: - Overlay Lifecycle
252
-
253
- RCT_EXPORT_METHOD(notifyOverlayReady) {
254
- [_shortKitBridge notifyOverlayReady];
255
- }
256
-
257
289
  // MARK: - New Architecture (TurboModule)
258
290
 
259
291
  #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