@shortkitsdk/react-native 0.2.31 → 0.2.33

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 (41) hide show
  1. package/android/libs/shortkit-release.aar +0 -0
  2. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +26 -5
  3. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +19 -5
  4. package/ios/FeedMaskHostView.swift +190 -0
  5. package/ios/ShortKitBridge.swift +111 -3
  6. package/ios/ShortKitModule.mm +5 -1
  7. package/ios/ShortKitPlayerNativeView.swift +31 -0
  8. package/ios/ShortKitPlayerNativeViewManager.mm +1 -0
  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 +3060 -259
  12. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +84 -7
  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 +84 -7
  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 +3060 -259
  19. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +84 -7
  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 +84 -7
  22. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +3060 -259
  23. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +84 -7
  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 +84 -7
  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/ios/ShortKitWidgetNativeView.swift +153 -6
  29. package/ios/ShortKitWidgetNativeViewManager.mm +2 -0
  30. package/package.json +1 -1
  31. package/src/ShortKitCommands.ts +12 -2
  32. package/src/ShortKitContext.ts +7 -1
  33. package/src/ShortKitFeedMaskSurface.tsx +132 -0
  34. package/src/ShortKitPlayer.tsx +15 -2
  35. package/src/ShortKitWidget.tsx +16 -2
  36. package/src/index.ts +8 -1
  37. package/src/serialization.ts +15 -0
  38. package/src/specs/NativeShortKitModule.ts +2 -1
  39. package/src/specs/ShortKitPlayerViewNativeComponent.ts +7 -0
  40. package/src/specs/ShortKitWidgetViewNativeComponent.ts +15 -0
  41. package/src/types.ts +99 -0
@@ -31,11 +31,40 @@ import ShortKitSDK
31
31
  /// one is mounted simultaneously.
32
32
  @objc public var widgetId: String?
33
33
 
34
+ /// Mirrors `ShortKitWidgetViewController.active`. Hosts use this to
35
+ /// release the widget's pool tile when it shouldn't be playing
36
+ /// (e.g. inactive tab, scrolled off-screen). Defaults to `true` so
37
+ /// existing host integrations behave identically to before.
38
+ @objc public var active: Bool = true {
39
+ didSet {
40
+ guard active != oldValue else { return }
41
+ widgetVC?.active = active
42
+ }
43
+ }
44
+
45
+ /// JSON-serialized `FeedInput[]` used to seed the expanded feed when
46
+ /// a card is tapped and `clickAction == .feed`. See
47
+ /// `ShortKitWidgetProps.feedItems` in the JS layer for full
48
+ /// semantics. The parsed list is forwarded to the SDK via
49
+ /// `ShortKitWidgetViewController.setFeedItems(_:)`.
50
+ @objc public var feedItems: String? {
51
+ didSet {
52
+ guard feedItems != oldValue else { return }
53
+ applyFeedItems()
54
+ }
55
+ }
56
+
34
57
  // MARK: - Child VC
35
58
 
36
59
  private var widgetVC: ShortKitWidgetViewController?
37
60
  private var parsedConfig: WidgetConfig?
38
61
  private var parsedItems: [WidgetInput] = []
62
+ private var parsedFeedItems: [FeedInput] = []
63
+ /// Parsed feedMask mode pulled out of the serialized config JSON in
64
+ /// `applyConfig()`. Stored separately because the SDK's
65
+ /// `ShortKitWidgetViewController.feedMask` is a top-level property,
66
+ /// not part of `WidgetConfig`.
67
+ private var parsedFeedMask: FeedMaskMode = .none
39
68
 
40
69
  // MARK: - Lifecycle
41
70
 
