@shortkitsdk/react-native 0.2.0 → 0.2.2

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 (34) hide show
  1. package/android/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +48 -0
  2. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +48 -6
  3. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +180 -2
  4. package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +28 -1
  5. package/android/src/main/java/com/shortkit/reactnative/ShortKitPackage.kt +5 -1
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +136 -0
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerViewManager.kt +35 -0
  8. package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +133 -0
  9. package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetViewManager.kt +30 -0
  10. package/ios/ShortKitBridge.swift +134 -2
  11. package/ios/ShortKitCarouselOverlayBridge.swift +54 -0
  12. package/ios/ShortKitFeedView.swift +46 -7
  13. package/ios/ShortKitModule.mm +42 -0
  14. package/ios/ShortKitOverlayBridge.swift +23 -1
  15. package/ios/ShortKitPlayerNativeView.swift +186 -0
  16. package/ios/ShortKitPlayerNativeViewManager.mm +28 -0
  17. package/ios/ShortKitWidgetNativeView.swift +168 -0
  18. package/ios/ShortKitWidgetNativeViewManager.mm +27 -0
  19. package/package.json +1 -1
  20. package/src/CarouselOverlayManager.tsx +71 -0
  21. package/src/ShortKitContext.ts +18 -0
  22. package/src/ShortKitFeed.tsx +13 -0
  23. package/src/ShortKitPlayer.tsx +61 -0
  24. package/src/ShortKitProvider.tsx +161 -2
  25. package/src/ShortKitWidget.tsx +63 -0
  26. package/src/index.ts +15 -1
  27. package/src/serialization.ts +16 -2
  28. package/src/specs/NativeShortKitModule.ts +37 -0
  29. package/src/specs/ShortKitPlayerViewNativeComponent.ts +13 -0
  30. package/src/specs/ShortKitWidgetViewNativeComponent.ts +12 -0
  31. package/src/types.ts +82 -3
  32. package/src/useShortKit.ts +5 -1
  33. package/src/useShortKitCarousel.ts +29 -0
  34. package/src/useShortKitPlayer.ts +10 -2
