@shortkitsdk/react-native 0.1.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.
@@ -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
+ }
@@ -6,7 +6,7 @@ import ShortKit
6
6
  ///
7
7
  /// Holds the `ShortKit` instance, subscribes to all Combine publishers on
8
8
  /// `ShortKitPlayer`, and forwards events to JS via the delegate protocol.
9
- @objc public class ShortKitBridge: NSObject, ShortKitDelegate {
9
+ @objc public class ShortKitBridge: NSObject {
10
10
 
11
11
  // MARK: - Shared instance (accessed by Fabric view manager in Task 13)
12
12
 
@@ -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,16 +38,17 @@ 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
43
45
  )
44
- sdk.delegate = self
45
46
  self.shortKit = sdk
46
47
 
47
48
  ShortKitBridge.shared = self
48
49
 
49
50
  subscribeToPublishers(sdk.player)
51
+ sdk.delegate = self
50
52
  }
51
53
 
52
54
  // MARK: - Teardown
@@ -141,31 +143,33 @@ import ShortKit
141
143
  shortKit?.player.setMaxBitrate(Int(bitrate))
142
144
  }
143
145
 
144
- // MARK: - ShortKitDelegate
146
+ // MARK: - Custom Feed
145
147
 
146
- public func shortKit(_ shortKit: ShortKit, didEncounterError error: ShortKitError) {
147
- let body: [String: Any]
148
- switch error {
149
- case .networkError(let underlying):
150
- body = ["code": "network_error", "message": underlying.localizedDescription]
151
- case .playbackError(let code, let message):
152
- body = ["code": code, "message": message]
153
- case .authError:
154
- body = ["code": "auth_error", "message": "Invalid API key"]
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)
155
152
  }
156
- emitOnMain("onError", body: body)
157
153
  }
158
154
 
159
- public func shortKit(_ shortKit: ShortKit, didTapShareFor item: ContentItem) {
160
- emitOnMain("onShareTapped", body: ["item": serializeContentItemToJSON(item)])
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
+ }
161
160
  }
162
161
 
163
- public func shortKit(_ shortKit: ShortKit, didRespondToSurvey surveyId: String, with option: SurveyOption) {
164
- emitOnMain("onSurveyResponse", body: [
165
- "surveyId": surveyId,
166
- "optionId": option.id,
167
- "optionText": option.text
168
- ])
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
+ }
169
173
  }
170
174
 
171
175
  // MARK: - Combine Subscriptions
@@ -305,6 +309,14 @@ import ShortKit
305
309
  self?.emit("onPrefetchedAheadCountChanged", body: ["count": count])
306
310
  }
307
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)
308
320
  }
309
321
 
310
322
  // MARK: - Event Emission Helpers
@@ -335,31 +347,6 @@ import ShortKit
335
347
  emitOnMain(name, body: body)
336
348
  }
337
349
 
338
- /// Emit an article tap event.
339
- public func emitArticleTapped(_ item: ContentItem) {
340
- emitOnMain("onArticleTapped", body: ["item": serializeContentItemToJSON(item)])
341
- }
342
-
343
- /// Emit a comment tap event.
344
- public func emitCommentTapped(_ item: ContentItem) {
345
- emitOnMain("onCommentTapped", body: ["item": serializeContentItemToJSON(item)])
346
- }
347
-
348
- /// Emit an overlay share tap event.
349
- public func emitOverlayShareTapped(_ item: ContentItem) {
350
- emitOnMain("onOverlayShareTapped", body: ["item": serializeContentItemToJSON(item)])
351
- }
352
-
353
- /// Emit a save tap event.
354
- public func emitSaveTapped(_ item: ContentItem) {
355
- emitOnMain("onSaveTapped", body: ["item": serializeContentItemToJSON(item)])
356
- }
357
-
358
- /// Emit a like tap event.
359
- public func emitLikeTapped(_ item: ContentItem) {
360
- emitOnMain("onLikeTapped", body: ["item": serializeContentItemToJSON(item)])
361
- }
362
-
363
350
  // MARK: - Content Item Serialization
364
351
 
365
352
  /// Serialize a ContentItem to a JSON string for bridge transport.
@@ -382,6 +369,10 @@ import ShortKit
382
369
  "thumbnailUrl": item.thumbnailUrl,
383
370
  ]
384
371
 
372
+ if let playbackId = item.playbackId {
373
+ dict["playbackId"] = playbackId
374
+ }
375
+
385
376
  if let description = item.description {
386
377
  dict["description"] = description
387
378
  }
@@ -463,13 +454,17 @@ import ShortKit
463
454
  let muteOnStart = obj["muteOnStart"] as? Bool ?? true
464
455
  let videoOverlay = parseVideoOverlay(obj["overlay"] as? String)
465
456
 
457
+ let feedSourceStr = obj["feedSource"] as? String ?? "algorithmic"
458
+ let feedSource: FeedSource = feedSourceStr == "custom" ? .custom : .algorithmic
459
+
466
460
  return FeedConfig(
467
461
  feedHeight: feedHeight,
468
462
  videoOverlay: videoOverlay,
469
463
  carouselOverlay: .none,
470
464
  surveyOverlay: .none,
471
465
  adOverlay: .none,
472
- muteOnStart: muteOnStart
466
+ muteOnStart: muteOnStart,
467
+ feedSource: feedSource
473
468
  )
