@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.
- package/android/libs/shortkit-release.aar +0 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +8 -1
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +6 -0
- package/ios/ReactVideoCarouselOverlayHost.swift +6 -0
- package/ios/ShortKitBridge.swift +139 -36
- package/ios/ShortKitFeedView.swift +51 -45
- package/ios/ShortKitFeedViewManager.mm +1 -0
- package/ios/ShortKitModule.mm +5 -1
- package/ios/ShortKitSDK.xcframework/Info.plist +5 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +2 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +912 -103
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +21 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +21 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +9 -9
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Info.plist +2 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +912 -103
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +21 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +21 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +912 -103
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +21 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +21 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +17 -17
- package/package.json +1 -1
- package/src/ShortKitFeed.tsx +33 -0
- package/src/ShortKitProvider.tsx +4 -0
- package/src/specs/NativeShortKitModule.ts +8 -0
- package/src/specs/ShortKitFeedViewNativeComponent.ts +5 -0
- 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,
|
package/ios/ShortKitBridge.swift
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
568
|
-
//
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
|
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: [
|
|
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
|
-
///
|
|
42
|
-
///
|
|
43
|
-
|
|
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 {
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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.
|
|
196
|
-
// the
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
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
|
|
package/ios/ShortKitModule.mm
CHANGED
|
@@ -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-
|
|
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-
|
|
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.
|
|
14
|
+
<string>0.2.30</string>
|
|
15
15
|
<key>CFBundleShortVersionString</key>
|
|
16
|
-
<string>0.2.
|
|
16
|
+
<string>0.2.30</string>
|
|
17
17
|
<key>MinimumOSVersion</key>
|
|
18
18
|
<string>16.0</string>
|
|
19
19
|
</dict>
|