@shortkitsdk/react-native 0.2.28 → 0.2.30

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 (33) hide show
  1. package/android/libs/shortkit-release.aar +0 -0
  2. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +8 -1
  3. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +6 -0
  4. package/ios/ReactVideoCarouselOverlayHost.swift +6 -0
  5. package/ios/ShortKitBridge.swift +139 -36
  6. package/ios/ShortKitFeedView.swift +51 -45
  7. package/ios/ShortKitFeedViewManager.mm +1 -0
  8. package/ios/ShortKitModule.mm +5 -1
  9. package/ios/ShortKitSDK.xcframework/Info.plist +5 -5
  10. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +2 -2
  11. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +912 -103
  12. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +21 -2
  13. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  14. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +21 -2
  15. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  16. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +9 -9
  17. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Info.plist +2 -2
  18. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +912 -103
  19. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +21 -2
  20. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  21. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +21 -2
  22. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +912 -103
  23. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +21 -2
  24. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  25. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +21 -2
  26. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  27. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +17 -17
  28. package/package.json +1 -1
  29. package/src/ShortKitFeed.tsx +33 -0
  30. package/src/ShortKitProvider.tsx +4 -0
  31. package/src/specs/NativeShortKitModule.ts +8 -0
  32. package/src/specs/ShortKitFeedViewNativeComponent.ts +5 -0
  33. package/src/types.ts +10 -0
Binary file
@@ -30,6 +30,12 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
30
30
  var feedId: String? = null
31
31
  var startAtItemId: String? = null
32
32
  var preloadId: String? = null
33
+ // Fabric's generated delegate maps an absent boolean prop to the Java
34
+ // primitive false, so Boolean? is always non-null from the bridge. Track
35
+ // whether the prop was explicitly set so we can fall back to the provider
36
+ // flag when it wasn't.
37
+ var debugPanel: Boolean = false
38
+ var debugPanelPropSet: Boolean = false
33
39
 
34
40
  // -----------------------------------------------------------------------
35
41
  // Fragment management
@@ -191,7 +197,8 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
191
197
 
192
198
  val fragment = ShortKitFeedFragment.newInstance(sdk, feedConfig, startAtItemId)
193
199
 
