@shortkitsdk/react-native 0.2.12 → 0.2.14

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 (48) hide show
  1. package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +47 -4
  2. package/android/src/main/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHost.kt +431 -0
  3. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +83 -0
  4. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +2 -0
  5. package/ios/ReactCarouselOverlayHost.swift +37 -17
  6. package/ios/ReactOverlayHost.swift +20 -8
  7. package/ios/ReactVideoCarouselOverlayHost.swift +283 -0
  8. package/ios/ShortKitBridge.swift +42 -0
  9. package/ios/ShortKitModule.mm +2 -1
  10. package/ios/ShortKitSDK.xcframework/Info.plist +4 -4
  11. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +1833 -201
  12. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +51 -1
  13. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  14. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +51 -1
  15. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  16. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
  17. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +1833 -201
  18. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +51 -1
  19. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  20. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +51 -1
  21. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  22. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
  23. package/ios/ShortKitSDK.xcframework.bak2/Info.plist +43 -0
  24. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
  25. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
  26. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +31351 -0
  27. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +865 -0
  28. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  29. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +865 -0
  30. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  31. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  32. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
  33. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
  34. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +31351 -0
  35. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +865 -0
  36. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  37. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +865 -0
  38. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  39. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  40. package/package.json +1 -1
  41. package/src/ShortKitCarouselOverlaySurface.tsx +57 -2
  42. package/src/ShortKitFeed.tsx +5 -1
  43. package/src/ShortKitOverlaySurface.tsx +4 -5
  44. package/src/ShortKitVideoCarouselOverlaySurface.tsx +156 -0
  45. package/src/index.ts +4 -1
  46. package/src/serialization.ts +7 -0
  47. package/src/specs/NativeShortKitModule.ts +13 -0
  48. package/src/types.ts +39 -1
@@ -5,8 +5,8 @@ import ShortKitSDK
5
5
  /// for rendering the developer's React carousel overlay component inside a feed cell.
6
6
  ///
7
7
  /// Unlike ReactOverlayHost, this does not subscribe to player state (carousels