474
469
  }
475
470
 
@@ -525,6 +520,34 @@ import ShortKit
525
520
  }
526
521
  }
527
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
+
528
551
  /// Parse optional custom dimensions JSON string into dictionary.
529
552
  private static func parseCustomDimensions(_ json: String?) -> [String: String]? {
530
553
  guard let json,
@@ -535,3 +558,14 @@ import ShortKit
535
558
  return dict
536
559
  }
537
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
+ }
@@ -31,8 +31,10 @@ import ShortKit
31
31
  private weak var currentOverlayView: UIView?
32
32
  /// Overlay for the upcoming cell (nativeID="overlay-next").
33
33
  private weak var nextOverlayView: UIView?
34
- /// The page index from which the current scroll gesture started.
34
+ /// The page index used for overlay transform calculations.
35
35
  private var currentPage: Int = 0
36
+ /// Deferred page update to avoid flashing stale metadata.
37
+ private var pageUpdateWorkItem: DispatchWorkItem?
36
38
 
37
39
  // MARK: - Lifecycle
38
40
 
@@ -86,6 +88,8 @@ import ShortKit
86
88
  }
87
89
 
88
90
  private func removeFeedViewController() {
91
+ pageUpdateWorkItem?.cancel()
92
+ pageUpdateWorkItem = nil
89
93
  scrollObservation?.invalidate()
90
94
  scrollObservation = nil
91
95
  currentOverlayView?.transform = .identity
@@ -106,7 +110,6 @@ import ShortKit
106
110
 
107
111
  private func setupScrollTracking(_ feedVC: ShortKitFeedViewController) {
108
112
  guard let scrollView = findScrollView(in: feedVC.view) else {
109
- NSLog("[ShortKitFeedView] Could not find scroll view in feed VC hierarchy.")
110
113
  return
111
114
  }
112
115
 
@@ -120,14 +123,47 @@ import ShortKit
120
123
  guard cellHeight > 0 else { return }
121
124
 
122
125
  let offset = scrollView.contentOffset.y
123
- let delta = offset - CGFloat(currentPage) * cellHeight
124
126
 
125
- // Update currentPage when the scroll settles near a page boundary
127
+ // Detect page change, but DEFER updating currentPage.
128
+ //
129
+ // Why: when the scroll settles on a new page, overlay-current still
130
+ // shows the OLD page's metadata (React hasn't processed OVERLAY_ACTIVATE
131
+ // yet). If we update currentPage immediately, delta snaps to 0 and
132
+ // overlay-current becomes visible with stale data.
133
+ //
134
+ // By deferring ~80ms, overlay-next (which already shows the correct
135
+ // data via NextOverlayProvider) stays visible at y=0 while React
136
+ // processes the state update. After the delay, overlay-current has
137
+ // been re-rendered with correct data and takes over seamlessly.
126
138
  let nearestPage = Int(round(offset / cellHeight))
127
139
  if abs(offset - CGFloat(nearestPage) * cellHeight) < 1.0 {
140
+ if nearestPage != currentPage && pageUpdateWorkItem == nil {
141
+ let targetPage = nearestPage
142
+ let workItem = DispatchWorkItem { [weak self] in
143
+ guard let self else { return }
144
+ self.currentPage = targetPage
145
+ self.pageUpdateWorkItem = nil
146
+ // Reapply overlay transforms now that currentPage is updated.
147
+ // Without this, overlay-next (static NextOverlayProvider state)
148
+ // stays visible at y=0 while overlay-current (live state) stays
149
+ // hidden — no scroll event fires to trigger handleScrollOffset.
150
+ let h = self.bounds.height
151
+ self.currentOverlayView?.transform = .identity
152
+ self.nextOverlayView?.transform = CGAffineTransform(translationX: 0, y: h)
153
+ }
154
+ pageUpdateWorkItem = workItem
155
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.08, execute: workItem)
156
+ }
157
+ } else if let workItem = pageUpdateWorkItem {
158
+ // User is scrolling again — apply pending update immediately
159
+ // so transforms stay aligned for the new gesture.
160
+ workItem.cancel()
161
+ pageUpdateWorkItem = nil
128
162
  currentPage = nearestPage
129
163
  }
130
164
 
165
+ let delta = offset - CGFloat(currentPage) * cellHeight
166
+
131
167
  // Find the overlay views if not cached
132
168
  if currentOverlayView == nil || nextOverlayView == nil {
133
169
  findOverlayViews()
@@ -146,28 +182,39 @@ import ShortKit
146
182
  }
147
183
  }
148
184
 