@@ -0,0 +1,133 @@
1
+ package com.shortkit.reactnative
2
+
3
+ import android.content.Context
4
+ import android.widget.FrameLayout
5
+ import com.shortkit.sdk.config.PlayerClickAction
6
+ import com.shortkit.sdk.config.VideoOverlayMode
7
+ import com.shortkit.sdk.config.WidgetConfig
8
+ import com.shortkit.sdk.model.ContentItem
9
+ import com.shortkit.sdk.widget.ShortKitWidgetView
10
+ import org.json.JSONArray
11
+ import org.json.JSONObject
12
+
13
+ /**
14
+ * Fabric native view wrapping [ShortKitWidgetView] for use as a
15
+ * horizontal video carousel in React Native.
16
+ */
17
+ class ShortKitWidgetNativeView(context: Context) : FrameLayout(context) {
18
+
19
+ private var widgetView: ShortKitWidgetView? = null
20
+ private var configJson: String? = null
21
+ private var itemsJson: String? = null
22
+
23
+ var config: String?
24
+ get() = configJson
25
+ set(value) {
26
+ if (value == configJson) return
27
+ configJson = value
28
+ rebuildIfNeeded()
29
+ }
30
+
31
+ var items: String?
32
+ get() = itemsJson
33
+ set(value) {
34
+ if (value == itemsJson) return
35
+ itemsJson = value
36
+ applyItems()
37
+ }
38
+
39
+ override fun onAttachedToWindow() {
40
+ super.onAttachedToWindow()
41
+ rebuildIfNeeded()
42
+ }
43
+
44
+ override fun onDetachedFromWindow() {
45
+ super.onDetachedFromWindow()
46
+ }
47
+
48
+ private fun rebuildIfNeeded() {
49
+ if (widgetView != null) return
50
+
51
+ val sdk = ShortKitModule.shared?.sdk ?: return
52
+ val widgetConfig = parseWidgetConfig(configJson)
53
+
54
+ val view = ShortKitWidgetView(context).apply {
55
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
56
+ }
57
+ view.initialize(sdk, widgetConfig)
58
+ addView(view)
59
+ widgetView = view
60
+
61
+ applyItems()
62
+ }
63
+
64
+ private fun applyItems() {
65
+ val json = itemsJson ?: return
66
+ val view = widgetView ?: return
67
+ val contentItems = parseContentItems(json) ?: return
68
+ view.configure(contentItems)
69
+ }
70
+
71
+ private fun parseWidgetConfig(json: String?): WidgetConfig {
72
+ if (json.isNullOrEmpty()) return WidgetConfig()
73
+ return try {
74
+ val obj = JSONObject(json)
75
+ WidgetConfig(
76
+ cardCount = obj.optInt("cardCount", 3),
77
+ cardSpacing = obj.optDouble("cardSpacing", 8.0).toFloat(),
78
+ cornerRadius = obj.optDouble("cornerRadius", 12.0).toFloat(),
79
+ autoplay = obj.optBoolean("autoplay", true),
80
+ muteOnStart = obj.optBoolean("muteOnStart", true),
81
+ loop = obj.optBoolean("loop", true),
82
+ rotationInterval = obj.optLong("rotationInterval", 10_000L),
83
+ clickAction = when (obj.optString("clickAction", "feed")) {
84
+ "feed" -> PlayerClickAction.FEED
85
+ "mute" -> PlayerClickAction.MUTE
86
+ "none" -> PlayerClickAction.NONE
87
+ else -> PlayerClickAction.FEED
88
+ },
89
+ cardOverlay = parseOverlay(obj),
90
+ )
91
+ } catch (_: Exception) {
92
+ WidgetConfig()
93
+ }
94
+ }
95
+
96
+ private fun parseOverlay(obj: JSONObject): VideoOverlayMode {
97
+ val overlay = obj.opt("overlay") ?: return VideoOverlayMode.None
98
+ if (overlay is JSONObject && overlay.optString("type") == "custom") {
99
+ return VideoOverlayMode.Custom {
100
+ ShortKitOverlayBridge(context)
101
+ }
102
+ }
103
+ return VideoOverlayMode.None
104
+ }
105
+
106
+ private fun parseContentItems(json: String): List<ContentItem>? {
107
+ return try {
108
+ val arr = JSONArray(json)
109
+ val items = mutableListOf<ContentItem>()
110
+ for (i in 0 until arr.length()) {
111
+ val obj = arr.getJSONObject(i)
112
+ items.add(
113
+ ContentItem(
114
+ id = obj.getString("id"),
115
+ title = obj.getString("title"),
116
+ description = obj.optString("description", null),
117
+ duration = obj.getDouble("duration"),
118
+ streamingUrl = obj.getString("streamingUrl"),
119
+ thumbnailUrl = obj.getString("thumbnailUrl"),
120
+ captionTracks = emptyList(),
121
+ customMetadata = null,
122
+ author = obj.optString("author", null),
123
+ articleUrl = obj.optString("articleUrl", null),
124
+ commentCount = if (obj.has("commentCount")) obj.getInt("commentCount") else null,
125
+ )
126
+ )
127
+ }
128
+ items
129
+ } catch (_: Exception) {
130
+ null
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,30 @@
1
+ package com.shortkit.reactnative
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.SimpleViewManager
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.annotations.ReactProp
7
+
8
+ @ReactModule(name = ShortKitWidgetViewManager.REACT_CLASS)
9
+ class ShortKitWidgetViewManager : SimpleViewManager<ShortKitWidgetNativeView>() {
10
+
11
+ override fun getName(): String = REACT_CLASS
12
+
13
+ override fun createViewInstance(context: ThemedReactContext): ShortKitWidgetNativeView {
14
+ return ShortKitWidgetNativeView(context)
15
+ }
16
+
17
+ @ReactProp(name = "config")
18
+ fun setConfig(view: ShortKitWidgetNativeView, config: String?) {
19
+ view.config = config
20
+ }
21
+
22
+ @ReactProp(name = "items")
23
+ fun setItems(view: ShortKitWidgetNativeView, items: String?) {
24
+ view.items = items
25
+ }
26
+
27
+ companion object {
28
+ const val REACT_CLASS = "ShortKitWidgetView"
29
+ }
30
+ }
@@ -23,6 +23,7 @@ import ShortKit
23
23
  @objc public init(
24
24
  apiKey: String,
25
25
  config configJSON: String,
26
+ embedId: String?,
26
27
  clientAppName: String?,
27
28
  clientAppVersion: String?,
28
29
  customDimensions customDimensionsJSON: String?,
@@ -37,6 +38,7 @@ import ShortKit
37
38
  let sdk = ShortKit(
38
39
  apiKey: apiKey,
39
40
  config: feedConfig,
41
+ embedId: embedId,
40
42
  clientAppName: clientAppName,
41
43
  clientAppVersion: clientAppVersion,
42
44
  customDimensions: dimensions
@@ -46,6 +48,7 @@ import ShortKit
46
48
  ShortKitBridge.shared = self
47
49
 
48
50
  subscribeToPublishers(sdk.player)
51
+ sdk.delegate = self
49
52
  }
50
53
 
51
54
  // MARK: - Teardown
@@ -140,6 +143,35 @@ import ShortKit
140
143
  shortKit?.player.setMaxBitrate(Int(bitrate))
141
144
  }
142
145
 
146
+ // MARK: - Custom Feed
147
+
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)
152
+ }
153
+ }
154
+
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)
159
+ }
160
+ }
161
+
162
+ @objc public func fetchContent(_ limit: Int, completion: @escaping (String) -> Void) {
163
+ Task {
164
+ do {
165
+ let items = try await self.shortKit?.fetchContent(limit: limit) ?? []
166
+ let data = try JSONEncoder().encode(items)
167
+ let json = String(data: data, encoding: .utf8) ?? "[]"
168
+ completion(json)
169
+ } catch {
170
+ completion("[]")
171
+ }
172
+ }
173
+ }
174
+
143
175
  // MARK: - Combine Subscriptions
