@shortkitsdk/react-native 0.2.28 → 0.2.29

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 (25) hide show
  1. package/android/libs/shortkit-release.aar +0 -0
  2. package/ios/ReactVideoCarouselOverlayHost.swift +6 -0
  3. package/ios/ShortKitBridge.swift +134 -35
  4. package/ios/ShortKitFeedView.swift +43 -44
  5. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +2 -2
  6. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +696 -55
  7. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +20 -1
  8. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  9. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +20 -1
  10. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  11. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +9 -9
  12. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Info.plist +2 -2
  13. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +696 -55
  14. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +20 -1
  15. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  16. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +20 -1
  17. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +696 -55
  18. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +20 -1
  19. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  20. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +20 -1
  21. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  22. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +17 -17
  23. package/package.json +1 -1
  24. package/src/ShortKitFeed.tsx +31 -0
  25. package/src/specs/NativeShortKitModule.ts +6 -0
Binary file
@@ -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(
@@ -430,29 +475,21 @@ import ShortKitSDK
430
475
  let config = Self.parseFeedConfig(configJSON)
431
476
  let preload: FeedPreload?
432
477
 
433
- NSLog("[ShortKit Bridge] preloadFeed called — feedSource: %@, hasItemsJSON: %@",
434
- config.feedSource == .custom ? "custom" : "algorithmic",
435
- itemsJSON != nil ? "yes (\(itemsJSON!.prefix(100))...)" : "no")
436
-
437
478
  if config.feedSource == .custom {
438
479
  guard let json = itemsJSON, let items = Self.parseFeedInputs(json) else {
439
- NSLog("[ShortKit Bridge] ❌ preloadFeed: feedSource=custom but no valid items — returning empty")
440
480
  completion("")
441
481
  return
442
482
  }
443
- NSLog("[ShortKit Bridge] preloadFeed: creating custom preload with %d items", items.count)
444
483
  preload = shortKit?.preloadFeed(items: items)
445
484
  } else {
446
485
  preload = shortKit?.preloadFeed(filter: config.filter)
447
486
  }
448
487
 
449
488
  guard let preload else {
450
- NSLog("[ShortKit Bridge] ❌ preloadFeed: shortKit?.preloadFeed returned nil")
451
489
  completion("")
452
490
  return
453
491
  }
454
492
  let uuid = UUID().uuidString
455
- NSLog("[ShortKit Bridge] ✅ preloadFeed: created handle %@", uuid)
456
493
  preloadHandles[uuid] = preload
457
494
  activeFeedId = uuid
458
495
  completion(uuid)
@@ -553,38 +590,34 @@ import ShortKitSDK
553
590
  }
554
591
  .store(in: &cancellables)
555
592
 
556
- // Did loop
593
+ // Did loop — tagged with active surface's feedId. `player.didLoop`
594
+ // is a singleton Combine publisher on the shared player (only one
595
+ // item plays at a time). The event semantically belongs to
596
+ // whichever feed owns the active surface; tag with that feedId so
597
+ // JS consumers bound to specific feeds can filter.
557
598
  player.didLoop
558
599
  .receive(on: DispatchQueue.main)
559
600
  .sink { [weak self] event in
560
- self?.emit("onDidLoop", body: [
601
+ guard let self else { return }
602
+ let feedId = self.activeSurfaceFeedId()
603
+ self.emit("onDidLoop", body: [
604
+ "feedId": feedId,
561
605
  "contentId": event.contentId,
562
606
  "loopCount": event.loopCount
563
607
  ])
564
608
  }
565
609
  .store(in: &cancellables)
566
610
 
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)
611
+ // NOTE: The global `player.feedTransition` subscription used to live
612
+ // here. It's been removed feed transitions are now emitted to RN
613
+ // via the per-FVC `vc.onFeedTransition` closure wired in
614
+ // `registerFeed(id:viewController:)` above. This eliminates the
615
+ // cross-feed routing bug where every mounted <ShortKitFeed>
616
+ // consumer received every feed's transitions.
617
+ //
618
+ // `player.sendFeedTransition` is still called by FVC.handleSwipe
619
+ // for backward compatibility with native iOS consumers that
620
+ // subscribe to `ShortKit.player.feedTransition` directly.
588
621
 
589
622
  // Feed scroll phase — coalesced: only emit .dragging on first touch
590
623
  // (transition from settled), drop intermediate .dragging events. Always
@@ -613,11 +646,16 @@ import ShortKitSDK
613
646
  }