8
- /// are image-based). It emits image URLs to JS for Image.prefetch() and pushes
9
- /// ImageCarouselItem data as appProperties.
8
+ /// are image-based). It delivers `isActive` and `activeImageIndex` via bridge
9
+ /// events (routed by surfaceId) to avoid Fabric remounts.
10
10
  @objc public class ReactCarouselOverlayHost: UIView, @unchecked Sendable, CarouselOverlay {
11
11
 
12
12
  // MARK: - Configuration
@@ -20,9 +20,6 @@ import ShortKitSDK
20
20
 
21
21
  // MARK: - Eager Surface Creation
22
22
 
23
- /// Eagerly create the RN surface so it's ready before the first configure().
24
- /// Called by the overlay factory right after setting surfacePresenter and moduleName.
25
- /// Mirrors ReactOverlayHost.attach(player:) which does the same for video overlays.
26
23
  func prepareSurface() {
27
24
  createSurfaceIfNeeded()
28
25
  }
@@ -37,6 +34,11 @@ import ShortKitSDK
37
34
  private var surface: SKFabricSurfaceWrapper?
38
35
  private var surfaceView: UIView?
39
36
  private var pendingProps: [String: Any]?
37
+ private var isActive = false
38
+ private var activeImageIndex = 0
39
+
40
+ /// Unique identifier for this overlay instance, used for event routing.
41
+ let surfaceId = UUID().uuidString
40
42
 
41
43
  // MARK: - Init
42
44
 
@@ -57,11 +59,11 @@ import ShortKitSDK
57
59
  // MARK: - CarouselOverlay
58
60
 
59
61
  public func configure(with item: ImageCarouselItem) {
62
+ isActive = false
63
+ activeImageIndex = 0
60
64
  createSurfaceIfNeeded()
61
65
 
62
66
  // Replace remote URLs with local file URLs for any natively-cached images.
63
- // The SDK's PrefetchManager downloads images into NSCache<NSString, UIImage>
64
- // which RN can't access. Writing to temp files lets RN load instantly from disk.
65
67
  var modifiedItem = item
66
68
  if let cachedImage {
67
69
  var localImages: [CarouselImage] = []
@@ -85,10 +87,12 @@ import ShortKitSDK
85
87
  )
86
88
  }
87
89
 
88
- // Build props — push now if surface exists, otherwise store for later
89
90
  if let data = try? JSONEncoder().encode(modifiedItem),
90
91
  let json = String(data: data, encoding: .utf8) {
91
- let props: [String: Any] = ["item": json]
92
+ let props: [String: Any] = [
93
+ "surfaceId": surfaceId,
94
+ "item": json,
95
+ ]
92
96
  if let surface {
93
97
  surface.setProperties(props)
94
98
  } else {
@@ -97,14 +101,35 @@ import ShortKitSDK
97
101
  }
98
102
  }
99
103
 
104
+ public func activatePlayback() {
105
+ isActive = true
106
+ bridge?.emit("onOverlayFullState", body: [
107
+ "surfaceId": surfaceId,
108
+ "isActive": true,
109
+ // Include required fields for the full state event
110
+ "playerState": "idle",
111
+ "isMuted": true,
112
+ "playbackRate": 1.0,
113
+ "captionsEnabled": false,
114
+ "activeCue": NSNull(),
115
+ "feedScrollPhase": NSNull(),
116
+ ])
117
+ }
118
+
119
+ public func updateActiveImage(index: Int) {
120
+ activeImageIndex = index
121
+ bridge?.emit("onCarouselActiveImageChanged", body: [
122
+ "surfaceId": surfaceId,
123
+ "activeImageIndex": index,
124
+ ])
125
+ }
126
+
100
127
  /// Write a UIImage to the temp directory, returning a file:// URL string.
101
- /// Uses a hash of the remote URL as the filename to avoid duplicates.
102
128
  private func writeTempImage(_ image: UIImage, for remoteURL: String) -> String? {
103
129
  let hash = remoteURL.hash
104
130
  let fileName = "sk-carousel-\(abs(hash)).jpg"
105
131
  let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
106
132
 
107
- // Skip if already written
108
133
  if FileManager.default.fileExists(atPath: fileURL.path) {
109
134
  return fileURL.absoluteString
110
135
  }
@@ -119,10 +144,7 @@ import ShortKitSDK
119
144
  }
120
145
 
121
146
  public func resetState() {
122
- // Don't clear surface props — matches video overlay (ReactOverlayHost) pattern.
123
- // configure() will overwrite with new props directly, letting React do an
124
- // in-place re-render (old→new) instead of an unmount+remount (old→empty→new).
125
- // The overlay's synchronous reset pattern handles state transitions.
147
+ // Don't clear surface props — configure() will overwrite.
126
148
  }
127
149
 
128
150
  // MARK: - Surface Creation
@@ -130,7 +152,6 @@ import ShortKitSDK
130
152
  private func createSurfaceIfNeeded() {
131
153
  guard surface == nil, let presenter = surfacePresenter else { return }
132
154
 
133
- // Must always dispatch to main queue — see ReactOverlayHost for details.
134
155
  DispatchQueue.main.async { [weak self] in
135
156
  guard let self, self.surface == nil else { return }
136
157
  self.installSurface(presenter: presenter)
@@ -157,7 +178,6 @@ import ShortKitSDK
157
178
  surfaceView = view
158
179
  surface = surf
159
180
 
160
- // Flush any props that arrived before surface was ready
161
181
  if let pending = pendingProps {
162
182
  surf.setProperties(pending)
163
183
  pendingProps = nil
@@ -126,6 +126,7 @@ import ShortKitSDK
126
126
  }
127
127
 
128
128
  public func configure(with item: ContentItem) {
129
+ let isSameItem = item.id == currentItem?.id
129
130
  currentItem = item
130
131
  isActive = false
131
132
  timeDirty = false
@@ -140,10 +141,12 @@ import ShortKitSDK
140
141
 
141
142
  createSurfaceIfNeeded()
142
143
 
143
- // Set surface properties ONCE per item. This triggers a Fabric remount,
144
- // which is desired on cell reuse (new content, fresh React state).
145
- // All subsequent dynamic updates go through events, not setProperties().
146
- pushInitialProperties()
144
+ // Only push properties if the surface already exists.
145
+ // If the surface is still being created asynchronously,
146
+ // installSurface() will call pushInitialProperties() when ready.
147
+ if surface != nil && !isSameItem {
148
+ pushInitialProperties()
149
+ }
147
150
  }
148
151
 
149
152
  public func activatePlayback() {
@@ -226,6 +229,15 @@ import ShortKitSDK
226
229
 
227
230
  // Push any pending properties now that the surface exists
228
231
  pushInitialProperties()
232
+
233
+ // If activatePlayback() was called before the surface was ready,
234
+ // emit the full state now that JS can receive events.
235
+ if isActive {
236
+ DispatchQueue.main.async { [weak self] in
237
+ guard let self, self.isActive else { return }
238
+ self.emitFullState()
239
+ }
240
+ }
229
241
  }
230
242
 
231
243
  // MARK: - Layout
@@ -404,10 +416,10 @@ import ShortKitSDK
404
416
  props["item"] = json
405
417
  }
406
418
 
407
- // Initial valuesmay be stale by the time activatePlayback() fires,
408
- // which is why activatePlayback() emits a full state flush via events.
409
- props["isActive"] = false
410
- props["playerState"] = "idle"
419
+ // Use current state if activatePlayback() was called before the
420
+ // surface was created, isActive will already be true.
421
+ props["isActive"] = isActive
422
+ props["playerState"] = cachedPlayerState
411
423
  props["isMuted"] = cachedIsMuted
412
424
  props["playbackRate"] = cachedPlaybackRate
413
425
  props["captionsEnabled"] = cachedCaptionsEnabled
@@ -0,0 +1,283 @@
1
+ import UIKit
2
+ import Combine
3
+ import ShortKitSDK
4
+
5
+ /// A UIView that conforms to `VideoCarouselOverlay` and hosts an `RCTFabricSurface`
6
+ /// for rendering the developer's React video carousel overlay component inside a feed cell.
7
+ ///
8
+ /// Pushes `VideoCarouselItem` and active video data as surface properties, and
9
+ /// emits playback state (isActive, time, playerState, isMuted) via bridge events
10
+ /// using the same event names as `ReactOverlayHost` (routed by `surfaceId`).
11
+ @objc public class ReactVideoCarouselOverlayHost: UIView, @unchecked Sendable, VideoCarouselOverlay {
12
+
13
+ // MARK: - Configuration
14
+
15
+ var surfacePresenter: AnyObject?
16
+ weak var bridge: ShortKitBridge?
17
+
18
+ /// Module name for the RCTFabricSurface. Set by the overlay factory
19
+ /// based on the feed config's video carousel overlay name.
20
+ var videoCarouselOverlayModuleName: String = "ShortKitVideoCarouselOverlay"
21
+
22
+ // MARK: - Eager Surface Creation
23
+
24
+ func prepareSurface() {
25
+ createSurfaceIfNeeded()
26
+ }
27
+
28
+ // MARK: - State
29
+
30
+ private var surface: SKFabricSurfaceWrapper?
31
+ private var surfaceView: UIView?
32
+ private var pendingProps: [String: Any]?
33
+
34
+ /// Unique identifier for this overlay instance, used for event routing.
35
+ let surfaceId = UUID().uuidString
36
+
37
+ /// Cached carouselItem JSON — setProperties replaces all props, doesn't merge.
38
+ private var carouselItemJSON: String?
39
+
40
+ // Player state
41
+ private var player: ShortKitPlayer?
42
+ private var cancellables = Set<AnyCancellable>()
43
+ private var isActive = false
44
+ private var cachedPlayerState: String = "idle"
45
+ private var cachedIsMuted: Bool = true
46
+ private var cachedTime: (current: Double, duration: Double, buffered: Double) = (0, 0, 0)
47
+ private var timeCoalesceTimer: Timer?
48
+ private var timeDirty = false
49
+
50
+ // MARK: - Init
51
+
52
+ override init(frame: CGRect) {
53
+ super.init(frame: frame)
54
+ backgroundColor = .clear
55
+ isUserInteractionEnabled = true
56
+ }
57
+
58
+ required init?(coder: NSCoder) {
59
+ fatalError("init(coder:) is not supported")
60
+ }
61
+
62
+ deinit {
63
+ timeCoalesceTimer?.invalidate()
64
+ surface?.stop()
65
+ }
66
+
67
+ // MARK: - VideoCarouselOverlay
68
+
69
+ public func configure(with item: VideoCarouselItem) {
70
+ isActive = false
71
+ timeDirty = false
72
+ timeCoalesceTimer?.invalidate()
73
+ timeCoalesceTimer = nil
74
+ cachedTime = (0, 0, 0)
75
+ cachedPlayerState = "idle"
76
+
77
+ createSurfaceIfNeeded()
78
+
79
+ guard let data = try? JSONEncoder().encode(item),
80
+ let json = String(data: data, encoding: .utf8) else { return }
81
+ carouselItemJSON = json
82
+
83
+ var props: [String: Any] = [
84
+ "surfaceId": surfaceId,
85
+ "carouselItem": json,
86
+ "isActive": false,
87
+ "playerState": "idle",
88
+ "isMuted": cachedIsMuted,
89
+ ]
90
+ if let firstVideo = item.videos.first,
91
+ let videoData = try? JSONEncoder().encode(firstVideo),
92
+ let videoJSON = String(data: videoData, encoding: .utf8) {
93
+ props["activeVideo"] = videoJSON
94
+ props["activeVideoIndex"] = 0
95
+ }
96
+ if let surface {
97
+ surface.setProperties(props)
98
+ } else {
99
+ pendingProps = props
100
+ }
101
+ }
102
+
103
+ public func updateActiveVideo(index: Int, item: ContentItem) {
104
+ guard let data = try? JSONEncoder().encode(item),
105
+ let json = String(data: data, encoding: .utf8) else { return }
106
+
107
+ // Only emit when active — matches ReactOverlayHost pattern.
108
+ // During initial setup, configure() sets the first video via surface props.
109
+ // Before activation, the AsyncEventEmitter may not be initialized.
110
+ guard isActive else { return }
111
+
112
+ bridge?.emit("onVideoCarouselActiveVideoChanged", body: [
113
+ "surfaceId": surfaceId,
114
+ "activeVideo": json,
115
+ "activeVideoIndex": index,
116
+ ])
117
+ }
118
+
119
+ public func resetState() {
120
+ // Don't clear surface props — configure() will overwrite.
121
+ }
122
+
123
+ public func attach(player: ShortKitPlayer) {
124
+ self.player = player
125
+ subscribeToPlayer(player)
126
+ createSurfaceIfNeeded()
127
+ }
128
+
129
+ public func activatePlayback() {
130
+ isActive = true
131
+ startTimeCoalescing()
132
+
133
+ DispatchQueue.main.async { [weak self] in
134
+ guard let self, self.isActive else { return }
135
+ self.emitFullState()
136
+ }
137
+ }
138
+
139
+ public func deactivatePlayback() {
140
+ isActive = false
141
+ timeDirty = false
142
+ timeCoalesceTimer?.invalidate()
143
+ timeCoalesceTimer = nil
144
+ }
145
+
146
+ // MARK: - Player Subscriptions
147
+
148
+ private func subscribeToPlayer(_ player: ShortKitPlayer) {
149
+ cancellables.removeAll()
150
+ player.playerState
151
+ .receive(on: DispatchQueue.main)
152
+ .sink { [weak self] state in
153
+ guard let self else { return }
154
+ self.cachedPlayerState = Self.playerStateString(state)
155
+ if self.isActive {
156
+ self.bridge?.emit("onOverlayPlayerStateChanged", body: [
157
+ "surfaceId": self.surfaceId,
158
+ "playerState": self.cachedPlayerState
159
+ ])
160
+ }
161
+ }
162
+ .store(in: &cancellables)
163
+
164
+ player.isMuted
165
+ .receive(on: DispatchQueue.main)
166
+ .sink { [weak self] muted in
167
+ guard let self else { return }
168
+ self.cachedIsMuted = muted
169
+ if self.isActive {
170
+ self.bridge?.emit("onOverlayMutedChanged", body: [
171
+ "surfaceId": self.surfaceId,
172
+ "isMuted": self.cachedIsMuted
173
+ ])
174
+ }
175
+ }
176
+ .store(in: &cancellables)
177
+
178
+ player.time
179
+ .receive(on: DispatchQueue.main)
180
+ .sink { [weak self] time in
181
+ guard let self, self.isActive else { return }
182
+ self.cachedTime = (time.current, time.duration, time.buffered)
183
+ self.timeDirty = true
184
+ }
185
+ .store(in: &cancellables)
186
+ }
187
+
188
+ // MARK: - Time Coalescing
189
+
190
+ private func startTimeCoalescing() {
191
+ timeCoalesceTimer?.invalidate()
192
+ timeCoalesceTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
193
+ guard let self, self.timeDirty else { return }
194
+ self.timeDirty = false
195
+ self.bridge?.emit("onOverlayTimeUpdate", body: [
196
+ "surfaceId": self.surfaceId,
197
+ "current": self.cachedTime.current,
198
+ "duration": self.cachedTime.duration,
199
+ "buffered": self.cachedTime.buffered
200
+ ])
201
+ }
202
+ }
203
+
204
+ // MARK: - Full State Emission
205
+
206
+ private func emitFullState() {
207
+ bridge?.emit("onOverlayFullState", body: [
208
+ "surfaceId": surfaceId,
209
+ "isActive": true,
210
+ "playerState": cachedPlayerState,
211
+ "isMuted": cachedIsMuted,
212
+ "playbackRate": 1.0,
213
+ "captionsEnabled": false,
214
+ "activeCue": NSNull(),
215
+ "feedScrollPhase": NSNull(),
216
+ ])
217
+ }
218
+
219
+ // MARK: - Surface Creation
220
+
221
+ private func createSurfaceIfNeeded() {
222
+ guard surface == nil, let presenter = surfacePresenter else { return }
223
+
224
+ DispatchQueue.main.async { [weak self] in
225
+ guard let self, self.surface == nil else { return }
226
+ self.installSurface(presenter: presenter)
227
+ }
228
+ }
229
+
230
+ private func installSurface(presenter: AnyObject) {
231
+ guard let surf = SKFabricSurfaceWrapper(
232
+ presenter: presenter,
233
+ moduleName: videoCarouselOverlayModuleName,
234
+ initialProperties: [:]
235
+ ) else { return }
236
+ surf.start()
237
+
238
+ let view = surf.view
239
+ view.translatesAutoresizingMaskIntoConstraints = false
240
+ addSubview(view)
241
+ NSLayoutConstraint.activate([
242
+ view.topAnchor.constraint(equalTo: topAnchor),
243
+ view.leadingAnchor.constraint(equalTo: leadingAnchor),
244
+ view.trailingAnchor.constraint(equalTo: trailingAnchor),
245
+ view.bottomAnchor.constraint(equalTo: bottomAnchor),
246
+ ])
247
+ surfaceView = view
248
+ surface = surf
249
+
250
+ // Flush any props that arrived before surface was ready
251
+ if let pending = pendingProps {
252
+ surf.setProperties(pending)
253
+ pendingProps = nil
254
+ }
255
+ }
256
+
257
+ // MARK: - Layout
258
+
259
+ public override func layoutSubviews() {
260
+ super.layoutSubviews()
261
+ guard let surface else { return }
262
+ let size = bounds.size
263
+ guard size.width > 0, size.height > 0 else { return }
264
+ surface.setMinimumSize(size)
265
+ surface.setMaximumSize(size)
266
+ }
267
+
268
+ // MARK: - Helpers
269
+
270
+ private static func playerStateString(_ state: PlayerState) -> String {
271
+ switch state {
272
+ case .idle: return "idle"
273
+ case .loading: return "loading"
274
+ case .ready: return "ready"
275
+ case .playing: return "playing"
276
+ case .paused: return "paused"
277
+ case .seeking: return "seeking"
278
+ case .buffering: return "buffering"
279
+ case .ended: return "ended"
280
+ case .error(_): return "error"
281
+ }
282
+ }
283
+ }
@@ -651,6 +651,7 @@ import ShortKitSDK
651
651
  let feedSource: FeedSource = feedSourceStr == "custom" ? .custom : .algorithmic
652
652
 
653
653
  let carouselOverlay = parseCarouselOverlay(obj["carouselOverlay"] as? String)
654
+ let videoCarouselOverlay = parseVideoCarouselOverlay(obj["videoCarouselOverlay"] as? String)
654
655
 
655
656
  let filter = parseFeedFilter(obj["filter"] as? String)
656
657
 
@@ -662,6 +663,7 @@ import ShortKitSDK
662
663
  scrollAxis: scrollAxis,
663
664
  videoOverlay: videoOverlay,
664
665
  carouselOverlay: carouselOverlay,
666
+ videoCarouselOverlay: videoCarouselOverlay,
665
667
  surveyOverlay: .none,
666
668
  adOverlay: .none,
667
669
  muteOnStart: muteOnStart,
@@ -740,6 +742,39 @@ import ShortKitSDK
740
742
  return .none
741
743
  }