144
176
 
145
177
  private func subscribeToPublishers(_ player: ShortKitPlayer) {
@@ -277,6 +309,14 @@ import ShortKit
277
309
  self?.emit("onPrefetchedAheadCountChanged", body: ["count": count])
278
310
  }
279
311
  .store(in: &cancellables)
312
+
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)
280
320
  }
281
321
 
282
322
  // MARK: - Event Emission Helpers
@@ -307,6 +347,20 @@ import ShortKit
307
347
  emitOnMain(name, body: body)
308
348
  }
309
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
+
310
364
  // MARK: - Content Item Serialization
311
365
 
312
366
  /// Serialize a ContentItem to a JSON string for bridge transport.
@@ -329,6 +383,10 @@ import ShortKit
329
383
  "thumbnailUrl": item.thumbnailUrl,
330
384
  ]
331
385
 
386
+ if let playbackId = item.playbackId {
387
+ dict["playbackId"] = playbackId
388
+ }
389
+
332
390
  if let description = item.description {
333
391
  dict["description"] = description
334
392
  }
@@ -410,13 +468,19 @@ import ShortKit
410
468
  let muteOnStart = obj["muteOnStart"] as? Bool ?? true
411
469
  let videoOverlay = parseVideoOverlay(obj["overlay"] as? String)
412
470
 
471
+ let feedSourceStr = obj["feedSource"] as? String ?? "algorithmic"
472
+ let feedSource: FeedSource = feedSourceStr == "custom" ? .custom : .algorithmic
473
+
474
+ let carouselOverlay = parseCarouselOverlay(obj["carouselOverlay"] as? String)
475
+
413
476
  return FeedConfig(
414
477
  feedHeight: feedHeight,
415
478
  videoOverlay: videoOverlay,
416
- carouselOverlay: .none,
479
+ carouselOverlay: carouselOverlay,
417
480
  surveyOverlay: .none,
418
481
  adOverlay: .none,
419
- muteOnStart: muteOnStart
482
+ muteOnStart: muteOnStart,
483
+ feedSource: feedSource
420
484
  )
421
485
  }
422
486
 