@@ -49,7 +78,17 @@ import ShortKitSDK
49
78
  public override func willMove(toWindow newWindow: UIWindow?) {
50
79
  super.willMove(toWindow: newWindow)
51
80
  if newWindow == nil {
52
- removeWidgetVC()
81
+ // The host UIView is leaving the window — typically a
82
+ // transient detach driven by `react-native-screens` when
83
+ // a tab blurs or a screen is pushed on top. We must NOT
84
+ // destroy the widget VC here; doing so causes the user
85
+ // to see the host's background through the widget on
86
+ // return, plus a fresh full reload of cards / players /
87
+ // HLS streams. Suspend instead: detach from the parent
88
+ // VC hierarchy + pause playback, but keep the VC and
89
+ // all its loaded state alive so we can re-attach in
90
+ // `didMoveToWindow` instantly.
91
+ suspendWidgetVC()
53
92
  }
54
93
  }
55
94
 
@@ -58,18 +97,62 @@ import ShortKitSDK
58
97
  widgetVC?.view.frame = bounds
59
98
  }
60
99
 
100
+ deinit {
101
+ // Final teardown when React truly unmounts the bridge UIView.
102
+ // The instance is going away, so it's safe to release the
103
+ // child VC's parent-chain references here. Without this, a
104
+ // suspended widget VC would leak (its parent VC still holds
105
+ // it as a child until the parent itself is deallocated).
106
+ destroyWidgetVC()
107
+ }
108
+
61
109
  // MARK: - VC Containment
62
110
 
63
111
  private func embedWidgetVCIfNeeded() {
64
- guard widgetVC == nil else { return }
65
- guard let sdk = ShortKitBridge.shared?.sdk else { return }
66
112
  guard let parentVC = findParentViewController() else { return }
67
113
 
114
+ // Re-attach a suspended VC. All loaded state — items, player
115
+ // pool checkouts, collection-view layout, scroll position —
116
+ // is preserved across a suspend/resume cycle, so the user
117
+ // sees the same widget reappear in place rather than a fresh
118
+ // load. Mirrors `ShortKitPlayerNativeView`.
119
+ if let vc = widgetVC {
120
+ parentVC.addChild(vc)
121
+ vc.view.frame = bounds
122
+ addSubview(vc.view)
123
+ vc.didMove(toParent: parentVC)
124
+ // Re-sync the active prop. While suspended we forced the
125
+ // VC inactive; restore whatever the JS prop currently says.
126
+ vc.active = active
127
+ return
128
+ }
129
+
130
+ // First mount — create a fresh VC.
131
+ guard let sdk = ShortKitBridge.shared?.sdk else { return }
132
+
68
133
  let widgetConfig = parsedConfig ?? WidgetConfig()
69
134
 
70
135
  // Pass items at construction so the VC never races the server fetch —
71
136
  // analogous to the feed's `feedItems` prop wiring.
72
137
  let vc = ShortKitWidgetViewController(shortKit: sdk, config: widgetConfig, items: parsedItems)
138
+ // Sync the active prop to the freshly-created VC. Without this, a
139
+ // widget that mounts already with active=false (e.g. on an
140
+ // unfocused tab) would still go through its initial-load path and
141
+ // start rotating before the prop change reaches it.
142
+ vc.active = active
143
+ // Forward the optional expanded-feed seed list. Set before view
144
+ // load so it's available the first time `openFeed()` runs —
145
+ // same ordering rationale as the player's bridge.
146
+ if !parsedFeedItems.isEmpty {
147
+ vc.setFeedItems(parsedFeedItems)
148
+ }
149
+ // Forward the optional host-supplied feed mask. The SDK's
150
+ // `feedMask` lives as a top-level VC property (not in
151
+ // WidgetConfig), so it has to be set after construction. The
152
+ // factory closure inside the mode is invoked by the SDK each
153
+ // time `openFeed()` fires — so each expansion gets its own
154
+ // FeedMaskHostView instance.
155
+ vc.feedMask = parsedFeedMask
73
156
  self.widgetVC = vc
74
157
 
75
158
  // Wire host-handleable card-tap callback. When the JS wrapper has
@@ -96,7 +179,25 @@ import ShortKitSDK
96
179
  vc.didMove(toParent: parentVC)
97
180
  }
98
181
 
