@shortkitsdk/react-native 0.2.0 → 0.2.1

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.
@@ -5,10 +5,14 @@ import com.facebook.react.bridge.ReactApplicationContext
5
5
  import com.facebook.react.bridge.ReactMethod
6
6
  import com.facebook.react.bridge.WritableMap
7
7
  import com.facebook.react.modules.core.DeviceEventManagerModule
8
+ import com.shortkit.CarouselImage
8
9
  import com.shortkit.ContentItem
9
10
  import com.shortkit.ContentSignal
11
+ import com.shortkit.CustomFeedItem
12
+ import com.shortkit.ImageCarouselItem
10
13
  import com.shortkit.FeedConfig
11
14
  import com.shortkit.FeedHeight
15
+ import com.shortkit.FeedSource
12
16
  import com.shortkit.FeedTransitionPhase
13
17
  import com.shortkit.JsonValue
14
18
  import com.shortkit.ShortKit
@@ -89,6 +93,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
89
93
  override fun initialize(
90
94
  apiKey: String,
91
95
  config: String,
96
+ embedId: String?,
92
97
  clientAppName: String?,
93
98
  clientAppVersion: String?,
94
99
  customDimensions: String?
@@ -105,6 +110,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
105
110
  context = context,
106
111
  apiKey = apiKey,
107
112
  config = feedConfig,
113
+ embedId = embedId,
108
114
  userId = null,
109
115
  adProvider = null,
110
116
  clientAppName = clientAppName,
@@ -115,6 +121,16 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
115
121
  shared = this
116
122
 
117
123
  subscribeToFlows(sdk.player)
124
+
125
+ sdk.delegate = object : com.shortkit.ShortKitDelegate {
126
+ override fun onContentTapped(contentId: String, index: Int) {
127
+ val params = Arguments.createMap().apply {
128
+ putString("contentId", contentId)
129
+ putInt("index", index)
130
+ }
131
+ sendEvent("onContentTapped", params)
132
+ }
133
+ }
118
134
  }
119
135
 
120
136
  @ReactMethod
@@ -211,6 +227,43 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
211
227
  shortKit?.player?.setMaxBitrate(bitrate.toInt())
212
228
  }
213
229
 
230
+ // -----------------------------------------------------------------------
231
+ // Custom feed
232
+ // -----------------------------------------------------------------------
233
+
234
+ @ReactMethod
235
+ override fun setFeedItems(items: String) {
236
+ val parsed = parseCustomFeedItems(items) ?: return
237
+ shortKit?.setFeedItems(parsed)
238
+ }
239
+
240
+ @ReactMethod
241
+ override fun appendFeedItems(items: String) {
242
+ val parsed = parseCustomFeedItems(items) ?: return
243
+ shortKit?.appendFeedItems(parsed)
244
+ }
245
+
246
+ @ReactMethod
247
+ override fun fetchContent(limit: Double, promise: com.facebook.react.bridge.Promise) {
248
+ val sdk = shortKit
249
+ if (sdk == null) {
250
+ promise.resolve("[]")
251
+ return
252
+ }
253
+ scope?.launch {
254
+ try {
255
+ val items = sdk.fetchContent(limit.toInt())
256
+ val arr = JSONArray()
257
+ for (item in items) {
258
+ arr.put(JSONObject(serializeContentItemToJSON(item)))
259
+ }
260
+ promise.resolve(arr.toString())
261
+ } catch (e: Exception) {
262
+ promise.resolve("[]")
263
+ }
264
+ }
265
+ }
266
+
214
267
  // -----------------------------------------------------------------------
215
268
  // Overlay lifecycle events (called by Fabric view)
216
269
  // -----------------------------------------------------------------------
@@ -385,6 +438,16 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
385
438
  sendEvent("onPrefetchedAheadCountChanged", params)
386
439
  }
387
440
  }
441
+
442
+ // Remaining content count
443
+ newScope.launch {
444
+ player.remainingContentCount.collect { count ->
445
+ val params = Arguments.createMap().apply {
446
+ putInt("count", count)
447
+ }
448
+ sendEvent("onRemainingContentCountChanged", params)
449
+ }
450
+ }
388
451
  }
389
452
 
390
453
  // -----------------------------------------------------------------------