@@ -451,6 +515,35 @@ import ShortKit
451
515
  return .none
452
516
  }
453
517
 
518
+ /// Parse a double-stringified carousel overlay JSON into a `CarouselOverlayMode`.
519
+ ///
520
+ /// Examples:
521
+ /// - `"\"none\""` → `.none`
522
+ /// - `"{\"type\":\"custom\"}"` → `.custom { ShortKitCarouselOverlayBridge() }`
523
+ private static func parseCarouselOverlay(_ json: String?) -> CarouselOverlayMode {
524
+ guard let json,
525
+ let data = json.data(using: .utf8),
526
+ let parsed = try? JSONSerialization.jsonObject(with: data) else {
527
+ return .none
528
+ }
529
+
530
+ if let str = parsed as? String, str == "none" {
531
+ return .none
532
+ }
533
+
534
+ if let obj = parsed as? [String: Any],
535
+ let type = obj["type"] as? String,
536
+ type == "custom" {
537
+ return .custom { @Sendable in
538
+ let overlay = ShortKitCarouselOverlayBridge()
539
+ overlay.bridge = ShortKitBridge.shared
540
+ return overlay
541
+ }
542
+ }
543
+
544
+ return .none
545
+ }
546
+
454
547
  /// Parse a double-stringified feedHeight JSON.
455
548
  /// e.g. `"{\"type\":\"fullscreen\"}"` or `"{\"type\":\"percentage\",\"value\":0.8}"`
456
549
  private static func parseFeedHeight(_ json: String?) -> FeedHeight {
@@ -472,6 +565,34 @@ import ShortKit
472
565
  }
473
566
  }
474
567
 
568
+ /// Parse a JSON string of CustomFeedItem[] from the JS bridge.
569
+ private static func parseCustomFeedItems(_ json: String) -> [CustomFeedItem]? {
570
+ guard let data = json.data(using: .utf8),
571
+ let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
572
+ return nil
573
+ }
574
+
575
+ var result: [CustomFeedItem] = []
576
+ for obj in arr {
577
+ guard let type = obj["type"] as? String else { continue }
578
+ switch type {
579
+ case "video":
580
+ guard let playbackId = obj["playbackId"] as? String else { continue }
581
+ result.append(.video(playbackId: playbackId))
582
+ case "imageCarousel":
583
+ guard let itemData = obj["item"],
584
+ let itemJSON = try? JSONSerialization.data(withJSONObject: itemData),
585
+ let carouselItem = try? JSONDecoder().decode(ImageCarouselItem.self, from: itemJSON) else {
586
+ continue
587
+ }
588
+ result.append(.imageCarousel(carouselItem))
589
+ default:
590
+ continue
591
+ }
592
+ }
593
+ return result.isEmpty ? nil : result
594
+ }
595
+
475
596
  /// Parse optional custom dimensions JSON string into dictionary.
476
597
  private static func parseCustomDimensions(_ json: String?) -> [String: String]? {
477
598
  guard let json,
@@ -482,3 +603,14 @@ import ShortKit
482
603
  return dict
483
604
  }
484
605
  }