99
- private func removeWidgetVC() {
182
+ /// Detach the widget VC from the parent VC + view hierarchy
183
+ /// without destroying it. Called when the bridge UIView leaves
184
+ /// the window (typically a `react-native-screens` tab blur).
185
+ /// All of the widget's loaded state is preserved so the
186
+ /// re-attach in `embedWidgetVCIfNeeded` can be instant. Pause
187
+ /// playback so the SDK pool tiles aren't held while suspended.
188
+ private func suspendWidgetVC() {
189
+ guard let vc = widgetVC else { return }
190
+ vc.active = false
191
+ vc.willMove(toParent: nil)
192
+ vc.view.removeFromSuperview()
193
+ vc.removeFromParent()
194
+ // Keep widgetVC reference — re-attached in embedWidgetVCIfNeeded.
195
+ }
196
+
197
+ /// Full teardown — called from `deinit` when React unmounts the
198
+ /// native view, and from `applyConfig` when the config string
199
+ /// changes and we need to rebuild the VC from scratch.
200
+ private func destroyWidgetVC() {
100
201
  guard let vc = widgetVC else { return }
101
202
  vc.willMove(toParent: nil)
102
203
  vc.view.removeFromSuperview()
@@ -109,12 +210,37 @@ import ShortKitSDK
109
210
  private func applyConfig() {
110
211
  guard let json = config else { return }
111
212
  parsedConfig = Self.parseWidgetConfig(json)
213
+ // FeedMask is stored as a sibling field in the serialized
214
+ // config JSON; extract it here so it's available when the VC
215
+ // gets (re)constructed in embedWidgetVCIfNeeded.
216
+ parsedFeedMask = Self.extractFeedMask(json)
112
217
  if widgetVC != nil {
113
- removeWidgetVC()
218
+ destroyWidgetVC()
114
219
  embedWidgetVCIfNeeded()
115
220
  }
116
221
  }
117
222
 
223
+ /// Pulls the `feedMask` field out of the raw config JSON without
224
+ /// re-parsing the rest of `WidgetConfig`. Returns `.none` if the
225
+ /// JSON is malformed or omits `feedMask`. Mirrors how the
226
+ /// equivalent value is read on `ShortKitPlayerNativeView`.
227
+ private static func extractFeedMask(_ json: String) -> FeedMaskMode {
228
+ guard let data = json.data(using: .utf8),
229
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
230
+ return .none
231
+ }
232
+ // The serializer emits feedMask either as the string "none" or
233
+ // as a {type, name} object — both arrive nested as a sub-JSON
234
+ // value (not stringified again, unlike feedConfig).
235
+ if let mask = obj["feedMask"] {
236
+ if let data = try? JSONSerialization.data(withJSONObject: mask),
237
+ let json = String(data: data, encoding: .utf8) {
238
+ return ShortKitBridge.parseFeedMask(json)
239
+ }
240
+ }
241
+ return .none
242
+ }
243
+
118
244
  private func applyItems() {
119
245
  parsedItems = items.flatMap { Self.parseWidgetInputs($0) } ?? []
120
246
  // Post-mount updates: push items through the public API.
@@ -123,6 +249,18 @@ import ShortKitSDK
123
249
  }
124
250
  }
125
251
 
252
+ private func applyFeedItems() {
253
+ // Empty / nil JSON ⇒ clear the host seed.
254
+ guard let json = feedItems,
255
+ let items = ShortKitBridge.parseFeedInputs(json) else {
256
+ parsedFeedItems = []
257
+ widgetVC?.setFeedItems([])
258
+ return
259
+ }
260
+ parsedFeedItems = items
261
+ widgetVC?.setFeedItems(items)
262
+ }
263
+
126
264
  // MARK: - Parsing
127
265
 