194
- if (sdk.debugPanelEnabled) {
200
+ val debugPanelEnabled = if (debugPanelPropSet) debugPanel else sdk.debugPanelEnabled
201
+ if (debugPanelEnabled) {
195
202
  fragment.debugPanelFactory = { active, prev, next, lifecycleOwner ->
196
203
  com.shortkit.sdk.debug.DebugPanelView(context).also { panel ->
197
204
  panel.subscribe(active, prev, next, lifecycleOwner)
@@ -35,6 +35,12 @@ class ShortKitFeedViewManager : SimpleViewManager<ShortKitFeedView>() {
35
35
  view.preloadId = preloadId
36
36
  }
37
37
 
38
+ @ReactProp(name = "debugPanel")
39
+ fun setDebugPanel(view: ShortKitFeedView, debugPanel: Boolean) {
40
+ view.debugPanel = debugPanel
41
+ view.debugPanelPropSet = true
42
+ }
43
+
38
44
  override fun onDropViewInstance(view: ShortKitFeedView) {
39
45
  view.destroy()
40
46
  super.onDropViewInstance(view)
@@ -324,7 +324,13 @@ import ShortKitSDK
324
324
  } else {
325
325
  carouselItemJSON = "{}"
326
326
  }
327
+ // Tag with active surface's feedId so per-<ShortKitFeed>
328
+ // subscribers can filter to their own feed instance — same
329
+ // pattern as the other per-feed events. surfaceId continues
330
+ // to identify the React overlay host (a separate concern).
331
+ let feedId = self.bridge?.activeSurfaceFeedIdPublic() ?? ""
327
332
  self.bridge?.emit("onCarouselActiveVideoCompleted", body: [
333
+ "feedId": feedId,
328
334
  "surfaceId": self.surfaceId,
329
335
  "contentItem": contentItemJSON,
330
336
  "indexInCarousel": event.indexInCarousel,
@@ -72,7 +72,6 @@ import ShortKitSDK
72
72
 
73
73
  // Wire per-feed refresh state callback (scoped by feedId)
74
74
  vc.onRefreshStateChanged = { [weak self] state in
75
- NSLog("[ShortKit Bridge] onRefreshStateChangedPerFeed feedId=%@ status=%@", id, "\(state)")
76
75
  var body: [String: Any] = ["feedId": id]
77
76
  switch state {
78
77
  case .idle:
@@ -101,6 +100,34 @@ import ShortKitSDK
101
100
  ])
102
101
  }
103
102
 
103
+ // Wire per-feed transition event. The FVC fires this closure from
104
+ // handleSwipe(to:) — one per transition, bound to this feed. This
105
+ // replaces the global `player.feedTransition` subscription pattern
106
+ // (which fanned every feed's transitions out to every mounted
107
+ // <ShortKitFeed>, causing cross-feed state pollution). Structural
108
+ // fix: callback literally cannot fire on the wrong feed.
109
+ vc.onFeedTransition = { [weak self] event in
110
+ guard let self else { return }
111
+ var body: [String: Any] = [
112
+ "feedId": id,
113
+ "phase": Self.transitionPhaseString(event.phase),
114
+ "direction": Self.transitionDirectionString(event.direction)
115
+ ]
116
+ // Serialize from the underlying FeedItem (not event.from/to
117
+ // which are nil for non-content cells). See the docstring on
118
+ // `serializeFeedItemIdentityJSON` for the why — in short, this
119
+ // ensures carousels/ads/surveys come through with a populated
120
+ // `id`, so JS-side hosts can track feed position for any cell
121
+ // type.
122
+ if let fromFeedItem = event.fromFeedItem {
123
+ body["fromItem"] = self.serializeFeedItemIdentityJSON(fromFeedItem)
124
+ }
125
+ if let toFeedItem = event.toFeedItem {
126
+ body["toItem"] = self.serializeFeedItemIdentityJSON(toFeedItem)
127
+ }
128
+ self.emitOnMain("onFeedTransition", body: body)
129
+ }
130
+
104
131
  // Replay buffered operations on the next run-loop tick so the VC's
105
132
  // view hierarchy is fully set up after didMoveToWindow returns.
106
133
  if let ops = pendingOps.removeValue(forKey: id) {
@@ -122,6 +149,24 @@ import ShortKitSDK
122
149
  return feedRegistry[id]?.vc
123
150
  }
124
151
 
152
+ /// Look up the feedId for the currently-active surface. Used by events
153
+ /// that originate in shared singletons (player Combine publishers, the
154
+ /// `ShortKitDelegate`) and must be attributed to one feed for the JS
155
+ /// wrapper's feedId filter to work. Returns `""` if no active surface
156
+ /// or if the active surface isn't in the registry — consumers should
157
+ /// treat empty feedId as "unknown/global" (JS wrapper accepts it as a
158
+ /// fallback for forward compatibility with older native builds).
159
+ private func activeSurfaceFeedId() -> String {
160
+ return feedRegistry.first(where: { $0.value.vc?.isActiveSurface == true })?.key ?? ""
161
+ }
162
+
163
+ /// Internal accessor for `activeSurfaceFeedId()` so cross-file emit
164
+ /// sites (e.g. `ReactVideoCarouselOverlayHost`) can tag emits with
165
+ /// the active feed's feedId without duplicating the lookup logic.
166
+ internal func activeSurfaceFeedIdPublic() -> String {
167
+ return activeSurfaceFeedId()
168
+ }
169
+
125
170
  // MARK: - Init
126
171
 
127
172
  @objc public init(
@@ -131,6 +176,8 @@ import ShortKitSDK
131
176
  clientAppVersion: String?,
132
177
  customDimensions customDimensionsJSON: String?,
133
178
  debugPanel: Bool,
179
+ serverTracingEnabled: Bool,
180
+ consoleTracingEnabled: Bool,
134
181
  delegate: ShortKitBridgeDelegateProtocol,
135
182
  surfacePresenter: AnyObject?
136
183
  ) {
@@ -145,7 +192,9 @@ import ShortKitSDK
145
192
  clientAppName: clientAppName,
146
193
  clientAppVersion: clientAppVersion,
147
194
  customDimensions: dimensions,
148
- debugPanelEnabled: debugPanel
195
+ debugPanelEnabled: debugPanel,
196
+ serverTracingEnabled: serverTracingEnabled,
197
+ consoleTracingEnabled: consoleTracingEnabled
149
198
  )
150
199
  self.shortKit = sdk
151
200
 
@@ -430,29 +479,21 @@ import ShortKitSDK
430
479
  let config = Self.parseFeedConfig(configJSON)
431
480
  let preload: FeedPreload?
432
481
 
433
- NSLog("[ShortKit Bridge] preloadFeed called — feedSource: %@, hasItemsJSON: %@",
434
- config.feedSource == .custom ? "custom" : "algorithmic",
435
- itemsJSON != nil ? "yes (\(itemsJSON!.prefix(100))...)" : "no")
436
-
437
482
  if config.feedSource == .custom {
438
483
  guard let json = itemsJSON, let items = Self.parseFeedInputs(json) else {
439
- NSLog("[ShortKit Bridge] ❌ preloadFeed: feedSource=custom but no valid items — returning empty")
440
484
  completion("")
441
485
  return
442
486
  }
443
- NSLog("[ShortKit Bridge] preloadFeed: creating custom preload with %d items", items.count)
444
487
  preload = shortKit?.preloadFeed(items: items)
445
488
  } else {
446
489
  preload = shortKit?.preloadFeed(filter: config.filter)
447
490
  }
448
491
 
449
492
  guard let preload else {
450
- NSLog("[ShortKit Bridge] ❌ preloadFeed: shortKit?.preloadFeed returned nil")
451
493
  completion("")
452
494
  return
453
495
  }
454
496
  let uuid = UUID().uuidString
455
- NSLog("[ShortKit Bridge] ✅ preloadFeed: created handle %@", uuid)
456
497
  preloadHandles[uuid] = preload
457
498
  activeFeedId = uuid
458
499
  completion(uuid)
@@ -553,38 +594,34 @@ import ShortKitSDK
553
594
  }
554
595
  .store(in: &cancellables)
555
596
 
556
- // Did loop
597
+ // Did loop — tagged with active surface's feedId. `player.didLoop`
598
+ // is a singleton Combine publisher on the shared player (only one
599
+ // item plays at a time). The event semantically belongs to
600
+ // whichever feed owns the active surface; tag with that feedId so
601
+ // JS consumers bound to specific feeds can filter.
557
602
  player.didLoop
558
603
  .receive(on: DispatchQueue.main)
559
604
  .sink { [weak self] event in
560
- self?.emit("onDidLoop", body: [
605
+ guard let self else { return }
606
+ let feedId = self.activeSurfaceFeedId()
607
+ self.emit("onDidLoop", body: [
608
+ "feedId": feedId,
561
609
  "contentId": event.contentId,
562
610
  "loopCount": event.loopCount
563
611
  ])
564
612
  }
565
613
  .store(in: &cancellables)
566
614
 
567
- // Feed transition deferred to next run-loop tick so emission doesn't
568
- // contend with Core Animation's commit phase during swipe settle.
569
- player.feedTransition
570
- .receive(on: DispatchQueue.main)
571
- .sink { [weak self] event in
572
- guard let self else { return }
573
- var body: [String: Any] = [
574
- "phase": Self.transitionPhaseString(event.phase),
575
- "direction": Self.transitionDirectionString(event.direction)
576
- ]
577
- if let from = event.from {
578
- body["fromItem"] = self.serializeContentItemToJSON(from)
579
- }
580
- if let to = event.to {
581
- body["toItem"] = self.serializeContentItemToJSON(to)
582
- }
583
- DispatchQueue.main.async { [weak self] in
584
- self?.emit("onFeedTransition", body: body)
585
- }
586
- }
587
- .store(in: &cancellables)
615
+ // NOTE: The global `player.feedTransition` subscription used to live
616
+ // here. It's been removed feed transitions are now emitted to RN
617
+ // via the per-FVC `vc.onFeedTransition` closure wired in
618
+ // `registerFeed(id:viewController:)` above. This eliminates the
619
+ // cross-feed routing bug where every mounted <ShortKitFeed>
620
+ // consumer received every feed's transitions.
621
+ //
622
+ // `player.sendFeedTransition` is still called by FVC.handleSwipe
623
+ // for backward compatibility with native iOS consumers that
624
+ // subscribe to `ShortKit.player.feedTransition` directly.
588
625
 
589
626
  // Feed scroll phase — coalesced: only emit .dragging on first touch
590
627
  // (transition from settled), drop intermediate .dragging events. Always
@@ -613,11 +650,16 @@ import ShortKitSDK
613
650
  }
614
651
  .store(in: &cancellables)
615
652
 
616
- // Format change
653
+ // Format change — tagged with active surface's feedId. Same
654
+ // reasoning as onDidLoop: fires from shared player singleton, but
655
+ // semantically belongs to whichever feed owns the active surface.
617
656
  player.formatChange
618
657
  .receive(on: DispatchQueue.main)
619
658
  .sink { [weak self] event in
620
- self?.emit("onFormatChange", body: [
659
+ guard let self else { return }
660
+ let feedId = self.activeSurfaceFeedId()
661
+ self.emit("onFormatChange", body: [
662
+ "feedId": feedId,
621
663
  "contentId": event.contentId,
622
664
  "fromBitrate": Double(event.fromBitrate),
623
665
  "toBitrate": Double(event.toBitrate),
@@ -683,6 +725,52 @@ import ShortKitSDK
683
725
  return json
684
726
  }
685
727
 
728
+ /// Serialize a `FeedItem` to a ContentItem-shaped JSON string for the
729
+ /// `onFeedTransition` event. For `.content` cells this is the real
730
+ /// `ContentItem`. For every other cell kind (adSlot, imageCarousel,
731
+ /// survey, videoCarousel) there is no playable `ContentItem`, so we
732
+ /// synthesize a minimal one whose ONLY meaningful field is `id`.
733
+ ///
734
+ /// Why: JS hosts use `event.to` as an identity cursor for resume-on-
735
+ /// tab-return (`setFeedItems(startAt:)`). Before this synthesis, non-
736
+ /// content cells came through as `to=null` and the host's stored
737
+ /// resume id stayed pinned to the last regular video — causing the
738
+ /// SDK to re-seed the feed at the wrong index on tab return, which
739
+ /// in turn caused black carousel cells + wrong audio (see repro
740
+ /// analysis in debug.log, apr 2026).
741
+ ///
742
+ /// Scope: this is a bridge-only synthesis. It never flows back into
743
+ /// the Swift SDK (pool, cache, FeedDataSource, etc. all still see the
744
+ /// real `FeedItem` / `ContentItem?` and treat non-content cells
745
+ /// honestly). JS-side consumers should ONLY read `.id` / `.playbackId`
746
+ /// from `onFeedTransition` items — deeper fields are placeholders for
747
+ /// non-content cells.
748
+ ///
749
+ /// TODO: when we want to expose richer per-cell metadata on the JS
750
+ /// side, migrate to a proper FeedPosition event payload rather than
751
+ /// extending this synthesis. See the long-term fix plan.
752
+ private func serializeFeedItemIdentityJSON(_ feedItem: FeedItem) -> String {
753
+ if let contentItem = feedItem.contentItem {
754
+ return serializeContentItemToJSON(contentItem)
755
+ }
756
+ let synthetic = ContentItem(
757
+ id: feedItem.id,
758
+ playbackId: nil,
759
+ title: "",
760
+ description: nil,
761
+ duration: 0,
762
+ streamingUrl: "",
763
+ thumbnailUrl: "",
764
+ captionTracks: [],
765
+ customMetadata: nil,
766
+ author: nil,
767
+ articleUrl: nil,
768
+ commentCount: nil,
769
+ fallbackUrl: nil
770
+ )
771
+ return serializeContentItemToJSON(synthetic)
772
+ }
773
+
686
774
  /// Build an NSDictionary from a ContentItem with fields matching the JS spec.
687
775
  /// `captionTracks` and `customMetadata` are JSON-serialized strings.
688
776
  private static func contentItemDict(_ item: ContentItem) -> [String: Any] {
@@ -1007,7 +1095,14 @@ import ShortKitSDK
1007
1095
 
1008
1096
  extension ShortKitBridge: ShortKitDelegate {
1009
1097
  public func shortKit(_ shortKit: ShortKit, didTapContent contentId: String, at index: Int) {
1098
+ // Tag with active surface's feedId. The delegate is a singleton
1099
+ // (the bridge), so we use the activeSurface lookup to attribute
1100
+ // this tap to the feed it originated from. Without the tag, every
1101
+ // mounted <ShortKitFeed> consumer would fire onContentTapped on
1102
+ // every tap — including taps in sibling feeds.
1103
+ let feedId = activeSurfaceFeedId()
1010
1104
  emitOnMain("onContentTapped", body: [
1105
+ "feedId": feedId,
1011
1106
  "contentId": contentId,
1012
1107
  "index": index
1013
1108
  ])
@@ -1038,10 +1133,18 @@ extension ShortKitBridge: ShortKitDelegate {
1038
1133
  self.itemCache[item.id] = item
1039
1134
  }
1040
1135
  }
1136
+ // Capture the feedId synchronously before the async Task — after
1137
+ // the await, activeSurface may have shifted (user swiped tabs).
1138
+ // The fetch semantically belongs to whichever feed initiated it,
1139
+ // which at this delegate-call moment is the active surface.
1140
+ let feedId = activeSurfaceFeedId()
1041
1141
  Task {
1042
1142
  let data = try? JSONEncoder().encode(items)
1043
1143
  let json = data.flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
1044
- self.emitOnMain("onDidFetchContentItems", body: ["items": json])
1144
+ self.emitOnMain("onDidFetchContentItems", body: [
1145
+ "feedId": feedId,
1146
+ "items": json
1147
+ ])
1045
1148
  }
1046
1149
  }
1047
1150
  }
@@ -38,14 +38,19 @@ import ShortKitSDK
38
38
  didSet { /* used at embed time only */ }
39
39
  }
40
40
 
41
- /// When the consumer explicitly manages this prop, it becomes the
42
- /// authoritative suspension signal overriding willMove(toWindow:).
43
- private var isActiveManagedByProp = false
44
-
41
+ /// The `active` prop is authoritative: whenever the view is deciding
42
+ /// whether to activate/deactivate/suspend its FeedViewController, it
43
+ /// consults this value directly. The `didSet` only exists to react to
44
+ /// transitions while the VC already exists. Transitions that arrive
45
+ /// before the VC is created (a common Fabric ordering: props set on the
46
+ /// view instance before `didMoveToWindow` runs) are stored in the Bool
47
+ /// property by Swift automatically — `embedFeedViewControllerIfNeeded`
48
+ /// reads the current value and acts on it when the VC is finally set up.
45
49
  @objc public var active: Bool = true {
46
50
  didSet {
47
- guard active != oldValue, let feedVC = feedViewController else { return }
48
- if !active { isActiveManagedByProp = true }
51
+ guard active != oldValue, let feedVC = feedViewController else {
52
+ return
53
+ }
49
54
  if active {
50
55
  feedVC.activate()
51
56
  } else {
@@ -54,6 +59,12 @@ import ShortKitSDK
54
59
  }
55
60
  }
56
61
 
62
+ /// Per-surface debug panel override. NSNumber so we can distinguish
63
+ /// unset (fall back to provider-global) from explicit true/false.
64
+ @objc public var debugPanel: NSNumber? {
65
+ didSet { /* read once at embed time */ }
66
+ }
67
+
57
68
  @objc var feedId: String?
58
69
 
59
70
  // MARK: - Child VC
@@ -62,9 +73,16 @@ import ShortKitSDK
62
73
 
63
74
  // MARK: - Lifecycle
64
75
 
76
+ public override init(frame: CGRect) {
77
+ super.init(frame: frame)
78
+ }
79
+
80
+ public required init?(coder: NSCoder) {
81
+ super.init(coder: coder)
82
+ }
83
+
65
84
  public override func didMoveToWindow() {
66
85
  super.didMoveToWindow()
67
-
68
86
  if window != nil {
69
87
  embedFeedViewControllerIfNeeded()
70
88
  }
@@ -72,8 +90,11 @@ import ShortKitSDK
72
90
 
73
91
  public override func willMove(toWindow newWindow: UIWindow?) {
74
92
  super.willMove(toWindow: newWindow)
75
-
76
- if newWindow == nil && !isActiveManagedByProp {
93
+ // Suspend only if we're leaving the window AND the prop says this
94
+ // surface should be active. When the prop is false, the consumer has
95
+ // already driven the VC into deactivate via active.didSet (or will,
96
+ // once the VC exists), and we mustn't double-suspend.
97
+ if newWindow == nil && active {
77
98
  suspendFeedViewController()
78
99
  }
79
100
  }
@@ -96,18 +117,21 @@ import ShortKitSDK
96
117
 
97
118
  // Re-attach an existing suspended VC (e.g. after native stack pop)
98
119
  if let feedVC = feedViewController {
99
- NSLog("[ShortKit FeedView] re-attaching suspended VC feedId=%@", feedId ?? "nil")
100
120
  parentVC.addChild(feedVC)
101
121
  feedVC.view.frame = bounds
102
122
  addSubview(feedVC.view)
103
123
  feedVC.didMove(toParent: parentVC)
104
- if !isActiveManagedByProp {
124
+ // Authoritative: read the current active prop and act on it.
125
+ // If prop is false, stay deactivated — do NOT claim the pool.
126
+ // This is the fix for the prop-arrived-before-VC race: a prop
127
+ // update dropped by the didSet guard (vcExists=false) lands here
128
+ // and is honored.
129
+ if active {
105
130
  feedVC.activate()
106
131
  }
107
132
  if let feedId = self.feedId {
108
133
  ShortKitBridge.shared?.registerFeed(id: feedId, viewController: feedVC)
109
134
  }
110
- NSLog("[ShortKit FeedView] re-attach complete — activate() called")
111
135
  return
112
136
  }
113
137
 
@@ -120,33 +144,22 @@ import ShortKitSDK
120
144
 
121
145
  // Consume preload handle if available
122
146
  if let preloadId = self.preloadId {
123
- NSLog("[ShortKit FeedView] preloadId prop: %@", preloadId)
124
147
  if let preload = ShortKitBridge.shared?.consumePreload(id: preloadId) {
125
148
  feedConfig.preload = preload
126
- NSLog("[ShortKit FeedView] ✅ Preload handle consumed for feedId: %@", preloadId)
127
- } else {
128
- NSLog("[ShortKit FeedView] ❌ No preload handle found for feedId: %@", preloadId)
129
- NSLog("[ShortKit FeedView] Available preload handles: %@", ShortKitBridge.shared?.preloadHandles.keys.joined(separator: ", ") ?? "none")
130
149
  }
131
150
  } else if let json = self.feedItemsJSON,
132
151
  let items = ShortKitBridge.parseFeedInputs(json) {
133
152
  // feedItems prop: wrap in an immediate preload (no async prefetch
134
153
  // work — items are available synchronously at viewDidLoad).
135
154
  feedConfig.preload = FeedPreload(immediateItems: items)
136
- NSLog("[ShortKit FeedView] feedItems prop: created immediate preload with %d items", items.count)
137
- } else {
138
- NSLog("[ShortKit FeedView] No preloadId or feedItems prop set")
139
155
  }
140
156
 
141
- NSLog("[ShortKit FeedView] feedConfig.preload is %@", feedConfig.preload != nil ? "SET" : "NIL")
142
- NSLog("[ShortKit FeedView] feedConfig.feedSource: %@", feedConfig.feedSource == .custom ? "custom" : "algorithmic")
143
-
144
- NSLog("[ShortKit FeedView] Creating ShortKitFeedViewController with config.preload=%@, config.feedSource=%@",
145
- feedConfig.preload != nil ? "SET" : "NIL",
146
- feedConfig.feedSource == .custom ? "custom" : "algorithmic")
147
-
148
- let feedVC = ShortKitFeedViewController(shortKit: sdk, config: feedConfig, startAtItemId: startAtItemId)
149
- feedVC.setBridgeManaged()
157
+ let feedVC = ShortKitFeedViewController(
158
+ shortKit: sdk,
159
+ config: feedConfig,
160
+ startAtItemId: startAtItemId,
161
+ lifecycle: .manual
162
+ )
150
163
 
151
164
  // Seed a thumbnail from the host app's image cache so the first cell
152
165
  // renders with a visible thumbnail from frame zero. Synchronous
@@ -159,7 +172,8 @@ import ShortKitSDK
159
172
  feedVC.seedThumbnail = image
160
173
  }
161
174
 
162
- if sdk.debugPanelEnabled {
175
+ let debugPanelEnabled = self.debugPanel?.boolValue ?? sdk.debugPanelEnabled
176
+ if debugPanelEnabled {
163
177
  feedVC.debugPanelFactory = { active, prev, next in
164
178
  let panel = DebugPanelView(frame: CGRect(
165
179
  x: 0, y: 0,
@@ -174,8 +188,6 @@ import ShortKitSDK
174
188
  }
175
189
  }
176
190
 
177
- NSLog("[ShortKit FeedView] VC created successfully (bridge-managed)")
178
-
179
191
  feedVC.onDismiss = {
180
192
  ShortKitBridge.shared?.emitDismiss()
181
193
  }
@@ -192,13 +204,12 @@ import ShortKitSDK
192
204
  feedVC.didMove(toParent: parentVC)
193
205
 
194
206
  // With FVC.viewDidAppear no longer self-claiming for bridge-managed
195
- // surfaces, the bridge is the sole authority on claim timing. Mirror
196
- // the reattach path: explicitly activate iff the RN `active` prop
197
- // is true. If the prop is false (e.g. user is on a different tab
198
- // while this FVC was created to handle a delayed API response), the
199
- // FVC stays idle — no claim, no pool mutation, no hijack. A later
200
- // prop change to active=true triggers `feedVC.activate()` via the
201
- // prop's didSet.
207
+ // surfaces, the bridge is the sole authority on claim timing. Read
208
+ // the authoritative `active` prop and act on it. If the prop is
209
+ // false (including the case where it was set to false before the VC
210
+ // existed and the didSet guard returned early), the FVC stays idle
211
+ // no claim, no pool mutation, no hijack. A later prop change to
212
+ // active=true triggers `feedVC.activate()` via the prop's didSet.
202
213
  if active {
203
214
  feedVC.activate()
204
215
  }
@@ -209,20 +220,15 @@ import ShortKitSDK
209
220
  /// (e.g. pushing a new screen on top). The feedVC and its state are preserved
210
221
  /// so they can be re-attached when the view returns to the window.
211
222
  private func suspendFeedViewController() {
212
- NSLog("[ShortKit FeedView] suspendFeedViewController ENTRY feedId=%@", feedId ?? "nil")
213
223
  if let feedId = self.feedId {
214
224
  ShortKitBridge.shared?.unregisterFeed(id: feedId)
215
225
  }
216
- guard let feedVC = feedViewController else {
217
- NSLog("[ShortKit FeedView] suspendFeedViewController NOOP — no feedVC")
218
- return
219
- }
226
+ guard let feedVC = feedViewController else { return }
220
227
 
221
228
  feedVC.deactivate()
222
229
  feedVC.willMove(toParent: nil)
223
230
  feedVC.view.removeFromSuperview()
224
231
  feedVC.removeFromParent()
225
- NSLog("[ShortKit FeedView] suspendFeedViewController EXIT — VC retained for re-attach")
226
232
  // Keep feedViewController reference — re-attached in embedFeedViewControllerIfNeeded
227
233
  }
228
234
 
@@ -29,5 +29,6 @@ RCT_EXPORT_VIEW_PROPERTY(feedId, NSString)
29
29
  RCT_EXPORT_VIEW_PROPERTY(seedThumbnailUrl, NSString)
30
30
  RCT_EXPORT_VIEW_PROPERTY(feedItemsJSON, NSString)
31
31
  RCT_EXPORT_VIEW_PROPERTY(active, BOOL)
32
+ RCT_EXPORT_VIEW_PROPERTY(debugPanel, NSNumber)
32
33
 
33
34
  @end
@@ -134,7 +134,9 @@ RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
134
134
  clientAppName:(NSString *)clientAppName
135
135
  clientAppVersion:(NSString *)clientAppVersion
136
136
  customDimensions:(NSString *)customDimensions
137
- debugPanel:(NSNumber *)debugPanel) {
137
+ debugPanel:(NSNumber *)debugPanel
138
+ serverTracingEnabled:(NSNumber *)serverTracingEnabled
139
+ consoleTracingEnabled:(NSNumber *)consoleTracingEnabled) {
138
140
  // Tear down any existing instance to prevent leaks on re-initialize
139
141
  [_shortKitBridge teardown];
140
142
 
@@ -144,6 +146,8 @@ RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
144
146
  clientAppVersion:clientAppVersion
145
147
  customDimensions:customDimensions
146
148
  debugPanel:[debugPanel boolValue]
149
+ serverTracingEnabled:[serverTracingEnabled boolValue]
150
+ consoleTracingEnabled:[consoleTracingEnabled boolValue]
147
151
  delegate:self
148
152
  surfacePresenter:_surfacePresenter];
149
153
 
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>ShortKitSDK.framework/ShortKitSDK</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64_x86_64-simulator</string>
11
+ <string>ios-arm64</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>ShortKitSDK.framework</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
- <string>x86_64</string>
18
17
  </array>
19
18
  <key>SupportedPlatform</key>
20
19
  <string>ios</string>
21
- <key>SupportedPlatformVariant</key>
22
- <string>simulator</string>
23
20
  </dict>
24
21
  <dict>
25
22
  <key>BinaryPath</key>
26
23
  <string>ShortKitSDK.framework/ShortKitSDK</string>
27
24
  <key>LibraryIdentifier</key>
28
- <string>ios-arm64</string>
25
+ <string>ios-arm64_x86_64-simulator</string>
29
26
  <key>LibraryPath</key>
30
27
  <string>ShortKitSDK.framework</string>
31
28
  <key>SupportedArchitectures</key>
32
29
  <array>
33
30
  <string>arm64</string>
31
+ <string>x86_64</string>
34
32
  </array>
35
33
  <key>SupportedPlatform</key>
36
34
  <string>ios</string>
35
+ <key>SupportedPlatformVariant</key>
36
+ <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>
@@ -11,9 +11,9 @@
11
11
  <key>CFBundlePackageType</key>
12
12
  <string>FMWK</string>
13
13
  <key>CFBundleVersion</key>
14
- <string>0.2.28</string>
14
+ <string>0.2.30</string>
15
15
  <key>CFBundleShortVersionString</key>
16
- <string>0.2.28</string>
16
+ <string>0.2.30</string>
17
17
  <key>MinimumOSVersion</key>
18
18
  <string>16.0</string>
19
19
  </dict>