606
+
607
+ // MARK: - ShortKitDelegate
608
+
609
+ extension ShortKitBridge: ShortKitDelegate {
610
+ public func shortKit(_ shortKit: ShortKit, didTapContent contentId: String, at index: Int) {
611
+ emitOnMain("onContentTapped", body: [
612
+ "contentId": contentId,
613
+ "index": index
614
+ ])
615
+ }
616
+ }
@@ -0,0 +1,54 @@
1
+ import UIKit
2
+ import ShortKit
3
+
4
+ /// A transparent UIView that conforms to `CarouselOverlay` and bridges
5
+ /// carousel overlay lifecycle calls to JS events via `ShortKitBridge`.
6
+ ///
7
+ /// The actual carousel overlay UI is rendered by React Native on the JS side
8
+ /// through the `CarouselOverlayManager` component. This view simply relays
9
+ /// the SDK lifecycle events so the JS carousel overlay knows when to
10
+ /// configure, activate, reset, etc.
11
+ @objc public class ShortKitCarouselOverlayBridge: UIView, @unchecked Sendable, CarouselOverlay {
12
+
13
+ // MARK: - Bridge Reference
14
+
15
+ /// Weak reference to the bridge, set by the factory closure in `parseFeedConfig`.
16
+ weak var bridge: ShortKitBridge?
17
+
18
+ // MARK: - State
19
+
20
+ /// Stores the last configured ImageCarouselItem so we can pass it with
21
+ /// lifecycle events that don't receive the item as a parameter.
22
+ private var currentItem: ImageCarouselItem?
23
+
24
+ // MARK: - Init
25
+
26
+ override init(frame: CGRect) {
27
+ super.init(frame: frame)
28
+ backgroundColor = .clear
29
+ isUserInteractionEnabled = true
30
+ }
31
+
32
+ required init?(coder: NSCoder) {
33
+ fatalError("init(coder:) is not supported")
34
+ }
35
+
36
+ // MARK: - CarouselOverlay
37
+
38
+ public func configure(with item: ImageCarouselItem) {
39
+ currentItem = item
40
+ bridge?.emitCarouselOverlayEvent("onCarouselOverlayConfigure", item: item)
41
+ }
42
+
43
+ public func resetState() {
44
+ bridge?.emitCarouselOverlayEvent("onCarouselOverlayReset", body: [:])
45
+ }
46
+
47
+ public func fadeOutForTransition() {
48
+ bridge?.emitCarouselOverlayEvent("onCarouselOverlayFadeOut", body: [:])
49
+ }
50
+
51
+ public func restoreFromTransition() {
52
+ bridge?.emitCarouselOverlayEvent("onCarouselOverlayRestore", body: [:])
53
+ }
54
+ }
@@ -27,10 +27,14 @@ import ShortKit
27
27
  // MARK: - Scroll Tracking
28
28
 
29
29
  private var scrollObservation: NSKeyValueObservation?
30
- /// Overlay for the currently active cell (nativeID="overlay-current").
30
+ /// Video overlay for the currently active cell (nativeID="overlay-current").
31
31
  private weak var currentOverlayView: UIView?
32
- /// Overlay for the upcoming cell (nativeID="overlay-next").
32
+ /// Video overlay for the upcoming cell (nativeID="overlay-next").
33
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?
34
38
  /// The page index used for overlay transform calculations.
35
39
  private var currentPage: Int = 0
36
40
  /// Deferred page update to avoid flashing stale metadata.
@@ -94,8 +98,12 @@ import ShortKit
94
98
  scrollObservation = nil
95
99
  currentOverlayView?.transform = .identity
96
100
  nextOverlayView?.transform = .identity
101
+ currentCarouselOverlayView?.transform = .identity
102
+ nextCarouselOverlayView?.transform = .identity
97
103
  currentOverlayView = nil
98
104
  nextOverlayView = nil
105
+ currentCarouselOverlayView = nil
106
+ nextCarouselOverlayView = nil
99
107
 
100
108
  guard let feedVC = feedViewController else { return }
101
109
 
@@ -150,6 +158,8 @@ import ShortKit
150
158
  let h = self.bounds.height
151
159
  self.currentOverlayView?.transform = .identity
152
160
  self.nextOverlayView?.transform = CGAffineTransform(translationX: 0, y: h)
161
+ self.currentCarouselOverlayView?.transform = .identity
162
+ self.nextCarouselOverlayView?.transform = CGAffineTransform(translationX: 0, y: h)
153
163
  }
154
164
  pageUpdateWorkItem = workItem
155
165
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.08, execute: workItem)
@@ -168,17 +178,27 @@ import ShortKit
168
178
  if currentOverlayView == nil || nextOverlayView == nil {
169
179
  findOverlayViews()
170
180
  }
181
+ if currentCarouselOverlayView == nil || nextCarouselOverlayView == nil {
182
+ findCarouselOverlayViews()
183
+ }
184
+
185
+ let translateY = CGAffineTransform(translationX: 0, y: -delta)
171
186
 