614
647
  .store(in: &cancellables)
615
648
 
616
- // Format change
649
+ // Format change — tagged with active surface's feedId. Same
650
+ // reasoning as onDidLoop: fires from shared player singleton, but
651
+ // semantically belongs to whichever feed owns the active surface.
617
652
  player.formatChange
618
653
  .receive(on: DispatchQueue.main)
619
654
  .sink { [weak self] event in
620
- self?.emit("onFormatChange", body: [
655
+ guard let self else { return }
656
+ let feedId = self.activeSurfaceFeedId()
657
+ self.emit("onFormatChange", body: [
658
+ "feedId": feedId,
621
659
  "contentId": event.contentId,
622
660
  "fromBitrate": Double(event.fromBitrate),
623
661
  "toBitrate": Double(event.toBitrate),
@@ -683,6 +721,52 @@ import ShortKitSDK
683
721
  return json
684
722
  }
685
723
 
724
+ /// Serialize a `FeedItem` to a ContentItem-shaped JSON string for the
725
+ /// `onFeedTransition` event. For `.content` cells this is the real
726
+ /// `ContentItem`. For every other cell kind (adSlot, imageCarousel,
727
+ /// survey, videoCarousel) there is no playable `ContentItem`, so we
728
+ /// synthesize a minimal one whose ONLY meaningful field is `id`.
729
+ ///
730
+ /// Why: JS hosts use `event.to` as an identity cursor for resume-on-
731
+ /// tab-return (`setFeedItems(startAt:)`). Before this synthesis, non-
732
+ /// content cells came through as `to=null` and the host's stored
733
+ /// resume id stayed pinned to the last regular video — causing the
734
+ /// SDK to re-seed the feed at the wrong index on tab return, which
735
+ /// in turn caused black carousel cells + wrong audio (see repro
736
+ /// analysis in debug.log, apr 2026).
737
+ ///
738
+ /// Scope: this is a bridge-only synthesis. It never flows back into
739
+ /// the Swift SDK (pool, cache, FeedDataSource, etc. all still see the
740
+ /// real `FeedItem` / `ContentItem?` and treat non-content cells
741
+ /// honestly). JS-side consumers should ONLY read `.id` / `.playbackId`
742
+ /// from `onFeedTransition` items — deeper fields are placeholders for
743
+ /// non-content cells.
744
+ ///
745
+ /// TODO: when we want to expose richer per-cell metadata on the JS
746
+ /// side, migrate to a proper FeedPosition event payload rather than
747
+ /// extending this synthesis. See the long-term fix plan.
748
+ private func serializeFeedItemIdentityJSON(_ feedItem: FeedItem) -> String {
749
+ if let contentItem = feedItem.contentItem {
750
+ return serializeContentItemToJSON(contentItem)
751
+ }
752
+ let synthetic = ContentItem(
753
+ id: feedItem.id,
754
+ playbackId: nil,
755
+ title: "",
756
+ description: nil,
757
+ duration: 0,
758
+ streamingUrl: "",
759
+ thumbnailUrl: "",
760
+ captionTracks: [],
761
+ customMetadata: nil,
762
+ author: nil,
763
+ articleUrl: nil,
764
+ commentCount: nil,
765
+ fallbackUrl: nil
766
+ )
767
+ return serializeContentItemToJSON(synthetic)
768
+ }
769
+
686
770
  /// Build an NSDictionary from a ContentItem with fields matching the JS spec.
687
771
  /// `captionTracks` and `customMetadata` are JSON-serialized strings.
688
772
  private static func contentItemDict(_ item: ContentItem) -> [String: Any] {
@@ -1007,7 +1091,14 @@ import ShortKitSDK
1007
1091
 
1008
1092
  extension ShortKitBridge: ShortKitDelegate {
1009
1093
  public func shortKit(_ shortKit: ShortKit, didTapContent contentId: String, at index: Int) {
1094
+ // Tag with active surface's feedId. The delegate is a singleton
1095
+ // (the bridge), so we use the activeSurface lookup to attribute
1096
+ // this tap to the feed it originated from. Without the tag, every
1097
+ // mounted <ShortKitFeed> consumer would fire onContentTapped on
1098
+ // every tap — including taps in sibling feeds.
1099
+ let feedId = activeSurfaceFeedId()
1010
1100
  emitOnMain("onContentTapped", body: [
1101
+ "feedId": feedId,
1011
1102
  "contentId": contentId,
1012
1103
  "index": index
1013
1104
  ])
@@ -1038,10 +1129,18 @@ extension ShortKitBridge: ShortKitDelegate {
1038
1129
  self.itemCache[item.id] = item
1039
1130
  }
1040
1131
  }
1132
+ // Capture the feedId synchronously before the async Task — after
1133
+ // the await, activeSurface may have shifted (user swiped tabs).
1134
+ // The fetch semantically belongs to whichever feed initiated it,
1135
+ // which at this delegate-call moment is the active surface.
1136
+ let feedId = activeSurfaceFeedId()
1041
1137
  Task {
1042
1138
  let data = try? JSONEncoder().encode(items)
1043
1139
  let json = data.flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
1044
- self.emitOnMain("onDidFetchContentItems", body: ["items": json])
1140
+ self.emitOnMain("onDidFetchContentItems", body: [
1141
+ "feedId": feedId,
1142
+ "items": json
1143
+ ])
1045
1144
  }
1046
1145
  }
1047
1146
  }
@@ -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 {
@@ -62,9 +67,16 @@ import ShortKitSDK
62
67
 
63
68
  // MARK: - Lifecycle
64
69
 
70
+ public override init(frame: CGRect) {
71
+ super.init(frame: frame)
72
+ }
73
+
74
+ public required init?(coder: NSCoder) {
75
+ super.init(coder: coder)
76
+ }
77
+
65
78
  public override func didMoveToWindow() {
66
79
  super.didMoveToWindow()
67
-
68
80
  if window != nil {
69
81
  embedFeedViewControllerIfNeeded()
70
82
  }
@@ -72,8 +84,11 @@ import ShortKitSDK
72
84
 
73
85
  public override func willMove(toWindow newWindow: UIWindow?) {
74
86
  super.willMove(toWindow: newWindow)
75
-
76
- if newWindow == nil && !isActiveManagedByProp {
87
+ // Suspend only if we're leaving the window AND the prop says this
88
+ // surface should be active. When the prop is false, the consumer has
89
+ // already driven the VC into deactivate via active.didSet (or will,
90
+ // once the VC exists), and we mustn't double-suspend.
91
+ if newWindow == nil && active {
77
92
  suspendFeedViewController()
78
93
  }
79
94
  }
@@ -96,18 +111,21 @@ import ShortKitSDK
96
111
 
97
112
  // Re-attach an existing suspended VC (e.g. after native stack pop)
98
113
  if let feedVC = feedViewController {
99
- NSLog("[ShortKit FeedView] re-attaching suspended VC feedId=%@", feedId ?? "nil")
100
114
  parentVC.addChild(feedVC)
101
115
  feedVC.view.frame = bounds
102
116
  addSubview(feedVC.view)
103
117
  feedVC.didMove(toParent: parentVC)
104
- if !isActiveManagedByProp {
118
+ // Authoritative: read the current active prop and act on it.
119
+ // If prop is false, stay deactivated — do NOT claim the pool.
120
+ // This is the fix for the prop-arrived-before-VC race: a prop
121
+ // update dropped by the didSet guard (vcExists=false) lands here
122
+ // and is honored.
123
+ if active {
105
124
  feedVC.activate()
106
125
  }
107
126
  if let feedId = self.feedId {
108
127
  ShortKitBridge.shared?.registerFeed(id: feedId, viewController: feedVC)
109
128
  }
110
- NSLog("[ShortKit FeedView] re-attach complete — activate() called")
111
129
  return
112
130
  }
113
131
 
@@ -120,33 +138,22 @@ import ShortKitSDK
120
138
 
121
139
  // Consume preload handle if available
122
140
  if let preloadId = self.preloadId {
123
- NSLog("[ShortKit FeedView] preloadId prop: %@", preloadId)
124
141
  if let preload = ShortKitBridge.shared?.consumePreload(id: preloadId) {
125
142
  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
143
  }
131
144
  } else if let json = self.feedItemsJSON,
132
145
  let items = ShortKitBridge.parseFeedInputs(json) {
133
146
  // feedItems prop: wrap in an immediate preload (no async prefetch
134
147
  // work — items are available synchronously at viewDidLoad).
135
148
  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
149
  }
140
150
 
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()
151
+ let feedVC = ShortKitFeedViewController(
152
+ shortKit: sdk,
153
+ config: feedConfig,
154
+ startAtItemId: startAtItemId,
155
+ lifecycle: .manual
156
+ )
150
157
 
151
158
  // Seed a thumbnail from the host app's image cache so the first cell
152
159
  // renders with a visible thumbnail from frame zero. Synchronous
@@ -174,8 +181,6 @@ import ShortKitSDK
174
181
  }
175
182
  }
176
183
 
177
- NSLog("[ShortKit FeedView] VC created successfully (bridge-managed)")
178
-
179
184
  feedVC.onDismiss = {
180
185
  ShortKitBridge.shared?.emitDismiss()
181
186
  }
@@ -192,13 +197,12 @@ import ShortKitSDK
192
197
  feedVC.didMove(toParent: parentVC)
193
198
 
194
199
  // 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.
200
+ // surfaces, the bridge is the sole authority on claim timing. Read
201
+ // the authoritative `active` prop and act on it. If the prop is
202
+ // false (including the case where it was set to false before the VC
203
+ // existed and the didSet guard returned early), the FVC stays idle
204
+ // no claim, no pool mutation, no hijack. A later prop change to
205
+ // active=true triggers `feedVC.activate()` via the prop's didSet.
202
206
  if active {
203
207
  feedVC.activate()
204
208
  }
@@ -209,20 +213,15 @@ import ShortKitSDK
209
213
  /// (e.g. pushing a new screen on top). The feedVC and its state are preserved
210
214
  /// so they can be re-attached when the view returns to the window.
211
215
  private func suspendFeedViewController() {
212
- NSLog("[ShortKit FeedView] suspendFeedViewController ENTRY feedId=%@", feedId ?? "nil")
213
216
  if let feedId = self.feedId {
214
217
  ShortKitBridge.shared?.unregisterFeed(id: feedId)
215
218
  }
216
- guard let feedVC = feedViewController else {
217
- NSLog("[ShortKit FeedView] suspendFeedViewController NOOP — no feedVC")
218
- return
219
- }
219
+ guard let feedVC = feedViewController else { return }
220
220
 
221
221
  feedVC.deactivate()
222
222
  feedVC.willMove(toParent: nil)
223
223
  feedVC.view.removeFromSuperview()
224
224
  feedVC.removeFromParent()
225
- NSLog("[ShortKit FeedView] suspendFeedViewController EXIT — VC retained for re-attach")
226
225
  // Keep feedViewController reference — re-attached in embedFeedViewControllerIfNeeded
227
226
  }
228
227
 
@@ -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.29</string>
15
15
  <key>CFBundleShortVersionString</key>
16
- <string>0.2.28</string>
16
+ <string>0.2.29</string>
17
17
  <key>MinimumOSVersion</key>
18
18
  <string>16.0</string>
19
19
  </dict>