@@ -425,6 +488,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
425
488
  private fun contentItemMap(item: ContentItem): WritableMap {
426
489
  return Arguments.createMap().apply {
427
490
  putString("id", item.id)
491
+ item.playbackId?.let { putString("playbackId", it) }
428
492
  putString("title", item.title)
429
493
  item.description?.let { putString("description", it) }
430
494
  putDouble("duration", item.duration)
@@ -575,13 +639,17 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
575
639
  val muteOnStart = obj.optBoolean("muteOnStart", true)
576
640
  val videoOverlay = parseVideoOverlay(obj.optString("overlay", null))
577
641
 
642
+ val feedSourceStr = obj.optString("feedSource", "algorithmic")
643
+ val feedSource = if (feedSourceStr == "custom") FeedSource.CUSTOM else FeedSource.ALGORITHMIC
644
+
578
645
  FeedConfig(
579
646
  feedHeight = feedHeight,
580
647
  videoOverlay = videoOverlay,
581
648
  carouselOverlay = CarouselOverlayMode.None,
582
649
  surveyOverlay = SurveyOverlayMode.None,
583
650
  adOverlay = AdOverlayMode.None,
584
- muteOnStart = muteOnStart
651
+ muteOnStart = muteOnStart,
652
+ feedSource = feedSource
585
653
  )
586
654
  } catch (_: Exception) {
587
655
  FeedConfig()
@@ -644,6 +712,54 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
644
712
  }
645
713
  }
646
714
 
715
+ private fun parseCustomFeedItems(json: String): List<CustomFeedItem>? {
716
+ return try {
717
+ val arr = JSONArray(json)
718
+ val result = mutableListOf<CustomFeedItem>()
719
+ for (i in 0 until arr.length()) {
720
+ val obj = arr.getJSONObject(i)
721
+ when (obj.optString("type")) {
722
+ "video" -> {
723
+ val playbackId = obj.optString("playbackId", null) ?: continue
724
+ result.add(CustomFeedItem.Video(playbackId))
725
+ }
726
+ "imageCarousel" -> {
727
+ val itemObj = obj.optJSONObject("item") ?: continue
728
+ val carouselItem = parseImageCarouselItem(itemObj) ?: continue
729
+ result.add(CustomFeedItem.ImageCarousel(carouselItem))
730
+ }
731
+ }
732
+ }
733
+ result.ifEmpty { null }
734
+ } catch (_: Exception) {
735
+ null
736
+ }
737
+ }
738
+
739
+ private fun parseImageCarouselItem(obj: JSONObject): ImageCarouselItem? {
740
+ val id = obj.optString("id", null) ?: return null
741
+ val imagesArr = obj.optJSONArray("images") ?: return null
742
+ val images = mutableListOf<CarouselImage>()
743
+ for (i in 0 until imagesArr.length()) {
744
+ val imgObj = imagesArr.getJSONObject(i)
745
+ images.add(CarouselImage(
746
+ url = imgObj.getString("url"),
747
+ alt = imgObj.optString("alt", null)
748
+ ))
749
+ }
750
+ return ImageCarouselItem(
751
+ id = id,
752
+ images = images,
753
+ autoScrollInterval = if (obj.has("autoScrollInterval")) obj.getDouble("autoScrollInterval") else null,
754
+ caption = obj.optString("caption", null),
755
+ title = obj.optString("title", null),
756
+ description = obj.optString("description", null),
757
+ author = obj.optString("author", null),
758
+ section = obj.optString("section", null),
759
+ articleUrl = obj.optString("articleUrl", null)
760
+ )
761
+ }
762
+
647
763
  /**
648
764
  * Parse optional custom dimensions JSON string into map.
649
765
  */
@@ -35,6 +35,10 @@ class ShortKitPackage : TurboReactPackage() {
35
35
  override fun createViewManagers(
36
36
  reactContext: ReactApplicationContext
37
37
  ): List<ViewManager<*, *>> {
38
- return listOf(ShortKitFeedViewManager())
38
+ return listOf(
39
+ ShortKitFeedViewManager(),
40
+ ShortKitPlayerViewManager(),
41
+ ShortKitWidgetViewManager(),
42
+ )
39
43
  }
40
44
  }