172
- // Current overlay follows the active cell
173
- currentOverlayView?.transform = CGAffineTransform(translationX: 0, y: -delta)
187
+ // Current overlays follow the active cell
188
+ currentOverlayView?.transform = translateY
189
+ currentCarouselOverlayView?.transform = translateY
174
190
 
175
- // Next overlay: positioned one page ahead in the scroll direction
191
+ // Next overlays: positioned one page ahead in the scroll direction
176
192
  if delta >= 0 {
177
193
  // Forward scroll (or idle): next cell is below
178
- nextOverlayView?.transform = CGAffineTransform(translationX: 0, y: cellHeight - delta)
194
+ let nextTransform = CGAffineTransform(translationX: 0, y: cellHeight - delta)
195
+ nextOverlayView?.transform = nextTransform
196
+ nextCarouselOverlayView?.transform = nextTransform
179
197
  } else {
180
198
  // Backward scroll: next cell is above
181
- nextOverlayView?.transform = CGAffineTransform(translationX: 0, y: -cellHeight - delta)
199
+ let nextTransform = CGAffineTransform(translationX: 0, y: -cellHeight - delta)
200
+ nextOverlayView?.transform = nextTransform
201
+ nextCarouselOverlayView?.transform = nextTransform
182
202
  }
183
203
  }
184
204
 
@@ -208,6 +228,25 @@ import ShortKit
208
228
  }
209
229
  }
210
230
 
231
+ /// Find the sibling RN carousel overlay views by nativeID.
232
+ private func findCarouselOverlayViews() {
233
+ var ancestor: UIView? = superview
234
+ while let container = ancestor {
235
+ for child in container.subviews {
236
+ guard !self.isDescendant(of: child) else { continue }
237
+
238
+ if currentCarouselOverlayView == nil {
239
+ currentCarouselOverlayView = findView(withNativeID: "carousel-overlay-current", in: child)
240
+ }
241
+ if nextCarouselOverlayView == nil {
242
+ nextCarouselOverlayView = findView(withNativeID: "carousel-overlay-next", in: child)
243
+ }
244
+ }
245
+ if currentCarouselOverlayView != nil && nextCarouselOverlayView != nil { return }
246
+ ancestor = container.superview
247
+ }
248
+ }
249
+
211
250
  /// Recursively find a view by its React Native `nativeID` prop.
212
251
  /// In Fabric, this is stored on `RCTViewComponentView.nativeId`,
213
252
  /// not `accessibilityIdentifier`.
@@ -11,6 +11,8 @@
11
11
  @implementation ShortKitModule {
12
12
  ShortKitBridge *_shortKitBridge;
13
13
  BOOL _hasListeners;
14
+ NSString *_pendingFeedItems;
15
+ NSString *_pendingAppendItems;
14
16
  }
15
17
 
16
18
  RCT_EXPORT_MODULE(ShortKitModule)
@@ -54,6 +56,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
54
56
  @"onFeedTransition",
55
57
  @"onFormatChange",
56
58
  @"onPrefetchedAheadCountChanged",
59
+ @"onRemainingContentCountChanged",
57
60
  @"onError",
58
61
  @"onShareTapped",
59
62
  @"onSurveyResponse",
@@ -64,6 +67,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
64
67
  @"onOverlayRestore",
65
68
  @"onOverlayTap",
66
69
  @"onOverlayDoubleTap",
70
+ @"onContentTapped",
67
71
  ];
68
72
  }
69
73
 
@@ -102,6 +106,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
102
106
 
103
107
  RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
104
108
  config:(NSString *)config
109
+ embedId:(NSString *)embedId
105
110
  clientAppName:(NSString *)clientAppName
106
111
  clientAppVersion:(NSString *)clientAppVersion