742
744
 
745
+ /// Parse a double-stringified video carousel overlay JSON into a `VideoCarouselOverlayMode`.
746
+ ///
747
+ /// Examples:
748
+ /// - `"\"none\""` -> `.none`
749
+ /// - `"{\"type\":\"custom\",\"name\":\"news\"}"` -> `.custom { ReactVideoCarouselOverlayHost() }`
750
+ private static func parseVideoCarouselOverlay(_ json: String?) -> VideoCarouselOverlayMode {
751
+ guard let json,
752
+ let data = json.data(using: .utf8),
753
+ let parsed = try? JSONSerialization.jsonObject(with: data) else {
754
+ return .none
755
+ }
756
+
757
+ if let str = parsed as? String, str == "none" {
758
+ return .none
759
+ }
760
+
761
+ if let obj = parsed as? [String: Any],
762
+ let type = obj["type"] as? String,
763
+ type == "custom" {
764
+ let name = obj["name"] as? String ?? "Default"
765
+ return .custom { @Sendable in
766
+ let host = ReactVideoCarouselOverlayHost()
767
+ host.surfacePresenter = ShortKitBridge.shared?.surfacePresenter
768
+ host.bridge = ShortKitBridge.shared
769
+ host.videoCarouselOverlayModuleName = "ShortKitVideoCarouselOverlay_\(name)"
770
+ host.prepareSurface()
771
+ return host
772
+ }
773
+ }
774
+
775
+ return .none
776
+ }
777
+
743
778
  /// Parse a double-stringified feedHeight JSON.