149
- /// Find the sibling RN overlay views by nativeID (mapped to
150
- /// accessibilityIdentifier on iOS). In the Fabric hierarchy, the
151
- /// ShortKitFeedView and the overlay containers are children of the
152
- /// same parent (wrapped by ShortKitFeed.tsx with overflow: hidden).
185
+ /// Find the sibling RN overlay views by nativeID.
186
+ ///
187
+ /// In Fabric interop, `ShortKitFeedView` (a Paper-style `RCTViewManager`
188
+ /// view) is wrapped in an intermediate `RCTViewComponentView`. So
189
+ /// `self.superview` is that wrapper — not the React `<View>` container
190
+ /// rendered by ShortKitFeed.tsx. We walk up the ancestor chain until we
191
+ /// find the overlays.
153
192
  private func findOverlayViews() {
154
- guard let parent = superview else { return }
155
-
156
- // The nativeID views may be nested inside intermediate Fabric
157
- // wrapper views, so we search recursively within siblings.
158
- for sibling in parent.subviews where sibling !== self {
159
- if let found = findView(withNativeID: "overlay-current", in: sibling) {
160
- currentOverlayView = found
161
- }
162
- if let found = findView(withNativeID: "overlay-next", in: sibling) {
163
- nextOverlayView = found
193
+ var ancestor: UIView? = superview
194
+ while let container = ancestor {
195
+ for child in container.subviews {
196
+ // Skip subtrees that contain ourselves
197
+ guard !self.isDescendant(of: child) else { continue }
198
+
199
+ if currentOverlayView == nil {
200
+ currentOverlayView = findView(withNativeID: "overlay-current", in: child)
201
+ }
202
+ if nextOverlayView == nil {
203
+ nextOverlayView = findView(withNativeID: "overlay-next", in: child)
204
+ }
164
205
  }
206
+ if currentOverlayView != nil && nextOverlayView != nil { return }
207
+ ancestor = container.superview
165
208
  }
166
209
  }
167
210
 
168
- /// Recursively find a view by its accessibilityIdentifier (nativeID).
211
+ /// Recursively find a view by its React Native `nativeID` prop.
212
+ /// In Fabric, this is stored on `RCTViewComponentView.nativeId`,
213
+ /// not `accessibilityIdentifier`.
169
214
  private func findView(withNativeID nativeID: String, in view: UIView) -> UIView? {
170
- if view.accessibilityIdentifier == nativeID {
215
+ if view.responds(to: Selector(("nativeId"))),
216
+ let rnNativeId = view.value(forKey: "nativeId") as? String,
217
+ rnNativeId == nativeID {
171
218
  return view
172
219
  }
173
220
  for subview in view.subviews {
@@ -54,14 +54,10 @@ RCT_EXPORT_MODULE(ShortKitModule)
54
54
  @"onFeedTransition",
55
55
  @"onFormatChange",
56
56
  @"onPrefetchedAheadCountChanged",
57
+ @"onRemainingContentCountChanged",
57
58
  @"onError",
58
59
  @"onShareTapped",
59
60
  @"onSurveyResponse",
60
- @"onArticleTapped",
61
- @"onCommentTapped",
62
- @"onOverlayShareTapped",
63
- @"onSaveTapped",
64
- @"onLikeTapped",
65
61
  @"onOverlayConfigure",
66
62
  @"onOverlayActivate",
67
63
  @"onOverlayReset",
@@ -69,6 +65,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
69
65
  @"onOverlayRestore",
70
66
  @"onOverlayTap",
71
67
  @"onOverlayDoubleTap",
68
+ @"onContentTapped",
72
69
  ];
73
70
  }
74
71
 
@@ -107,6 +104,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
107
104
 
108
105
  RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
109
106
  config:(NSString *)config
107
+ embedId:(NSString *)embedId
110
108
  clientAppName:(NSString *)clientAppName
111
109
  clientAppVersion:(NSString *)clientAppVersion
112
110
  customDimensions:(NSString *)customDimensions) {
@@ -115,6 +113,7 @@ RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
115
113
 
116
114
  _shortKitBridge = [[ShortKitBridge alloc] initWithApiKey:apiKey
117
115
  config:config
116
+ embedId:embedId
118
117
  clientAppName:clientAppName
119
118
  clientAppVersion:clientAppVersion
120
119
  customDimensions:customDimensions
@@ -192,6 +191,24 @@ RCT_EXPORT_METHOD(setMaxBitrate:(double)bitrate) {
192
191
  [_shortKitBridge setMaxBitrate:bitrate];
193
192
  }
194
193
 
194
+ // MARK: - Custom Feed
195
+
196
+ RCT_EXPORT_METHOD(setFeedItems:(NSString *)items) {
197
+ [_shortKitBridge setFeedItems:items];
198
+ }
199
+
200
+ RCT_EXPORT_METHOD(appendFeedItems:(NSString *)items) {
201
+ [_shortKitBridge appendFeedItems:items];
202
+ }
203
+
204
+ RCT_EXPORT_METHOD(fetchContent:(NSInteger)limit
205
+ resolve:(RCTPromiseResolveBlock)resolve
206
+ reject:(RCTPromiseRejectBlock)reject) {
207
+ [_shortKitBridge fetchContent:limit completion:^(NSString *json) {
208
+ resolve(json);
209
+ }];
210
+ }
211
+
195
212
  // MARK: - New Architecture (TurboModule)
196
213
 
197
214
  #ifdef RCT_NEW_ARCH_ENABLED