107
112
  customDimensions:(NSString *)customDimensions) {
@@ -110,10 +115,21 @@ RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
110
115
 
111
116
  _shortKitBridge = [[ShortKitBridge alloc] initWithApiKey:apiKey
112
117
  config:config
118
+ embedId:embedId
113
119
  clientAppName:clientAppName
114
120
  clientAppVersion:clientAppVersion
115
121
  customDimensions:customDimensions
116
122
  delegate:self];
123
+
124
+ // Replay any feed items that arrived before initialization
125
+ if (_pendingFeedItems) {
126
+ [_shortKitBridge setFeedItems:_pendingFeedItems];
127
+ _pendingFeedItems = nil;
128
+ }
129
+ if (_pendingAppendItems) {
130
+ [_shortKitBridge appendFeedItems:_pendingAppendItems];
131
+ _pendingAppendItems = nil;
132
+ }
117
133
  }
118
134
 
119
135
  RCT_EXPORT_METHOD(destroy) {
@@ -187,6 +203,32 @@ RCT_EXPORT_METHOD(setMaxBitrate:(double)bitrate) {
187
203
  [_shortKitBridge setMaxBitrate:bitrate];
188
204
  }
189
205
 
206
+ // MARK: - Custom Feed
207
+
208
+ RCT_EXPORT_METHOD(setFeedItems:(NSString *)items) {
209
+ if (_shortKitBridge) {
210
+ [_shortKitBridge setFeedItems:items];
211
+ } else {
212
+ _pendingFeedItems = items;
213
+ }
214
+ }
215
+
216
+ RCT_EXPORT_METHOD(appendFeedItems:(NSString *)items) {
217
+ if (_shortKitBridge) {
218
+ [_shortKitBridge appendFeedItems:items];
219
+ } else {
220
+ _pendingAppendItems = items;
221
+ }
222
+ }
223
+
224
+ RCT_EXPORT_METHOD(fetchContent:(NSInteger)limit
225
+ resolve:(RCTPromiseResolveBlock)resolve
226
+ reject:(RCTPromiseRejectBlock)reject) {
227
+ [_shortKitBridge fetchContent:limit completion:^(NSString *json) {
228
+ resolve(json);
229
+ }];
230
+ }
231
+
190
232
  // MARK: - New Architecture (TurboModule)
191
233
 
192
234
  #ifdef RCT_NEW_ARCH_ENABLED
@@ -20,6 +20,11 @@ import ShortKit
20
20
  /// events that don't receive the item as a parameter.
21
21
  private var currentItem: ContentItem?
22
22
 
23
+ /// Deferred configure emission — cancelled if `activatePlayback()` fires
24
+ /// on the same run-loop iteration (meaning this was a handleSwipe
25
+ /// re-configure of the active cell, not a prefetch for the next cell).
26
+ private var pendingConfigureWorkItem: DispatchWorkItem?
27
+
23
28
  // MARK: - Init
24
29
 
25
30
  override init(frame: CGRect) {
@@ -66,7 +71,19 @@ import ShortKit
66
71
 
67
72
  public func configure(with item: ContentItem) {
68
73
  currentItem = item
69
- bridge?.emitOverlayEvent("onOverlayConfigure", item: item)
74
+
75
+ // Defer the configure event by one run-loop tick. If activatePlayback()
76
+ // fires before then (handleSwipe sequence: configure → reset → activate),
77
+ // the event is cancelled — preventing nextItem from being overwritten
78
+ // with the current cell's data.
79
+ pendingConfigureWorkItem?.cancel()
80
+ let workItem = DispatchWorkItem { [weak self] in
81
+ guard let self, let item = self.currentItem else { return }
82
+ self.bridge?.emitOverlayEvent("onOverlayConfigure", item: item)
83
+ self.pendingConfigureWorkItem = nil
84
+ }
85
+ pendingConfigureWorkItem = workItem
86
+ DispatchQueue.main.async(execute: workItem)
70
87
  }
71
88
 
72
89
  public func resetPlaybackProgress() {
@@ -75,6 +92,11 @@ import ShortKit
75
92
  }
76
93
 
77
94
  public func activatePlayback() {
95
+ // Cancel the pending configure — it was for the current cell (part of
96
+ // handleSwipe), not a prefetch for the next cell.
97
+ pendingConfigureWorkItem?.cancel()
98
+ pendingConfigureWorkItem = nil
99
+
78
100
  guard let item = currentItem else { return }
79
101
  bridge?.emitOverlayEvent("onOverlayActivate", item: item)
80
102
  }