128
266
  private static func parseWidgetConfig(_ json: String) -> WidgetConfig {
@@ -138,6 +276,13 @@ import ShortKitSDK
138
276
  let muteOnStart = obj["muteOnStart"] as? Bool ?? true
139
277
  let loop = obj["loop"] as? Bool ?? true
140
278
  let rotationInterval = obj["rotationInterval"] as? TimeInterval ?? 10000
279
+ let previewDuration = obj["previewDuration"] as? TimeInterval ?? 5
280
+ let playbackMode: WidgetPlaybackMode
281
+ switch obj["playbackMode"] as? String {
282
+ case "allVisibleSimultaneous": playbackMode = .allVisibleSimultaneous
283
+ case "singleVisibleRotating": playbackMode = .singleVisibleRotating
284
+ default: playbackMode = .singleVisibleRotating
285
+ }
141
286
 
142
287
  let clickAction: PlayerClickAction
143
288
  switch obj["clickAction"] as? String {
@@ -180,7 +325,9 @@ import ShortKitSDK
180
325
  clickAction: clickAction,
181
326
  cardOverlay: overlayMode,
182
327
  feedConfig: feedConfig,
183
- filter: filter
328
+ filter: filter,
329
+ playbackMode: playbackMode,
330
+ previewDuration: previewDuration
184
331
  )
185
332
  }
186
333
 
@@ -24,5 +24,7 @@ RCT_EXPORT_MODULE(ShortKitWidgetView)
24
24
  RCT_EXPORT_VIEW_PROPERTY(config, NSString)
25
25
  RCT_EXPORT_VIEW_PROPERTY(items, NSString)
26
26
  RCT_EXPORT_VIEW_PROPERTY(widgetId, NSString)
27
+ RCT_EXPORT_VIEW_PROPERTY(active, BOOL)
28
+ RCT_EXPORT_VIEW_PROPERTY(feedItems, NSString)
27
29
 
28
30
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shortkitsdk/react-native",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
4
4
  "description": "ShortKit React Native SDK — short-form video feed",
5
5
  "react-native": "src/index",
6
6
  "source": "src/index",
@@ -32,7 +32,17 @@ export const ShortKitCommands = {
32
32
  NativeShortKitModule?.prefetchStoryboard(playbackId),
33
33
  getStoryboardData: (playbackId: string): Promise<string | null> =>
34
34
  NativeShortKitModule?.getStoryboardData(playbackId) ?? Promise.resolve(null),
35
- downloadVideo: (itemId: string, mode: 'nonInterruptive' | 'interruptive' = 'nonInterruptive'): Promise<string> =>
36
- NativeShortKitModule?.downloadVideo(itemId, mode) ?? Promise.reject(new Error('ShortKit not initialized')),
35
+ downloadVideo: (
36
+ itemId: string,
37
+ options?: {
38
+ mode?: 'interruptive' | 'nonInterruptive';
39
+ overlayMode?: 'none' | 'static' | 'deterministic';
40
+ }
41
+ ): Promise<string> => {
42
+ const mode = options?.mode ?? 'nonInterruptive';
43
+ const overlayMode = options?.overlayMode ?? 'none';
44
+ return NativeShortKitModule?.downloadVideo(itemId, mode, overlayMode)
45
+ ?? Promise.reject(new Error('ShortKit not initialized'));
46
+ },
37
47
  cancelDownload: () => NativeShortKitModule?.cancelDownload(),
38
48
  } as const;
@@ -55,7 +55,13 @@ export interface ShortKitContextValue {
55
55
 
56
56
  // Download
57
57
  downloadState: DownloadState;
58
- downloadVideo: (itemId: string, mode?: 'nonInterruptive' | 'interruptive') => Promise<string>;
58
+ downloadVideo: (
59
+ itemId: string,
60
+ options?: {
61
+ mode?: 'interruptive' | 'nonInterruptive';
62
+ overlayMode?: 'none' | 'static' | 'deterministic';
63
+ }
64
+ ) => Promise<string>;
59
65
  cancelDownload: () => void;
60
66
  }
61
67
 