@@ -0,0 +1,136 @@
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.PlayerConfig
7
+ import com.shortkit.sdk.config.VideoOverlayMode
8
+ import com.shortkit.sdk.model.ContentItem
9
+ import com.shortkit.sdk.player.ShortKitPlayerView
10
+ import org.json.JSONObject
11
+
12
+ /**
13
+ * Fabric native view wrapping [ShortKitPlayerView] for use as a
14
+ * single-video player in React Native.
15
+ */
16
+ class ShortKitPlayerNativeView(context: Context) : FrameLayout(context) {
17
+
18
+ private var playerView: ShortKitPlayerView? = null
19
+ private var configJson: String? = null
20
+ private var contentItemJson: String? = null
21
+
22
+ var config: String?
23
+ get() = configJson
24
+ set(value) {
25
+ if (value == configJson) return
26
+ configJson = value
27
+ rebuildIfNeeded()
28
+ }
29
+
30
+ var contentItem: String?
31
+ get() = contentItemJson
32
+ set(value) {
33
+ if (value == contentItemJson) return
34
+ contentItemJson = value
35
+ applyContentItem()
36
+ }
37
+
38
+ var active: Boolean = true
39
+ set(value) {
40
+ if (field == value) return
41
+ field = value
42
+ applyActive()
43
+ }
44
+
45
+ override fun onAttachedToWindow() {
46
+ super.onAttachedToWindow()
47
+ rebuildIfNeeded()
48
+ }
49
+
50
+ override fun onDetachedFromWindow() {
51
+ playerView?.deactivate()
52
+ super.onDetachedFromWindow()
53
+ }
54
+
55
+ private fun rebuildIfNeeded() {
56
+ if (playerView != null) return
57
+
58
+ val sdk = ShortKitModule.shared?.sdk ?: return
59
+ val playerConfig = parsePlayerConfig(configJson)
60
+
61
+ val view = ShortKitPlayerView(context).apply {
62
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
63
+ }
64
+ view.initialize(sdk, playerConfig)
65
+ addView(view)
66
+ playerView = view
67
+
68
+ applyContentItem()
69
+ if (active) view.activate()
70
+ }
71
+
72
+ private fun applyContentItem() {
73
+ val json = contentItemJson ?: return
74
+ val view = playerView ?: return
75
+ val item = parseContentItem(json) ?: return
76
+ view.configure(item)
77
+ }
78
+
79
+ private fun applyActive() {
80
+ val view = playerView ?: return
81
+ if (active) view.activate() else view.deactivate()
82
+ }
83
+
84
+ private fun parsePlayerConfig(json: String?): PlayerConfig {
85
+ if (json.isNullOrEmpty()) return PlayerConfig()
86
+ return try {
87
+ val obj = JSONObject(json)
88
+ PlayerConfig(
89
+ cornerRadius = obj.optDouble("cornerRadius", 12.0).toFloat(),
90
+ clickAction = when (obj.optString("clickAction", "feed")) {
91
+ "feed" -> PlayerClickAction.FEED
92
+ "mute" -> PlayerClickAction.MUTE
93
+ "none" -> PlayerClickAction.NONE
94
+ else -> PlayerClickAction.FEED
95
+ },
96
+ autoplay = obj.optBoolean("autoplay", true),
97
+ loop = obj.optBoolean("loop", true),
98
+ muteOnStart = obj.optBoolean("muteOnStart", true),
99
+ videoOverlay = parseOverlay(obj),
100
+ )
101
+ } catch (_: Exception) {
102
+ PlayerConfig()
103
+ }
104
+ }
105
+
106
+ private fun parseOverlay(obj: JSONObject): VideoOverlayMode {
107
+ val overlay = obj.opt("overlay") ?: return VideoOverlayMode.None
108
+ if (overlay is JSONObject && overlay.optString("type") == "custom") {
109
+ return VideoOverlayMode.Custom {
110
+ ShortKitOverlayBridge(context)
111
+ }
112
+ }
113
+ return VideoOverlayMode.None
114
+ }
115
+
116
+ private fun parseContentItem(json: String): ContentItem? {
117
+ return try {
118
+ val obj = JSONObject(json)
119
+ ContentItem(
120
+ id = obj.getString("id"),
121
+ title = obj.getString("title"),
122
+ description = obj.optString("description", null),
123
+ duration = obj.getDouble("duration"),
124
+ streamingUrl = obj.getString("streamingUrl"),
125
+ thumbnailUrl = obj.getString("thumbnailUrl"),
126
+ captionTracks = emptyList(),
127
+ customMetadata = null,
128
+ author = obj.optString("author", null),
129
+ articleUrl = obj.optString("articleUrl", null),
130
+ commentCount = if (obj.has("commentCount")) obj.getInt("commentCount") else null,
131
+ )
132
+ } catch (_: Exception) {
133
+ null
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,35 @@
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 = ShortKitPlayerViewManager.REACT_CLASS)
9
+ class ShortKitPlayerViewManager : SimpleViewManager<ShortKitPlayerNativeView>() {
10
+
11
+ override fun getName(): String = REACT_CLASS
12
+
13
+ override fun createViewInstance(context: ThemedReactContext): ShortKitPlayerNativeView {
14
+ return ShortKitPlayerNativeView(context)
15
+ }
16
+
17
+ @ReactProp(name = "config")
18
+ fun setConfig(view: ShortKitPlayerNativeView, config: String?) {
19
+ view.config = config
20
+ }
21
+
22
+ @ReactProp(name = "contentItem")
23
+ fun setContentItem(view: ShortKitPlayerNativeView, contentItem: String?) {
24
+ view.contentItem = contentItem
25
+ }
26
+
27
+ @ReactProp(name = "active", defaultBoolean = true)
28
+ fun setActive(view: ShortKitPlayerNativeView, active: Boolean) {
29
+ view.active = active
30
+ }
31
+
32
+ companion object {
33
+ const val REACT_CLASS = "ShortKitPlayerView"
34
+ }
35
+ }
@@ -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
@@ -329,6 +369,10 @@ import ShortKit
329
369
  "thumbnailUrl": item.thumbnailUrl,
330
370
  ]
331
371
 
372
+ if let playbackId = item.playbackId {
373
+ dict["playbackId"] = playbackId
374
+ }
375
+
332
376
  if let description = item.description {
333
377
  dict["description"] = description
334
378
  }
@@ -410,13 +454,17 @@ import ShortKit
410
454
  let muteOnStart = obj["muteOnStart"] as? Bool ?? true
411
455
  let videoOverlay = parseVideoOverlay(obj["overlay"] as? String)
412
456
 
457
+ let feedSourceStr = obj["feedSource"] as? String ?? "algorithmic"
458
+ let feedSource: FeedSource = feedSourceStr == "custom" ? .custom : .algorithmic
459
+
413
460
  return FeedConfig(
414
461
  feedHeight: feedHeight,
415
462
  videoOverlay: videoOverlay,
416
463
  carouselOverlay: .none,
417
464
  surveyOverlay: .none,
418
465
  adOverlay: .none,
419
- muteOnStart: muteOnStart
466
+ muteOnStart: muteOnStart,
467
+ feedSource: feedSource
420
468
  )
421
469
  }
