@shortkitsdk/react-native 0.2.5 → 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 +126 -706
  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 +266 -65
  18. package/ios/ShortKitFeedView.swift +63 -207
  19. package/ios/ShortKitFeedViewManager.mm +3 -2
  20. package/ios/ShortKitModule.mm +86 -32
  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 +2 -1
  24. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +3998 -962
  25. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +85 -24
  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 +85 -24
  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 +2 -1
  30. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +3998 -962
  31. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +85 -24
  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 +85 -24
  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 +11 -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 +65 -250
  62. package/src/index.ts +9 -4
  63. package/src/serialization.ts +22 -42
  64. package/src/specs/NativeShortKitModule.ts +67 -53
  65. package/src/specs/ShortKitFeedViewNativeComponent.ts +3 -2
  66. package/src/types.ts +104 -19
  67. package/src/useShortKit.ts +1 -3
  68. package/src/useShortKitPlayer.ts +7 -8
  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 -54
  72. package/ios/ShortKitOverlayBridge.swift +0 -113
  73. package/src/CarouselOverlayManager.tsx +0 -71
  74. package/src/OverlayManager.tsx +0 -87
  75. package/src/useShortKitCarousel.ts +0 -29
@@ -18,27 +18,78 @@ import ShortKitSDK
18
18
  private var cancellables = Set<AnyCancellable>()
19
19
  private weak var delegate: ShortKitBridgeDelegateProtocol?
20
20
 