@@ -0,0 +1,132 @@
1
+ import React from 'react';
2
+ import { AppRegistry, View, Text } from 'react-native';
3
+ import type { FeedMaskProps, ContentItem } from './types';
4
+ import { deserializeContentItem } from './serialization';
5
+
6
+ const SK_MASK_TAG = '[ShortKit:FeedMaskSurface]';
7
+
8
+ // Named registry — supports different mask components per host site.
9
+ const _maskRegistry = new Map<string, React.ComponentType<FeedMaskProps>>();
10
+
11
+ /**
12
+ * Register a named feed-mask component for rendering around the
13
+ * expanded feed. Called by `<ShortKitPlayer>` / `<ShortKitWidget>` on
14
+ * mount via `useLayoutEffect` whenever `config.feedMask` is set with
15
+ * `type: 'custom'`. Idempotent — registering the same name twice is
16
+ * a no-op.
17
+ */
18
+ export function registerFeedMaskComponent(
19
+ name: string,
20
+ component: React.ComponentType<FeedMaskProps>,
21
+ ) {
22
+ if (_maskRegistry.has(name)) {
23
+ return;
24
+ }
25
+ _maskRegistry.set(name, component);
26
+
27
+ const moduleName = `ShortKitFeedMask_${name}`;
28
+ AppRegistry.registerComponent(moduleName, () => {
29
+ return function NamedFeedMaskSurface(props: RawFeedMaskSurfaceProps) {
30
+ return <ShortKitFeedMaskSurfaceInner {...props} maskName={name} />;
31
+ };
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Error boundary mirrors the overlay-surface boundary so a crash in
37
+ * the mask component doesn't bring down the SDK's modal presentation.
38
+ */
39
+ class FeedMaskErrorBoundary extends React.Component<
40
+ { maskName: string; children: React.ReactNode },
41
+ { error: Error | null }
42
+ > {
43
+ state: { error: Error | null } = { error: null };
44
+
45
+ static getDerivedStateFromError(error: Error) {
46
+ return { error };
47
+ }
48
+
49
+ componentDidCatch(error: Error, info: React.ErrorInfo) {
50
+ console.error(
51
+ `${SK_MASK_TAG} CRASH in mask '${this.props.maskName}':`,
52
+ error.message,
53
+ '\nComponent stack:',
54
+ info.componentStack,
55
+ );
56
+ }
57
+
58
+ render() {
59
+ if (this.state.error) {
60
+ if (__DEV__) {
61
+ return (
62
+ <View style={{ flex: 1, backgroundColor: 'rgba(255,0,0,0.3)', justifyContent: 'center', alignItems: 'center', padding: 20 }}>
63
+ <Text style={{ color: 'white', fontSize: 14, textAlign: 'center' }}>
64
+ {`Mask '${this.props.maskName}' crashed:\n${this.state.error.message}`}
65
+ </Text>
66
+ </View>
67
+ );
68
+ }
69
+ // Production fallback: render an empty view so the SDK still
70
+ // has a usable feedRegion via the embed slot the host inserts
71
+ // (which they no longer do, since the mask crashed). The SDK's
72
+ // placeholder UIView keeps the feed visible.
73
+ return null;
74
+ }
75
+ return this.props.children;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Raw props pushed in by the native bridge (`FeedMaskHostView`)
81
+ * via `SKFabricSurfaceWrapper.setProperties`. Item arrives serialized
82
+ * as a JSON string; everything else is a primitive.
83
+ */
84
+ interface RawFeedMaskSurfaceProps {
85
+ /** Serialized `ContentItem`. JSON-stringified by the bridge. */
86
+ item?: string;
87
+ /** Active item's index in the feed, when known. */
88
+ activeIndex?: number;
89
+ /** Total feed length, when known. */
90
+ totalCount?: number;
91
+ }
92
+
93
+ interface InnerProps extends RawFeedMaskSurfaceProps {
94
+ maskName: string;
95
+ }
96
+
97
+ function ShortKitFeedMaskSurfaceInner(props: InnerProps) {
98
+ const Component = _maskRegistry.get(props.maskName);
99
+ if (!Component) {
100
+ if (__DEV__) {
101
+ console.warn(
102
+ `${SK_MASK_TAG} mask '${props.maskName}' is not registered — did you set config.feedMask?`,
103
+ );
104
+ }
105
+ return null;
106
+ }
107
+
108
+ let item: ContentItem | null = null;
109
+ if (props.item) {
110
+ try {
111
+ item = deserializeContentItem(props.item);
112
+ } catch (e) {
113
+ console.warn(`${SK_MASK_TAG} failed to deserialize item:`, e);
114
+ }
115
+ }
116
+
117
+ // No item to render around → render nothing yet. The SDK will push
118
+ // a configure() with the active item shortly after expansion.
119
+ if (!item) {
120
+ return null;
121
+ }
122
+
123
+ return (
124
+ <FeedMaskErrorBoundary maskName={props.maskName}>
125
+ <Component
126
+ item={item}
127
+ activeIndex={props.activeIndex}
128
+ totalCount={props.totalCount}
129
+ />
130
+ </FeedMaskErrorBoundary>
131
+ );
132
+ }
@@ -6,6 +6,7 @@ import { serializePlayerConfig } from './serialization';
6
6
  import { registerOverlayComponent } from './ShortKitOverlaySurface';
7
7
  import { registerCarouselOverlayComponent } from './ShortKitCarouselOverlaySurface';
8
8
  import { registerVideoCarouselOverlayComponent } from './ShortKitVideoCarouselOverlaySurface';
9
+ import { registerFeedMaskComponent } from './ShortKitFeedMaskSurface';
9
10
 
10
11
  /**
11
12
  * Single-video player component. Displays one video with thumbnail fallback
@@ -19,7 +20,7 @@ import { registerVideoCarouselOverlayComponent } from './ShortKitVideoCarouselOv
19
20
  * app's responsibility.
20
21
  */
21
22
  export function ShortKitPlayer(props: ShortKitPlayerProps) {
22
- const { config, contentItem, active, style } = props;
23
+ const { config, contentItem, active, feedItems, style } = props;
23
24
 
24
25
  const clickAction = config?.clickAction ?? 'feed';
25
26
 
@@ -39,7 +40,10 @@ export function ShortKitPlayer(props: ShortKitPlayerProps) {
39
40
  registerVideoCarouselOverlayComponent(fc.videoCarouselOverlay.name, fc.videoCarouselOverlay.component);
40
41
  }
41
42
  }
42
- }, [config?.overlay, config?.feedConfig]);
43
+ if (config?.feedMask && config.feedMask !== 'none') {
44
+ registerFeedMaskComponent(config.feedMask.name, config.feedMask.component);
45
+ }
46
+ }, [config?.overlay, config?.feedConfig, config?.feedMask]);
43
47
 
44
48
  const serializedConfig = useMemo(() => {
45
49
  return serializePlayerConfig(config ?? {});
@@ -50,6 +54,14 @@ export function ShortKitPlayer(props: ShortKitPlayerProps) {
50
54
  return JSON.stringify(contentItem);
51
55
  }, [contentItem]);
52
56
 
57
+ // Serialize the optional expanded-feed seed list. Empty/undefined ⇒
58
+ // pass `undefined` so the native side falls through to the legacy
59
+ // single-item behavior (preload built from `self.items`).
60
+ const serializedFeedItems = useMemo(() => {
61
+ if (!feedItems || feedItems.length === 0) return undefined;
62
+ return JSON.stringify(feedItems);
63
+ }, [feedItems]);
64
+
53
65
  // When clickAction is "none", the native view should not participate in
54
66
  // Fabric's JS-side hit testing. Without this, Fabric on Android sees the
55
67
  // native component as the touch target and never routes the touch to a
@@ -76,6 +88,7 @@ export function ShortKitPlayer(props: ShortKitPlayerProps) {
76
88
  config={serializedConfig}
77
89
  contentItem={serializedItem}
78
90
  active={active}
91
+ feedItems={serializedFeedItems}
79
92
  pointerEvents={nativeTouchPassthrough ? 'none' : 'auto'}
80
93
  />
81
94
  </View>
@@ -8,6 +8,7 @@ import { serializeWidgetConfig } from './serialization';
8
8
  import { registerOverlayComponent } from './ShortKitOverlaySurface';
9
9
  import { registerCarouselOverlayComponent } from './ShortKitCarouselOverlaySurface';
10
10
  import { registerVideoCarouselOverlayComponent } from './ShortKitVideoCarouselOverlaySurface';
11
+ import { registerFeedMaskComponent } from './ShortKitFeedMaskSurface';
11
12
 
12
13
  // Local UUID generator. We don't pull in a runtime dep just for this; a
13
14
  // timestamp + random suffix is more than unique enough to disambiguate
@@ -23,7 +24,7 @@ function generateWidgetId(): string {
23
24
  * Must be rendered inside a `<ShortKitProvider>`.
24
25
  */
25
26
  export function ShortKitWidget(props: ShortKitWidgetProps) {
26
- const { config, items, onCardTap, style } = props;
27
+ const { config, items, onCardTap, active, feedItems, style } = props;
27
28
 
28
29
  const isInitialized = useContext(ShortKitInitContext);
29
30
  if (!isInitialized) {
@@ -69,7 +70,10 @@ export function ShortKitWidget(props: ShortKitWidgetProps) {
69
70
  registerVideoCarouselOverlayComponent(fc.videoCarouselOverlay.name, fc.videoCarouselOverlay.component);
70
71
  }
71
72
  }
72
- }, [config?.overlay, config?.feedConfig]);
73
+ if (config?.feedMask && config.feedMask !== 'none') {
74
+ registerFeedMaskComponent(config.feedMask.name, config.feedMask.component);
75
+ }
76
+ }, [config?.overlay, config?.feedConfig, config?.feedMask]);
73
77
 
74
78
  // Subscribe to the global widget-card-tap event, filtered to this
75
79
  // instance's widgetId.
@@ -93,6 +97,14 @@ export function ShortKitWidget(props: ShortKitWidgetProps) {
93
97
  return JSON.stringify(items);
94
98
  }, [items]);