422
470
 
@@ -472,6 +520,34 @@ import ShortKit
472
520
  }
473
521
  }
474
522
 
523
+ /// Parse a JSON string of CustomFeedItem[] from the JS bridge.
524
+ private static func parseCustomFeedItems(_ json: String) -> [CustomFeedItem]? {
525
+ guard let data = json.data(using: .utf8),
526
+ let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
527
+ return nil
528
+ }
529
+
530
+ var result: [CustomFeedItem] = []
531
+ for obj in arr {
532
+ guard let type = obj["type"] as? String else { continue }
533
+ switch type {
534
+ case "video":
535
+ guard let playbackId = obj["playbackId"] as? String else { continue }
536
+ result.append(.video(playbackId: playbackId))
537
+ case "imageCarousel":
538
+ guard let itemData = obj["item"],
539
+ let itemJSON = try? JSONSerialization.data(withJSONObject: itemData),
540
+ let carouselItem = try? JSONDecoder().decode(ImageCarouselItem.self, from: itemJSON) else {
541
+ continue
542
+ }
543
+ result.append(.imageCarousel(carouselItem))
544
+ default:
545
+ continue
546
+ }
547
+ }
548
+ return result.isEmpty ? nil : result
549
+ }
550
+
475
551
  /// Parse optional custom dimensions JSON string into dictionary.
476
552
  private static func parseCustomDimensions(_ json: String?) -> [String: String]? {
477
553
  guard let json,
@@ -482,3 +558,14 @@ import ShortKit
482
558
  return dict
483
559
  }
484
560
  }
561
+
562
+ // MARK: - ShortKitDelegate
563
+
564
+ extension ShortKitBridge: ShortKitDelegate {
565
+ public func shortKit(_ shortKit: ShortKit, didTapContent contentId: String, at index: Int) {
566
+ emitOnMain("onContentTapped", body: [
567
+ "contentId": contentId,
568
+ "index": index
569
+ ])
570
+ }
571
+ }