744
779
  /// e.g. `"{\"type\":\"fullscreen\"}"` or `"{\"type\":\"percentage\",\"value\":0.8}"`
745
780
  private static func parseFeedHeight(_ json: String?) -> FeedHeight {
@@ -800,6 +835,13 @@ import ShortKitSDK
800
835
  continue
801
836
  }
802
837
  result.append(.imageCarousel(carouselItem))
838
+ case "videoCarousel":
839
+ guard let itemData = obj["item"] as? [String: Any],
840
+ let itemJSON = try? JSONSerialization.data(withJSONObject: itemData),
841
+ let carouselItem = try? JSONDecoder().decode(VideoCarouselItem.self, from: itemJSON) else {
842
+ continue
843
+ }
844
+ result.append(.videoCarousel(carouselItem))
803
845
  default:
804
846
  continue
805
847
  }
@@ -83,6 +83,8 @@ RCT_EXPORT_MODULE(ShortKitModule)
83
83
  @"onOverlayFeedScrollPhaseChanged",
84
84
  @"onOverlayTimeUpdate",
85
85
  @"onOverlayFullState",
86
+ @"onCarouselActiveImageChanged",
87
+ @"onVideoCarouselActiveVideoChanged",
86
88
  ];
87
89
  }
88
90
 
@@ -106,7 +108,6 @@ RCT_EXPORT_MODULE(ShortKitModule)
106
108
 