95
99
 
100
+ // Serialize the optional expanded-feed seed list. Empty/undefined ⇒
101
+ // pass `undefined` so the native side falls through to the legacy
102
+ // behavior (preload built from `self.items` — the carousel cards).
103
+ const serializedFeedItems = useMemo(() => {
104
+ if (!feedItems || feedItems.length === 0) return undefined;
105
+ return JSON.stringify(feedItems);
106
+ }, [feedItems]);
107
+
96
108
  return (
97
109
  <View style={[styles.container, style]}>
98
110
  <ShortKitWidgetView
@@ -100,6 +112,8 @@ export function ShortKitWidget(props: ShortKitWidgetProps) {
100
112
  config={serializedConfig}
101
113
  items={serializedItems}
102
114
  widgetId={widgetId}
115
+ active={active}
116
+ feedItems={serializedFeedItems}
103
117
  />
104
118
  </View>
105
119
  );
package/src/index.ts CHANGED
@@ -52,4 +52,11 @@ export { default as NativeShortKitModule } from './specs/NativeShortKitModule';
52
52
  export { registerOverlayComponent } from './ShortKitOverlaySurface';
53
53
  export { registerCarouselOverlayComponent } from './ShortKitCarouselOverlaySurface';
54
54
  export { registerVideoCarouselOverlayComponent } from './ShortKitVideoCarouselOverlaySurface';
55
- export type { OverlayProps, CarouselOverlayProps, VideoCarouselOverlayProps } from './types';
55
+ export { registerFeedMaskComponent } from './ShortKitFeedMaskSurface';
56
+ export type {
57
+ OverlayProps,
58
+ CarouselOverlayProps,
59
+ VideoCarouselOverlayProps,
60
+ FeedMaskProps,
61
+ FeedMaskConfig,
62
+ } from './types';
@@ -2,6 +2,7 @@ import type {
2
2
  FeedConfig,
3
3
  ContentItem,
4
4
  FeedInput,
5
+ FeedMaskConfig,
5
6
  PlayerState,
6
7
  PlayerTime,
7
8
  WidgetConfig,
@@ -139,6 +140,16 @@ function serializeOverlay(overlay: OverlayConfig | undefined): string | { type:
139
140
  return { type: 'custom', name: overlay.name };
140
141
  }
141
142
 
143
+ /**
144
+ * FeedMask serialization mirrors the overlay path: only the name
145
+ * crosses the bridge — the JS-side registry maps the name back to
146
+ * the React component at mount time inside `FeedMaskHostView`.
147
+ */
148
+ function serializeFeedMask(mask: FeedMaskConfig | undefined): string | { type: 'custom'; name: string } {
149
+ if (!mask || mask === 'none') return 'none';
150
+ return { type: 'custom', name: mask.name };
151
+ }
152
+
142
153
  export function serializeWidgetConfig(config: WidgetConfig): string {
143
154
  const cfg = config ?? {};
144
155
  return JSON.stringify({
@@ -153,6 +164,9 @@ export function serializeWidgetConfig(config: WidgetConfig): string {
153
164
  overlay: serializeOverlay(cfg.overlay),
154
165
  filter: cfg.filter ? JSON.stringify(cfg.filter) : undefined,
155
166
  feedConfig: cfg.feedConfig ? serializeFeedConfig(cfg.feedConfig) : undefined,
167
+ feedMask: serializeFeedMask(cfg.feedMask),
168
+ playbackMode: cfg.playbackMode ?? 'singleVisibleRotating',
169
+ previewDuration: cfg.previewDuration ?? 5,
156
170
  });
157
171
  }
158
172
 
@@ -166,5 +180,6 @@ export function serializePlayerConfig(config: PlayerConfig): string {
166
180
  muteOnStart: cfg.muteOnStart ?? true,
167
181
  overlay: serializeOverlay(cfg.overlay),
168
182
  feedConfig: cfg.feedConfig ? serializeFeedConfig(cfg.feedConfig) : undefined,
183
+ feedMask: serializeFeedMask(cfg.feedMask),
169
184
  });
170
185
  }
@@ -140,6 +140,7 @@ type DownloadStartedEvent = Readonly<{
140
140
  type DownloadProgressEvent = Readonly<{
141
141
  itemId: string;
142
142
  progress: number;
143
+ phase: 'downloading' | 'compositing' | 'finalizing';
143
144
  }>;
144
145
 
145
146
  type DownloadCompletedEvent = Readonly<{
@@ -306,7 +307,7 @@ export interface Spec extends TurboModule {
306
307
  getStoryboardData(playbackId: string): Promise<string>;
307
308
 
308
309
  // --- Download ---
309
- downloadVideo(itemId: string, mode: string): Promise<string>;
310
+ downloadVideo(itemId: string, mode: string, overlayMode: string): Promise<string>;
310
311
  cancelDownload(): void;
311
312
 
312
313
  // --- Carousel accessors ---
@@ -5,6 +5,13 @@ export interface NativeProps extends ViewProps {
5
5
  config: string;
6
6
  contentItem?: string;
7
7
  active?: boolean;
8
+ /**
9
+ * Serialized JSON of the host-provided expanded-feed seed list.
10
+ * See `ShortKitPlayerProps.feedItems` for semantics. Sent as a
11
+ * string to keep the prop primitive (Codegen-friendly) — the
12
+ * native bridge parses + hydrates into `[ContentItem]`.
13
+ */
14
+ feedItems?: string;
8
15
  }
9
16
 
10
17
  export default codegenNativeComponent<NativeProps>(
@@ -10,6 +10,21 @@ export interface NativeProps extends ViewProps {
10
10
  * `<ShortKitWidget>` when multiple widgets are mounted.
11
11
  */
12
12
  widgetId?: string;
13
+ /**
14
+ * Whether the widget is currently allowed to play. Defaults to true on
15
+ * the native side. Hosts use this to release the widget's pool tile
16
+ * when it shouldn't be playing (inactive tab, scrolled off-screen,
17
+ * etc.) so other surfaces can claim the tile cleanly. Mirrors
18
+ * `<ShortKitFeed>`'s `active` prop.
19
+ */
20
+ active?: boolean;
21
+ /**
22
+ * Serialized JSON of the host-provided expanded-feed seed list.
23
+ * See `ShortKitWidgetProps.feedItems` for semantics. Sent as a
24
+ * string to keep the prop primitive (Codegen-friendly) — the
25
+ * native bridge parses + hydrates into `[FeedInput]`.
26
+ */
27
+ feedItems?: string;
13
28
  }
14
29
 
15
30
  export default codegenNativeComponent<NativeProps>(