@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
@@ -18,25 +18,87 @@ 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
+ /// Tracks the most recently created preload or registered feed ID for feedReady routing.
34
+ private var activeFeedId: String = ""
35
+
36
+ // MARK: - Feed Instance Registry
37
+
38
+ private struct WeakFeedRef {
39
+ weak var vc: ShortKitFeedViewController?
40
+ }
41
+
42
+ private var feedRegistry: [String: WeakFeedRef] = [:]
43
+ private var pendingOps: [String: [(ShortKitFeedViewController) -> Void]] = [:]
44
+
45
+ public func registerFeed(id: String, viewController vc: ShortKitFeedViewController) {
46
+ feedRegistry[id] = WeakFeedRef(vc: vc)
47
+ activeFeedId = id
48
+
49
+ // Wire per-feed remaining content count callback
50
+ vc.onRemainingContentCountChange = { [weak self] count in
51
+ self?.emit("onRemainingContentCountChanged", body: [
52
+ "feedId": id,
53
+ "count": count
54
+ ])
55
+ }
56
+
57
+ // Wire per-feed ready callback
58
+ vc.onFeedReady = { [weak self] in
59
+ self?.emitOnMain("onFeedReady", body: ["feedId": id])
60
+ }
61
+
62
+ // Replay buffered operations on the next run-loop tick so the VC's
63
+ // view hierarchy is fully set up after didMoveToWindow returns.
64
+ if let ops = pendingOps.removeValue(forKey: id) {
65
+ DispatchQueue.main.async { [weak vc] in
66
+ guard let vc = vc else { return }
67
+ for op in ops {
68
+ op(vc)
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ public func unregisterFeed(id: String) {
75
+ feedRegistry.removeValue(forKey: id)
76
+ // Note: pendingOps are preserved for this ID to survive detach/reattach cycles
77
+ }
78
+
79
+ private func feedViewController(for id: String) -> ShortKitFeedViewController? {
80
+ return feedRegistry[id]?.vc
81
+ }
82
+
21
83
  // MARK: - Init
22
84
 
23
85
  @objc public init(
24
86
  apiKey: String,
25
- config configJSON: String,
87
+ hasLoadingView: Bool,
26
88
  clientAppName: String?,
27
89
  clientAppVersion: String?,
28
90
  customDimensions customDimensionsJSON: String?,
29
- delegate: ShortKitBridgeDelegateProtocol
91
+ delegate: ShortKitBridgeDelegateProtocol,
92
+ surfacePresenter: AnyObject?
30
93
  ) {
31
94
  self.delegate = delegate
95
+ self.surfacePresenter = surfacePresenter
32
96
  super.init()
33
97
 
34
- let feedConfig = Self.parseFeedConfig(configJSON)
35
98
  let dimensions = Self.parseCustomDimensions(customDimensionsJSON)
36
99
 
37
100
  let sdk = ShortKit(
38
101
  apiKey: apiKey,
39
- config: feedConfig,
40
102
  clientAppName: clientAppName,
41
103
  clientAppVersion: clientAppVersion,
42
104
  customDimensions: dimensions
@@ -45,6 +107,14 @@ import ShortKitSDK
45
107
 
46
108
  ShortKitBridge.shared = self
47
109
 
110
+ if hasLoadingView {
111
+ sdk.loadingViewProvider = { [weak self] in
112
+ let host = ReactLoadingHost()
113
+ host.surfacePresenter = self?.surfacePresenter
114
+ return host
115
+ }
116
+ }
117
+
48
118
  subscribeToPublishers(sdk.player)
49
119
  sdk.delegate = self
50
120
  }
@@ -53,6 +123,9 @@ import ShortKitSDK
53
123
 
54
124
  @objc public func teardown() {
55
125
  cancellables.removeAll()
126
+ preloadHandles.removeAll()
127
+ feedRegistry.removeAll()
128
+ pendingOps.removeAll()
56
129
  shortKit = nil
57
130
  if ShortKitBridge.shared === self {
58
131
  ShortKitBridge.shared = nil
@@ -143,17 +216,31 @@ import ShortKitSDK
143
216
 
144
217
  // MARK: - Custom Feed
145
218
 
146
- @objc public func setFeedItems(_ json: String) {
219
+ @objc public func setFeedItems(_ feedId: String, items json: String) {
147
220
  guard let items = Self.parseFeedInputs(json) else { return }
148
- Task { @MainActor in
149
- self.shortKit?.setFeedItems(items)
221
+ DispatchQueue.main.async { [weak self] in
222
+ guard let self else { return }
223
+ if let vc = self.feedViewController(for: feedId) {
224
+ vc.setFeedItems(items)
225
+ } else {
226
+ self.pendingOps[feedId, default: []].append { vc in
227
+ vc.setFeedItems(items)
228
+ }
229
+ }
150
230
  }
151
231
  }
152
232
 
153
- @objc public func appendFeedItems(_ json: String) {
233
+ @objc public func appendFeedItems(_ feedId: String, items json: String) {
154
234
  guard let items = Self.parseFeedInputs(json) else { return }
155
- Task { @MainActor in
156
- self.shortKit?.appendFeedItems(items)
235
+ DispatchQueue.main.async { [weak self] in
236
+ guard let self else { return }
237
+ if let vc = self.feedViewController(for: feedId) {
238
+ vc.appendFeedItems(items)
239
+ } else {
240
+ self.pendingOps[feedId, default: []].append { vc in
241
+ vc.appendFeedItems(items)
242
+ }
243
+ }
157
244
  }
158
245
  }
159
246
 
@@ -209,14 +296,25 @@ import ShortKitSDK
209
296
  }
210
297
  }
211
298
 
212
- @objc public func notifyOverlayReady() {
213
- NotificationCenter.default.post(name: .shortKitOverlayReady, object: nil)
299
+ @objc public func applyFilter(_ feedId: String, filterJSON: String?) {
300
+ let filter = filterJSON.flatMap { Self.parseFeedFilter($0) }
301
+ DispatchQueue.main.async { [weak self] in
302
+ guard let self else { return }
303
+ if let vc = self.feedViewController(for: feedId) {
304
+ vc.applyFilter(filter)
305
+ } else {
306
+ self.pendingOps[feedId, default: []].append { vc in
307
+ vc.applyFilter(filter)
308
+ }
309
+ }
310
+ }
214
311
  }
215
312
 
216
- @objc public func fetchContent(_ limit: Int, completion: @escaping (String) -> Void) {
313
+ @objc public func fetchContent(_ limit: Int, filterJSON: String?, completion: @escaping (String) -> Void) {
314
+ let filter = filterJSON.flatMap { Self.parseFeedFilter($0) }
217
315
  Task {
218
316
  do {
219
- let items = try await self.shortKit?.fetchContent(limit: limit) ?? []
317
+ let items = try await self.shortKit?.fetchContent(limit: limit, filter: filter) ?? []
220
318
  let data = try JSONEncoder().encode(items)
221
319
  let json = String(data: data, encoding: .utf8) ?? "[]"
222
320
  completion(json)
@@ -226,6 +324,43 @@ import ShortKitSDK
226
324
  }
227
325
  }
228
326
 
327
+ @objc public func preloadFeed(_ configJSON: String, itemsJSON: String?, completion: @escaping (String) -> Void) {
328
+ let config = Self.parseFeedConfig(configJSON)
329
+ let preload: FeedPreload?
330
+
331
+ NSLog("[ShortKit Bridge] preloadFeed called — feedSource: %@, hasItemsJSON: %@",
332
+ config.feedSource == .custom ? "custom" : "algorithmic",
333
+ itemsJSON != nil ? "yes (\(itemsJSON!.prefix(100))...)" : "no")
334
+
335
+ if config.feedSource == .custom {
336
+ guard let json = itemsJSON, let items = Self.parseFeedInputs(json) else {
337
+ NSLog("[ShortKit Bridge] ❌ preloadFeed: feedSource=custom but no valid items — returning empty")
338
+ completion("")
339
+ return
340
+ }
341
+ NSLog("[ShortKit Bridge] preloadFeed: creating custom preload with %d items", items.count)
342
+ preload = shortKit?.preloadFeed(items: items)
343
+ } else {
344
+ preload = shortKit?.preloadFeed(filter: config.filter)
345
+ }
346
+
347
+ guard let preload else {
348
+ NSLog("[ShortKit Bridge] ❌ preloadFeed: shortKit?.preloadFeed returned nil")
349
+ completion("")
350
+ return
351
+ }
352
+ let uuid = UUID().uuidString
353
+ NSLog("[ShortKit Bridge] ✅ preloadFeed: created handle %@", uuid)
354
+ preloadHandles[uuid] = preload
355
+ activeFeedId = uuid
356
+ completion(uuid)
357
+ }
358
+
359
+ /// Consume a preload handle by ID. Returns and removes the handle.
360
+ public func consumePreload(id: String) -> FeedPreload? {
361
+ return preloadHandles.removeValue(forKey: id)
362
+ }
363
+
229
364
  // MARK: - Combine Subscriptions
230
365
 
231
366
  private func subscribeToPublishers(_ player: ShortKitPlayer) {
@@ -382,22 +517,16 @@ import ShortKitSDK
382
517
  }
383
518
  .store(in: &cancellables)
384
519
 
385
- // Remaining content count
386
- player.remainingContentCount
387
- .receive(on: DispatchQueue.main)
388
- .sink { [weak self] count in
389
- self?.emit("onRemainingContentCountChanged", body: ["count": count])
390
- }
391
- .store(in: &cancellables)
520
+ // Remaining content count — now handled per-feed via onRemainingContentCountChange callback
392
521
  }
393
522
 
394
523
  // MARK: - Event Emission Helpers
395
524
 
396
- private func emit(_ name: String, body: [String: Any]) {
525
+ func emit(_ name: String, body: [String: Any]) {
397
526
  delegate?.emitEvent(name, body: body)
398
527
  }
399
528
 
400
- private func emitOnMain(_ name: String, body: [String: Any]) {
529
+ func emitOnMain(_ name: String, body: [String: Any]) {
401
530
  if Thread.isMainThread {
402
531
  emit(name, body: body)
403
532
  } else {
@@ -407,32 +536,6 @@ import ShortKitSDK
407
536
  }
408
537
  }
409
538
 
410
- // MARK: - Overlay Lifecycle Events (called by Fabric view in Task 13)
411
-
412
- /// Emit overlay lifecycle events from the Fabric view's overlay container.
413
- public func emitOverlayEvent(_ name: String, item: ContentItem) {
414
- emitOnMain(name, body: ["item": serializeContentItemToJSON(item)])
415
- }
416
-
417
- /// Emit a raw overlay event with an arbitrary body.
418
- public func emitOverlayEvent(_ name: String, body: [String: Any]) {
419
- emitOnMain(name, body: body)
420
- }
421
-
422
- // MARK: - Carousel Overlay Lifecycle Events
423
-
424
- /// Emit carousel overlay lifecycle events with an ImageCarouselItem.
425
- public func emitCarouselOverlayEvent(_ name: String, item: ImageCarouselItem) {
426
- guard let data = try? JSONEncoder().encode(item),
427
- let json = String(data: data, encoding: .utf8) else { return }
428
- emitOnMain(name, body: ["item": json])
429
- }
430
-
431
- /// Emit a raw carousel overlay event with an arbitrary body.
432
- public func emitCarouselOverlayEvent(_ name: String, body: [String: Any]) {
433
- emitOnMain(name, body: body)
434
- }
435
-
436
539
  // MARK: - Content Item Serialization
437
540
 
438
541
  /// Serialize a ContentItem to a JSON string for bridge transport.
@@ -487,6 +590,9 @@ import ShortKitSDK
487
590
  if let commentCount = item.commentCount {
488
591
  dict["commentCount"] = commentCount
489
592
  }
593
+ if let fallbackUrl = item.fallbackUrl {
594
+ dict["fallbackUrl"] = fallbackUrl
595
+ }
490
596
 
491
597
  return dict
492
598
  }
@@ -530,7 +636,7 @@ import ShortKitSDK
530
636
  /// {"feedHeight":"{\"type\":\"fullscreen\"}","overlay":"\"none\"",
531
637
  /// "carouselMode":"\"none\"","surveyMode":"\"none\"","muteOnStart":true}
532
638
  /// ```
533
- private static func parseFeedConfig(_ json: String) -> FeedConfig {
639
+ public static func parseFeedConfig(_ json: String) -> FeedConfig {
534
640
  guard let data = json.data(using: .utf8),
535
641
  let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
536
642
  return FeedConfig()
@@ -538,6 +644,7 @@ import ShortKitSDK
538
644
 
539
645
  let feedHeight = parseFeedHeight(obj["feedHeight"] as? String)
540
646
  let muteOnStart = obj["muteOnStart"] as? Bool ?? true
647
+ let autoplay = obj["autoplay"] as? Bool ?? true
541
648
  let videoOverlay = parseVideoOverlay(obj["overlay"] as? String)
542
649
 
543
650
  let feedSourceStr = obj["feedSource"] as? String ?? "algorithmic"
@@ -545,14 +652,22 @@ import ShortKitSDK
545
652
 
546
653
  let carouselOverlay = parseCarouselOverlay(obj["carouselOverlay"] as? String)
547
654
 
655
+ let filter = parseFeedFilter(obj["filter"] as? String)
656
+
657
+ let scrollAxisStr = obj["scrollAxis"] as? String ?? "vertical"
658
+ let scrollAxis: ScrollAxis = scrollAxisStr == "horizontal" ? .horizontal : .vertical
659
+
548
660
  return FeedConfig(
549
661
  feedHeight: feedHeight,
662
+ scrollAxis: scrollAxis,
550
663
  videoOverlay: videoOverlay,
551
664
  carouselOverlay: carouselOverlay,
552
665
  surveyOverlay: .none,
553
666
  adOverlay: .none,
554
667
  muteOnStart: muteOnStart,
555
- feedSource: feedSource
668
+ autoplay: autoplay,
669
+ feedSource: feedSource,
670
+ filter: filter
556
671
  )
557
672
  }
558
673
 
@@ -560,7 +675,7 @@ import ShortKitSDK
560
675
  ///
561
676
  /// Examples:
562
677
  /// - `"\"none\""` → `.none`
563
- /// - `"{\"type\":\"custom\"}"` → `.custom { ShortKitOverlayBridge() }`
678
+ /// - `"{\"type\":\"custom\"}"` → `.custom { ReactOverlayHost() }`
564
679
  private static func parseVideoOverlay(_ json: String?) -> VideoOverlayMode {
565
680
  guard let json,
566
681
  let data = json.data(using: .utf8),
@@ -577,10 +692,13 @@ import ShortKitSDK
577
692
  if let obj = parsed as? [String: Any],
578
693
  let type = obj["type"] as? String,
579
694
  type == "custom" {
695
+ let name = obj["name"] as? String ?? "Default"
580
696
  return .custom { @Sendable in
581
- let overlay = ShortKitOverlayBridge()
582
- overlay.bridge = ShortKitBridge.shared
583
- return overlay
697
+ let host = ReactOverlayHost()
698
+ host.surfacePresenter = ShortKitBridge.shared?.surfacePresenter
699
+ host.overlayModuleName = "ShortKitOverlay_\(name)"
700
+ host.bridge = ShortKitBridge.shared
701
+ return host
584
702
  }
585
703
  }
586
704
 
@@ -591,7 +709,7 @@ import ShortKitSDK
591
709
  ///
592
710
  /// Examples:
593
711
  /// - `"\"none\""` → `.none`
594
- /// - `"{\"type\":\"custom\"}"` → `.custom { ShortKitCarouselOverlayBridge() }`
712
+ /// - `"{\"type\":\"custom\",\"name\":\"news\"}"` → `.custom { ReactCarouselOverlayHost() }`
595
713
  private static func parseCarouselOverlay(_ json: String?) -> CarouselOverlayMode {
596
714
  guard let json,
597
715
  let data = json.data(using: .utf8),
@@ -606,10 +724,16 @@ import ShortKitSDK
606
724
  if let obj = parsed as? [String: Any],
607
725
  let type = obj["type"] as? String,
608
726
  type == "custom" {
727
+ let name = obj["name"] as? String ?? "Default"
609
728
  return .custom { @Sendable in
610
- let overlay = ShortKitCarouselOverlayBridge()
611
- overlay.bridge = ShortKitBridge.shared
612
- return overlay
729
+ let host = ReactCarouselOverlayHost()
730
+ host.surfacePresenter = ShortKitBridge.shared?.surfacePresenter
731
+ host.bridge = ShortKitBridge.shared
732
+ host.carouselOverlayModuleName = "ShortKitCarouselOverlay_\(name)"
733
+ // Eagerly create the RN surface so it's mounted and ready before
734
+ // the cell scrolls into view, matching video overlay behaviour.
735
+ host.prepareSurface()
736
+ return host
613
737
  }
614
738
  }
615
739
 
@@ -637,6 +761,23 @@ import ShortKitSDK
637
761
  }
638
762
  }
639
763
 
764
+ /// Parse a JSON string of FeedFilter from the JS bridge.
765
+ private static func parseFeedFilter(_ json: String?) -> FeedFilter? {
766
+ guard let json,
767
+ let data = json.data(using: .utf8),
768
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
769
+ return nil
770
+ }
771
+
772
+ return FeedFilter(
773
+ tags: obj["tags"] as? [String],
774
+ section: obj["section"] as? String,
775
+ author: obj["author"] as? String,
776
+ contentType: obj["contentType"] as? String,
777
+ metadata: obj["metadata"] as? [String: String]
778
+ )
779
+ }
780
+
640
781
  /// Parse a JSON string of FeedInput[] from the JS bridge.
641
782
  private static func parseFeedInputs(_ json: String) -> [FeedInput]? {
642
783
  guard let data = json.data(using: .utf8),
@@ -650,7 +791,8 @@ import ShortKitSDK
650
791
  switch type {
651
792
  case "video":
652
793
  guard let playbackId = obj["playbackId"] as? String else { continue }
653
- result.append(.video(playbackId: playbackId))
794
+ let fallbackUrl = obj["fallbackUrl"] as? String
795
+ result.append(.video(playbackId: playbackId, fallbackUrl: fallbackUrl))
654
796
  case "imageCarousel":
655
797
  guard let itemData = obj["item"],
656
798
  let itemJSON = try? JSONSerialization.data(withJSONObject: itemData),
@@ -685,10 +827,25 @@ extension ShortKitBridge: ShortKitDelegate {
685
827
  "index": index
686
828
  ])
687
829
  }
830
+
831
+ public func shortKitDidRequestRefresh(_ shortKit: ShortKit) {
832
+ emitOnMain("onRefreshRequested", body: [:])
833
+ }
834
+
835
+ public func shortKit(_ shortKit: ShortKit, didFetchContentItems items: [ContentItem]) {
836
+ Task {
837
+ let data = try? JSONEncoder().encode(items)
838
+ let json = data.flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
839
+ self.emitOnMain("onDidFetchContentItems", body: ["items": json])
840
+ }
841
+ }
688
842
  }
689
843
 
690
- // MARK: - Notification Names
844
+ // MARK: - Dismiss Emission
691
845
 
692
- extension Notification.Name {
693
- static let shortKitOverlayReady = Notification.Name("ShortKitOverlayReady")
846
+ extension ShortKitBridge {
847
+ @objc public func emitDismiss() {
848
+ emitOnMain("onDismiss", body: [:])
849
+ }
694
850
  }
851
+