107
109
  - (void)emitEvent:(NSString *)name body:(NSDictionary *)body {
108
110
  #ifdef RCT_NEW_ARCH_ENABLED
109
- // New Architecture: use the codegen EventEmitterCallback directly
110
111
  if (_eventEmitterCallback) {
111
112
  _eventEmitterCallback(std::string([name UTF8String]), body);
112
113
  }
@@ -8,7 +8,7 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>ShortKitSDK.framework/ShortKitSDK</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64-simulator</string>
11
+ <string>ios-arm64</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>ShortKitSDK.framework</string>
14
14
  <key>SupportedArchitectures</key>
@@ -17,14 +17,12 @@
17
17
  </array>
18
18
  <key>SupportedPlatform</key>
19
19
  <string>ios</string>
20
- <key>SupportedPlatformVariant</key>
21
- <string>simulator</string>
22
20
  </dict>
23
21
  <dict>
24
22
  <key>BinaryPath</key>
25
23
  <string>ShortKitSDK.framework/ShortKitSDK</string>
26
24
  <key>LibraryIdentifier</key>
27
- <string>ios-arm64</string>
25
+ <string>ios-arm64-simulator</string>
28
26
  <key>LibraryPath</key>
29
27
  <string>ShortKitSDK.framework</string>
30
28
  <key>SupportedArchitectures</key>
@@ -33,6 +31,8 @@
33
31
  </array>
34
32
  <key>SupportedPlatform</key>
35
33
  <string>ios</string>
34
+ <key>SupportedPlatformVariant</key>
35
+ <string>simulator</string>
36
36
  </dict>
37
37
  </array>
38
38
  <key>CFBundlePackageType</key>