21
+ /// Surface presenter for creating RCTFabricSurface instances in overlay hosts.
22
+ /// Set from ShortKitModule.mm via setSurfacePresenter: (called by RCTInstance).
23
+ public var surfacePresenter: AnyObject?
24
+
25
+ /// Called from ShortKitModule when RCTInstance provides the surface presenter.
26
+ @objc public func updateSurfacePresenter(_ presenter: AnyObject?) {
27
+ self.surfacePresenter = presenter
28
+ }
29
+
30
+ /// Preload handles keyed by UUID, consumed by feed views via preloadId prop.
31
+ public var preloadHandles: [String: FeedPreload] = [:]
32
+
33
+ // MARK: - Feed Instance Registry
34
+
35
+ private struct WeakFeedRef {
36
+ weak var vc: ShortKitFeedViewController?
37
+ }
38
+
39
+ private var feedRegistry: [String: WeakFeedRef] = [:]
40
+ private var pendingOps: [String: [(ShortKitFeedViewController) -> Void]] = [:]
41
+
42
+ public func registerFeed(id: String, viewController vc: ShortKitFeedViewController) {
43
+ feedRegistry[id] = WeakFeedRef(vc: vc)
44
+
45
+ // Wire per-feed remaining content count callback
46
+ vc.onRemainingContentCountChange = { [weak self] count in
47
+ self?.emit("onRemainingContentCountChanged", body: [
48
+ "feedId": id,
49
+ "count": count
50
+ ])
51
+ }
52
+
53
+ // Replay buffered operations on the next run-loop tick so the VC's
54
+ // view hierarchy is fully set up after didMoveToWindow returns.
55
+ if let ops = pendingOps.removeValue(forKey: id) {
56
+ DispatchQueue.main.async { [weak vc] in
57
+ guard let vc = vc else { return }
58
+ for op in ops {
59
+ op(vc)
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ public func unregisterFeed(id: String) {
66
+ feedRegistry.removeValue(forKey: id)
67
+ // Note: pendingOps are preserved for this ID to survive detach/reattach cycles
68
+ }
69
+
70
+ private func feedViewController(for id: String) -> ShortKitFeedViewController? {
71
+ return feedRegistry[id]?.vc
72
+ }
73
+
21
74
  // MARK: - Init
22
75
 
23
76
  @objc public init(
24
77
  apiKey: String,
25
- config configJSON: String,
26
- embedId: String?,
78
+ hasLoadingView: Bool,
27
79
  clientAppName: String?,
28
80
  clientAppVersion: String?,
29
81
  customDimensions customDimensionsJSON: String?,
30
- delegate: ShortKitBridgeDelegateProtocol
82
+ delegate: ShortKitBridgeDelegateProtocol,
83
+ surfacePresenter: AnyObject?
31
84
  ) {
32
85
  self.delegate = delegate
86
+ self.surfacePresenter = surfacePresenter
33
87
  super.init()
34
88
 
35
- let feedConfig = Self.parseFeedConfig(configJSON)
36
89
  let dimensions = Self.parseCustomDimensions(customDimensionsJSON)
37
90
 
38
91
  let sdk = ShortKit(
39
92
  apiKey: apiKey,
40
- config: feedConfig,
41
- embedId: embedId,
42
93
  clientAppName: clientAppName,
43
94
  clientAppVersion: clientAppVersion,
44
95
  customDimensions: dimensions
@@ -47,6 +98,14 @@ import ShortKitSDK
47
98
 
48
99
  ShortKitBridge.shared = self
49
100
 
101
+ if hasLoadingView {
102
+ sdk.loadingViewProvider = { [weak self] in
103
+ let host = ReactLoadingHost()
104
+ host.surfacePresenter = self?.surfacePresenter
105
+ return host
106
+ }
107
+ }
108
+
50
109
  subscribeToPublishers(sdk.player)
51
110
  sdk.delegate = self
52
111
  }
@@ -55,6 +114,9 @@ import ShortKitSDK
55
114
 
56
115
  @objc public func teardown() {
57
116
  cancellables.removeAll()
117
+ preloadHandles.removeAll()
118
+ feedRegistry.removeAll()
119
+ pendingOps.removeAll()
58
120
  shortKit = nil
59
121
  if ShortKitBridge.shared === self {
60
122
  ShortKitBridge.shared = nil
@@ -145,24 +207,105 @@ import ShortKitSDK
145
207
 
146
208
  // MARK: - Custom Feed
147
209
 
148
- @objc public func setFeedItems(_ json: String) {
149
- guard let items = Self.parseCustomFeedItems(json) else { return }
150
- Task { @MainActor in
151
- self.shortKit?.setFeedItems(items)
210
+ @objc public func setFeedItems(_ feedId: String, items json: String) {
211
+ guard let items = Self.parseFeedInputs(json) else { return }
212
+ DispatchQueue.main.async { [weak self] in
213
+ guard let self else { return }
214
+ if let vc = self.feedViewController(for: feedId) {
215
+ vc.setFeedItems(items)
216
+ } else {
217
+ self.pendingOps[feedId, default: []].append { vc in
218
+ vc.setFeedItems(items)
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ @objc public func appendFeedItems(_ feedId: String, items json: String) {
225
+ guard let items = Self.parseFeedInputs(json) else { return }
226
+ DispatchQueue.main.async { [weak self] in
227
+ guard let self else { return }
228
+ if let vc = self.feedViewController(for: feedId) {
229
+ vc.appendFeedItems(items)
230
+ } else {
231
+ self.pendingOps[feedId, default: []].append { vc in
232
+ vc.appendFeedItems(items)
233
+ }
234
+ }
152
235
  }
153
236
  }
154
237
 
155
- @objc public func appendFeedItems(_ json: String) {
156
- guard let items = Self.parseCustomFeedItems(json) else { return }
157
- Task { @MainActor in
158
- self.shortKit?.appendFeedItems(items)
238
+ // MARK: - Storyboard / Seek Thumbnails
239
+
240
+ @objc public func prefetchStoryboard(_ playbackId: String) {
241
+ Task {
242
+ _ = await StoryboardProvider.shared.fetch(playbackId: playbackId)
243
+ }
244
+ }
245
+
246
+ @objc public func getStoryboardData(_ playbackId: String, completion: @escaping (String?) -> Void) {
247
+ Task {
248
+ // Try cache first, otherwise fetch
249
+ let storyboard: CachedStoryboard?
250
+ if let cached = StoryboardProvider.shared.cached(for: playbackId) {
251
+ storyboard = cached
252
+ } else {
253
+ storyboard = await StoryboardProvider.shared.fetch(playbackId: playbackId)
254
+ }
255
+ guard let meta = storyboard?.metadata else {
256
+ completion(nil)
257
+ return
258
+ }
259
+ // Compute sprite sheet dimensions from tile positions
260
+ var maxX = 0, maxY = 0
261
+ for tile in meta.tiles {
262
+ if tile.x > maxX { maxX = tile.x }
263
+ if tile.y > maxY { maxY = tile.y }
264
+ }
265
+ let imageWidth = maxX + meta.tileWidth
266
+ let imageHeight = maxY + meta.tileHeight
267
+ // Build JSON matching the shape the JS side expects
268
+ var tilesArr: [[String: Any]] = []
269
+ for tile in meta.tiles {
270
+ tilesArr.append(["start": tile.start, "x": tile.x, "y": tile.y])
271
+ }
272
+ let result: [String: Any] = [
273
+ "url": meta.url,
274
+ "tileWidth": meta.tileWidth,
275
+ "tileHeight": meta.tileHeight,
276
+ "duration": meta.duration,
277
+ "imageWidth": imageWidth,
278
+ "imageHeight": imageHeight,
279
+ "tiles": tilesArr,
280
+ ]
281
+ if let data = try? JSONSerialization.data(withJSONObject: result),
282
+ let json = String(data: data, encoding: .utf8) {
283
+ completion(json)
284
+ } else {
285
+ completion(nil)
286
+ }
287
+ }
288
+ }
289
+
290
+ @objc public func applyFilter(_ feedId: String, filterJSON: String?) {
291
+ let filter = filterJSON.flatMap { Self.parseFeedFilter($0) }
292
+ DispatchQueue.main.async { [weak self] in
293
+ guard let self else { return }
294
+ if let vc = self.feedViewController(for: feedId) {
295
+ vc.applyFilter(filter)
296
+ } else {
297
+ self.pendingOps[feedId, default: []].append { vc in
298
+ vc.applyFilter(filter)
299
+ }
300
+ }
159
301
  }
160
302
  }
161
303
 
162
- @objc public func fetchContent(_ limit: Int, completion: @escaping (String) -> Void) {
304
+ @objc public func fetchContent(_ limit: Int, filterJSON: String?, completion: @escaping (String) -> Void) {
305
+ let filter = filterJSON.flatMap { Self.parseFeedFilter($0) }
163
306
  Task {
164
307
  do {
165
- let items = try await self.shortKit?.fetchContent(limit: limit) ?? []
308
+ let items = try await self.shortKit?.fetchContent(limit: limit, filter: filter) ?? []
166
309
  let data = try JSONEncoder().encode(items)
167
310
  let json = String(data: data, encoding: .utf8) ?? "[]"
168
311
  completion(json)
@@ -172,6 +315,22 @@ import ShortKitSDK
172
315
  }
173
316
  }
174
317
 
318
+ @objc public func preloadFeed(_ configJSON: String, completion: @escaping (String) -> Void) {
319
+ let config = Self.parseFeedConfig(configJSON)
320
+ guard let preload = shortKit?.preloadFeed(filter: config.filter) else {
321
+ completion("")
322
+ return
323
+ }
324
+ let uuid = UUID().uuidString
325
+ preloadHandles[uuid] = preload
326
+ completion(uuid)
327
+ }
328
+
329
+ /// Consume a preload handle by ID. Returns and removes the handle.
330
+ public func consumePreload(id: String) -> FeedPreload? {
331
+ return preloadHandles.removeValue(forKey: id)
332
+ }
333
+
175
334
  // MARK: - Combine Subscriptions
176
335
 
177
336
  private func subscribeToPublishers(_ player: ShortKitPlayer) {
@@ -288,6 +447,24 @@ import ShortKitSDK
288
447
  }
289
448
  .store(in: &cancellables)
290
449
 
450
+ // Feed scroll phase
451
+ player.feedScrollPhase
452
+ .receive(on: DispatchQueue.main)
453
+ .sink { [weak self] phase in
454
+ switch phase {
455
+ case .dragging(let fromId):
456
+ self?.emit("onFeedScrollPhase", body: [
457
+ "phase": "dragging",
458
+ "fromId": fromId
459
+ ])
460
+ case .settled:
461
+ self?.emit("onFeedScrollPhase", body: [
462
+ "phase": "settled"
463
+ ])
464
+ }
465
+ }
466
+ .store(in: &cancellables)
467
+
291
468
  // Format change
292
469
  player.formatChange
293
470
  .receive(on: DispatchQueue.main)
@@ -310,22 +487,16 @@ import ShortKitSDK
310
487
  }
311
488
  .store(in: &cancellables)
312
489
 
313
- // Remaining content count
314
- player.remainingContentCount
315
- .receive(on: DispatchQueue.main)
316
- .sink { [weak self] count in
317
- self?.emit("onRemainingContentCountChanged", body: ["count": count])
318
- }
319
- .store(in: &cancellables)
490
+ // Remaining content count — now handled per-feed via onRemainingContentCountChange callback
320
491
  }
321
492
 
322
493
  // MARK: - Event Emission Helpers
323
494
 
324
- private func emit(_ name: String, body: [String: Any]) {
495
+ func emit(_ name: String, body: [String: Any]) {
325
496
  delegate?.emitEvent(name, body: body)
326
497
  }
327
498
 
328
- private func emitOnMain(_ name: String, body: [String: Any]) {
499
+ func emitOnMain(_ name: String, body: [String: Any]) {
329
500
  if Thread.isMainThread {
330
501
  emit(name, body: body)
331
502
  } else {
@@ -335,32 +506,6 @@ import ShortKitSDK
335
506
  }
336
507
  }
337
508
 
338
- // MARK: - Overlay Lifecycle Events (called by Fabric view in Task 13)
339
-
340
- /// Emit overlay lifecycle events from the Fabric view's overlay container.
341
- public func emitOverlayEvent(_ name: String, item: ContentItem) {
342
- emitOnMain(name, body: ["item": serializeContentItemToJSON(item)])
343
- }
344
-
345
- /// Emit a raw overlay event with an arbitrary body.
346
- public func emitOverlayEvent(_ name: String, body: [String: Any]) {
347
- emitOnMain(name, body: body)
348
- }
349
-
350
- // MARK: - Carousel Overlay Lifecycle Events
351
-
352
- /// Emit carousel overlay lifecycle events with an ImageCarouselItem.
353
- public func emitCarouselOverlayEvent(_ name: String, item: ImageCarouselItem) {
354
- guard let data = try? JSONEncoder().encode(item),
355
- let json = String(data: data, encoding: .utf8) else { return }
356
- emitOnMain(name, body: ["item": json])
357
- }
358
-
359
- /// Emit a raw carousel overlay event with an arbitrary body.
360
- public func emitCarouselOverlayEvent(_ name: String, body: [String: Any]) {
361
- emitOnMain(name, body: body)
362
- }
363
-
364
509
  // MARK: - Content Item Serialization
365
510
 
366
511
  /// Serialize a ContentItem to a JSON string for bridge transport.
@@ -415,6 +560,9 @@ import ShortKitSDK
415
560
  if let commentCount = item.commentCount {
416
561
  dict["commentCount"] = commentCount
417
562
  }
563
+ if let fallbackUrl = item.fallbackUrl {
564
+ dict["fallbackUrl"] = fallbackUrl
565
+ }
418
566
 
419
567
  return dict
420
568
  }
@@ -458,7 +606,7 @@ import ShortKitSDK
458
606
  /// {"feedHeight":"{\"type\":\"fullscreen\"}","overlay":"\"none\"",
459
607
  /// "carouselMode":"\"none\"","surveyMode":"\"none\"","muteOnStart":true}
460
608
  /// ```
461
- private static func parseFeedConfig(_ json: String) -> FeedConfig {
609
+ public static func parseFeedConfig(_ json: String) -> FeedConfig {
462
610
  guard let data = json.data(using: .utf8),
463
611
  let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
464
612
  return FeedConfig()
@@ -466,6 +614,7 @@ import ShortKitSDK
466
614
 
467
615
  let feedHeight = parseFeedHeight(obj["feedHeight"] as? String)
468
616
  let muteOnStart = obj["muteOnStart"] as? Bool ?? true
617
+ let autoplay = obj["autoplay"] as? Bool ?? true
469
618
  let videoOverlay = parseVideoOverlay(obj["overlay"] as? String)
470
619
 
471
620
  let feedSourceStr = obj["feedSource"] as? String ?? "algorithmic"
@@ -473,6 +622,8 @@ import ShortKitSDK
473
622
 
474
623
  let carouselOverlay = parseCarouselOverlay(obj["carouselOverlay"] as? String)
475
624
 
625
+ let filter = parseFeedFilter(obj["filter"] as? String)
626
+
476
627
  return FeedConfig(
477
628
  feedHeight: feedHeight,
478
629
  videoOverlay: videoOverlay,
@@ -480,7 +631,9 @@ import ShortKitSDK
480
631
  surveyOverlay: .none,
481
632
  adOverlay: .none,
482
633
  muteOnStart: muteOnStart,
483
- feedSource: feedSource
634
+ autoplay: autoplay,
635
+ feedSource: feedSource,
636
+ filter: filter
484
637
  )
485
638
  }
486
639
 
@@ -488,7 +641,7 @@ import ShortKitSDK
488
641
  ///
489
642
  /// Examples:
490
643
  /// - `"\"none\""` → `.none`
491
- /// - `"{\"type\":\"custom\"}"` → `.custom { ShortKitOverlayBridge() }`
644
+ /// - `"{\"type\":\"custom\"}"` → `.custom { ReactOverlayHost() }`
492
645
  private static func parseVideoOverlay(_ json: String?) -> VideoOverlayMode {
493
646
  guard let json,
494
647
  let data = json.data(using: .utf8),
@@ -505,10 +658,13 @@ import ShortKitSDK
505
658
  if let obj = parsed as? [String: Any],
506
659
  let type = obj["type"] as? String,
507
660
  type == "custom" {
661
+ let name = obj["name"] as? String ?? "Default"
508
662
  return .custom { @Sendable in
509
- let overlay = ShortKitOverlayBridge()
510
- overlay.bridge = ShortKitBridge.shared
511
- return overlay
663
+ let host = ReactOverlayHost()
664
+ host.surfacePresenter = ShortKitBridge.shared?.surfacePresenter
665
+ host.overlayModuleName = "ShortKitOverlay_\(name)"
666
+ host.bridge = ShortKitBridge.shared
667
+ return host
512
668
  }
513
669
  }
514
670
 
@@ -519,7 +675,7 @@ import ShortKitSDK
519
675
  ///
520
676
  /// Examples:
521
677
  /// - `"\"none\""` → `.none`
522
- /// - `"{\"type\":\"custom\"}"` → `.custom { ShortKitCarouselOverlayBridge() }`
678
+ /// - `"{\"type\":\"custom\",\"name\":\"news\"}"` → `.custom { ReactCarouselOverlayHost() }`
523
679
  private static func parseCarouselOverlay(_ json: String?) -> CarouselOverlayMode {
524
680
  guard let json,
525
681
  let data = json.data(using: .utf8),
@@ -534,10 +690,16 @@ import ShortKitSDK
534
690
  if let obj = parsed as? [String: Any],
535
691
  let type = obj["type"] as? String,
536
692
  type == "custom" {
693
+ let name = obj["name"] as? String ?? "Default"
537
694
  return .custom { @Sendable in
538
- let overlay = ShortKitCarouselOverlayBridge()
539
- overlay.bridge = ShortKitBridge.shared
540
- return overlay
695
+ let host = ReactCarouselOverlayHost()
696
+ host.surfacePresenter = ShortKitBridge.shared?.surfacePresenter
697
+ host.bridge = ShortKitBridge.shared
698
+ host.carouselOverlayModuleName = "ShortKitCarouselOverlay_\(name)"
699
+ // Eagerly create the RN surface so it's mounted and ready before
700
+ // the cell scrolls into view, matching video overlay behaviour.
701
+ host.prepareSurface()
702
+ return host
541
703
  }
542
704
  }
543
705
 
@@ -565,20 +727,38 @@ import ShortKitSDK
565
727
  }
566
728
  }
567
729
 
568
- /// Parse a JSON string of CustomFeedItem[] from the JS bridge.
569
- private static func parseCustomFeedItems(_ json: String) -> [CustomFeedItem]? {
730
+ /// Parse a JSON string of FeedFilter from the JS bridge.
731
+ private static func parseFeedFilter(_ json: String?) -> FeedFilter? {
732
+ guard let json,
733
+ let data = json.data(using: .utf8),
734
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
735
+ return nil
736
+ }
737
+
738
+ return FeedFilter(
739
+ tags: obj["tags"] as? [String],
740
+ section: obj["section"] as? String,
741
+ author: obj["author"] as? String,
742
+ contentType: obj["contentType"] as? String,
743
+ metadata: obj["metadata"] as? [String: String]
744
+ )
745
+ }
746
+
747
+ /// Parse a JSON string of FeedInput[] from the JS bridge.
748
+ private static func parseFeedInputs(_ json: String) -> [FeedInput]? {
570
749
  guard let data = json.data(using: .utf8),
571
750
  let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
572
751
  return nil
573
752
  }
574
753
 
575
- var result: [CustomFeedItem] = []
754
+ var result: [FeedInput] = []
576
755
  for obj in arr {
577
756
  guard let type = obj["type"] as? String else { continue }
578
757
  switch type {
579
758
  case "video":
580
759
  guard let playbackId = obj["playbackId"] as? String else { continue }
581
- result.append(.video(playbackId: playbackId))
760
+ let fallbackUrl = obj["fallbackUrl"] as? String
761
+ result.append(.video(playbackId: playbackId, fallbackUrl: fallbackUrl))
582
762
  case "imageCarousel":
583
763
  guard let itemData = obj["item"],
584
764
  let itemJSON = try? JSONSerialization.data(withJSONObject: itemData),
@@ -613,4 +793,25 @@ extension ShortKitBridge: ShortKitDelegate {
613
793
  "index": index
614
794
  ])
615
795
  }
796
+
797
+ public func shortKitDidRequestRefresh(_ shortKit: ShortKit) {
798
+ emitOnMain("onRefreshRequested", body: [:])
799
+ }
800
+
801
+ public func shortKit(_ shortKit: ShortKit, didFetchContentItems items: [ContentItem]) {
802
+ Task {
803
+ let data = try? JSONEncoder().encode(items)
804
+ let json = data.flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
805
+ self.emitOnMain("onDidFetchContentItems", body: ["items": json])
806
+ }
807
+ }
616
808
  }
809
+
810
+ // MARK: - Dismiss Emission
811
+
812
+ extension ShortKitBridge {
813
+ @objc public func emitDismiss() {
814
+ emitOnMain("onDismiss", body: [:])
815
+ }
816
+ }
817
+