@shortkitsdk/react-native 0.2.6 → 0.2.12

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 (75) hide show
  1. package/ShortKitReactNative.podspec +1 -0
  2. package/android/build.gradle.kts +17 -1
  3. package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +379 -0
  4. package/android/src/main/java/com/shortkit/reactnative/ReactLoadingHost.kt +40 -0
  5. package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +570 -0
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +1029 -0
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +212 -219
  8. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +17 -3
  9. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +157 -742
  10. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +11 -2
  11. package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +2 -2
  12. package/ios/ReactCarouselOverlayHost.swift +177 -0
  13. package/ios/ReactLoadingHost.swift +38 -0
  14. package/ios/ReactOverlayHost.swift +444 -0
  15. package/ios/SKFabricSurfaceWrapper.h +18 -0
  16. package/ios/SKFabricSurfaceWrapper.mm +57 -0
  17. package/ios/ShortKitBridge.swift +220 -63
  18. package/ios/ShortKitFeedView.swift +82 -228
  19. package/ios/ShortKitFeedViewManager.mm +3 -2
  20. package/ios/ShortKitModule.mm +69 -37
  21. package/ios/ShortKitPlayerNativeView.swift +39 -8
  22. package/ios/ShortKitReactNative-Bridging-Header.h +2 -0
  23. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
  24. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +3683 -1249
  25. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +56 -15
  26. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  27. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +56 -15
  28. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  29. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
  30. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +3683 -1249
  31. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +56 -15
  32. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  33. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +56 -15
  34. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  35. package/ios/ShortKitSDK.xcframework.bak/Info.plist +43 -0
  36. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
  37. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
  38. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +28917 -0
  39. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +824 -0
  40. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  41. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +824 -0
  42. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  43. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  44. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
  45. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
  46. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +28917 -0
  47. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +824 -0
  48. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  49. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +824 -0
  50. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  51. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  52. package/ios/ShortKitWidgetNativeView.swift +3 -3
  53. package/package.json +1 -1
  54. package/src/ShortKitCarouselOverlaySurface.tsx +55 -0
  55. package/src/ShortKitCommands.ts +31 -0
  56. package/src/ShortKitContext.ts +6 -24
  57. package/src/ShortKitFeed.tsx +124 -41
  58. package/src/ShortKitLoadingSurface.tsx +24 -0
  59. package/src/ShortKitOverlaySurface.tsx +313 -0
  60. package/src/ShortKitPlayer.tsx +30 -9
  61. package/src/ShortKitProvider.tsx +28 -285
  62. package/src/index.ts +9 -3
  63. package/src/serialization.ts +20 -39
  64. package/src/specs/NativeShortKitModule.ts +74 -45
  65. package/src/specs/ShortKitFeedViewNativeComponent.ts +3 -2
  66. package/src/types.ts +84 -16
  67. package/src/useShortKit.ts +1 -3
  68. package/src/useShortKitPlayer.ts +7 -7
  69. package/android/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +0 -48
  70. package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +0 -128
  71. package/ios/ShortKitCarouselOverlayBridge.swift +0 -219
  72. package/ios/ShortKitOverlayBridge.swift +0 -111
  73. package/src/CarouselOverlayManager.tsx +0 -70
  74. package/src/OverlayManager.tsx +0 -87
  75. package/src/useShortKitCarousel.ts +0 -29
@@ -0,0 +1,313 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { AppRegistry, View, Text } from 'react-native';
3
+ import type { OverlayProps, PlayerState, PlayerTime, FeedScrollPhase } from './types';
4
+ import { deserializeContentItem } from './serialization';
5
+ import NativeShortKitModule from './specs/NativeShortKitModule';
6
+
7
+ const SK_OVERLAY_TAG = '[ShortKit:OverlaySurface]';
8
+
9
+ // Named registry — supports different overlay components per feed
10
+ const _overlayRegistry = new Map<string, React.ComponentType<OverlayProps>>();
11
+
12
+ /**
13
+ * Register a named overlay component for rendering inside feed cells.
14
+ * Called by ShortKitFeed on mount via useLayoutEffect.
15
+ * Idempotent — registering the same name twice is a no-op.
16
+ */
17
+ export function registerOverlayComponent(
18
+ name: string,
19
+ component: React.ComponentType<OverlayProps>,
20
+ ) {
21
+ if (_overlayRegistry.has(name)) {
22
+ return;
23
+ }
24
+ _overlayRegistry.set(name, component);
25
+
26
+ const moduleName = `ShortKitOverlay_${name}`;
27
+ AppRegistry.registerComponent(moduleName, () => {
28
+ return function NamedOverlaySurface(props: RawOverlaySurfaceProps) {
29
+ return <ShortKitOverlaySurfaceInner {...props} overlayName={name} />;
30
+ };
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Error boundary that catches crashes inside the overlay component.
36
+ * Without this, a crash in an isolated React surface is silent on Android.
37
+ */
38
+ class OverlayErrorBoundary extends React.Component<
39
+ { surfaceId?: string; overlayName: string; children: React.ReactNode },
40
+ { error: Error | null }
41
+ > {
42
+ state: { error: Error | null } = { error: null };
43
+
44
+ static getDerivedStateFromError(error: Error) {
45
+ return { error };
46
+ }
47
+
48
+ componentDidCatch(error: Error, info: React.ErrorInfo) {
49
+ console.error(
50
+ `${SK_OVERLAY_TAG} CRASH in overlay '${this.props.overlayName}' (surfaceId=${this.props.surfaceId}):`,
51
+ error.message,
52
+ '\nComponent stack:',
53
+ info.componentStack,
54
+ );
55
+ }
56
+
57
+ render() {
58
+ if (this.state.error) {
59
+ // Render a visible red indicator in __DEV__ so it's obvious
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
+ {`Overlay '${this.props.overlayName}' crashed:\n${this.state.error.message}`}
65
+ </Text>
66
+ </View>
67
+ );
68
+ }
69
+ return null;
70
+ }
71
+ return this.props.children;
72
+ }
73
+ }
74
+
75
+ /** Raw props received from native appProperties (set once per item in configure()). */
76
+ interface RawOverlaySurfaceProps {
77
+ surfaceId?: string;
78
+ item?: string;
79
+ isActive?: boolean;
80
+ playerState?: string;
81
+ isMuted?: boolean;
82
+ playbackRate?: number;
83
+ captionsEnabled?: boolean;
84
+ activeCue?: string;
85
+ feedScrollPhase?: string;
86
+ }
87
+
88
+ interface InnerProps extends RawOverlaySurfaceProps {
89
+ overlayName: string;
90
+ }
91
+
92
+ /**
93
+ * Subscribe to a native overlay event, filtered by surfaceId.
94
+ *
95
+ * Both iOS and Android (new arch) emit events through the codegen
96
+ * EventEmitter path (mEventEmitterCallback / JSI direct channel).
97
+ * We subscribe via the codegen emitter method on the TurboModule.
98
+ */
99
+ function useOverlayEvent<T extends { surfaceId: string }>(
100
+ eventName: string,
101
+ surfaceId: string | undefined,
102
+ handler: (event: T) => void,
103
+ ) {
104
+ useEffect(() => {
105
+ if (!surfaceId) return;
106
+
107
+ let sub: { remove: () => void } | undefined;
108
+
109
+ // Both platforms: events come through the codegen EventEmitterCallback.
110
+ // Access the codegen emitter method by name on the TurboModule.
111
+ const emitter = NativeShortKitModule?.[eventName as keyof typeof NativeShortKitModule];
112
+ if (typeof emitter === 'function') {
113
+ sub = (emitter as (cb: (e: T) => void) => { remove: () => void })((e: T) => {
114
+ if (e.surfaceId !== surfaceId) return;
115
+ handler(e);
116
+ });
117
+ }
118
+
119
+ return () => sub?.remove();
120
+ }, [surfaceId]);
121
+ }
122
+
123
+ function ShortKitOverlaySurfaceInner(props: InnerProps) {
124
+ const Component = _overlayRegistry.get(props.overlayName);
125
+ if (!Component) {
126
+ console.error(`${SK_OVERLAY_TAG} ShortKitOverlaySurfaceInner: Component NOT FOUND in registry for '${props.overlayName}'. Registered names: [${Array.from(_overlayRegistry.keys()).join(', ')}]`);
127
+ return null;
128
+ }
129
+
130
+ const [item, setItem] = useState(() =>
131
+ props.item ? deserializeContentItem(props.item) : null,
132
+ );
133
+
134
+ // Sync item from props when surface is first mounted or remounted via updateInitProps
135
+ const prevPropsItemRef = React.useRef(props.item);
136
+ if (props.item !== prevPropsItemRef.current) {
137
+ prevPropsItemRef.current = props.item;
138
+ // Synchronous state update during render — React restarts with new state
139
+ // before committing, so stale frame is never painted.
140
+ setItem(props.item ? deserializeContentItem(props.item) : null);
141
+ }
142
+
143
+ const sid = props.surfaceId;
144
+
145
+ // Initialize state from surface properties (set once in configure()).
146
+ // All subsequent updates arrive via events filtered by surfaceId.
147
+ const [isActive, setIsActive] = useState(props.isActive ?? false);
148
+ const [playerState, setPlayerState] = useState<PlayerState>(
149
+ (props.playerState ?? 'idle') as PlayerState,
150
+ );
151
+ const [isMuted, setIsMuted] = useState(props.isMuted ?? true);
152
+ const [playbackRate, setPlaybackRate] = useState(props.playbackRate ?? 1);
153
+ const [captionsEnabled, setCaptionsEnabled] = useState(
154
+ props.captionsEnabled ?? false,
155
+ );
156
+ const [time, setTime] = useState<PlayerTime>({
157
+ current: 0,
158
+ duration: 0,
159
+ buffered: 0,
160
+ });
161
+ const [activeCue, setActiveCue] = useState<OverlayProps['activeCue']>(
162
+ props.activeCue ? JSON.parse(props.activeCue) : null,
163
+ );
164
+ const [feedScrollPhase, setFeedScrollPhase] =
165
+ useState<FeedScrollPhase | null>(
166
+ props.feedScrollPhase ? JSON.parse(props.feedScrollPhase) : null,
167
+ );
168
+
169
+ // Reset state when the item changes (cell reuse). useState initial values
170
+ // only apply on mount, so on re-render with new props we must sync manually.
171
+ // This prevents stale progress bar / player state flashing from the previous cell.
172
+ useEffect(() => {
173
+ setIsActive(props.isActive ?? false);
174
+ setPlayerState((props.playerState ?? 'idle') as PlayerState);
175
+ setTime({ current: 0, duration: 0, buffered: 0 });
176
+ setIsMuted(props.isMuted ?? true);
177
+ setPlaybackRate(props.playbackRate ?? 1);
178
+ setCaptionsEnabled(props.captionsEnabled ?? false);
179
+ setActiveCue(props.activeCue ? JSON.parse(props.activeCue) : null);
180
+ setFeedScrollPhase(
181
+ props.feedScrollPhase ? JSON.parse(props.feedScrollPhase) : null,
182
+ );
183
+ }, [props.item]);
184
+
185
+ // --- Event subscriptions (filtered by surfaceId) ---
186
+
187
+ // Item changed via event (not updateInitProps) — triggers React DIFF
188
+ // instead of full remount. Uses DeviceEventEmitter because this event
189
+ // is not in the TurboModule spec (avoids native codegen changes).
190
+ useEffect(() => {
191
+ if (!sid) return;
192
+ const { DeviceEventEmitter } = require('react-native');
193
+ const sub = DeviceEventEmitter.addListener('onOverlayItemChanged', (e: { surfaceId: string; item: string }) => {
194
+ if (e.surfaceId !== sid) return;
195
+ const newItem = e.item ? deserializeContentItem(e.item) : null;
196
+ if (newItem) setItem(newItem);
197
+ // Reset playback state for the new item
198
+ setIsActive(false);
199
+ setPlayerState('idle' as PlayerState);
200
+ setTime({ current: 0, duration: 0, buffered: 0 });
201
+ });
202
+ return () => sub.remove();
203
+ }, [sid]);
204
+
205
+ useOverlayEvent<{ surfaceId: string; isActive: boolean }>(
206
+ 'onOverlayActiveChanged', sid,
207
+ (e) => setIsActive(e.isActive),
208
+ );
209
+
210
+ useOverlayEvent<{ surfaceId: string; playerState: string }>(
211
+ 'onOverlayPlayerStateChanged', sid,
212
+ (e) => setPlayerState(e.playerState as PlayerState),
213
+ );
214
+
215
+ useOverlayEvent<{ surfaceId: string; isMuted: boolean }>(
216
+ 'onOverlayMutedChanged', sid,
217
+ (e) => setIsMuted(e.isMuted),
218
+ );
219
+
220
+ useOverlayEvent<{ surfaceId: string; playbackRate: number }>(
221
+ 'onOverlayPlaybackRateChanged', sid,
222
+ (e) => setPlaybackRate(e.playbackRate),
223
+ );
224
+
225
+ useOverlayEvent<{ surfaceId: string; captionsEnabled: boolean }>(
226
+ 'onOverlayCaptionsEnabledChanged', sid,
227
+ (e) => setCaptionsEnabled(e.captionsEnabled),
228
+ );
229
+
230
+ useOverlayEvent<{ surfaceId: string; activeCue: string | null }>(
231
+ 'onOverlayActiveCueChanged', sid,
232
+ (e) => setActiveCue(e.activeCue ? JSON.parse(e.activeCue) : null),
233
+ );
234
+
235
+ useOverlayEvent<{ surfaceId: string; feedScrollPhase: string | null }>(
236
+ 'onOverlayFeedScrollPhaseChanged', sid,
237
+ (e) => {
238
+ try {
239
+ const next = e.feedScrollPhase ? JSON.parse(e.feedScrollPhase) : null;
240
+ // Stabilize object reference: only update state if the phase actually
241
+ // changed. Prevents unnecessary re-renders of memo'd overlay components
242
+ // that receive feedScrollPhase as a prop (new JSON.parse object !== old).
243
+ setFeedScrollPhase(prev => {
244
+ if (prev?.phase === next?.phase && prev?.fromId === next?.fromId) {
245
+ return prev;
246
+ }
247
+ return next;
248
+ });
249
+ } catch {
250
+ setFeedScrollPhase(null);
251
+ }
252
+ },
253
+ );
254
+
255
+ // EXPERIMENT: time updates disabled to measure impact on swipe perf
256
+ // useOverlayEvent<{ surfaceId: string; current: number; duration: number; buffered: number }>(
257
+ // 'onOverlayTimeUpdate', sid,
258
+ // (e) => setTime({ current: e.current, duration: e.duration, buffered: e.buffered }),
259
+ // );
260
+
261
+ // Batched full-state sync — replaces 7 individual events on swipe settle.
262
+ // All setState calls within one handler = one React render (auto-batched).
263
+ useOverlayEvent<{
264
+ surfaceId: string;
265
+ isActive: boolean;
266
+ playerState: string;
267
+ isMuted: boolean;
268
+ playbackRate: number;
269
+ captionsEnabled: boolean;
270
+ activeCue: string | null;
271
+ feedScrollPhase: string | null;
272
+ }>('onOverlayFullState', sid, (e) => {
273
+ setIsActive(e.isActive);
274
+ setPlayerState(e.playerState as PlayerState);
275
+ setIsMuted(e.isMuted);
276
+ setPlaybackRate(e.playbackRate);
277
+ setCaptionsEnabled(e.captionsEnabled);
278
+ try {
279
+ setActiveCue(e.activeCue ? JSON.parse(e.activeCue) : null);
280
+ } catch {
281
+ setActiveCue(null);
282
+ }
283
+ try {
284
+ const next = e.feedScrollPhase ? JSON.parse(e.feedScrollPhase) : null;
285
+ setFeedScrollPhase(prev => {
286
+ if (prev?.phase === next?.phase && prev?.fromId === next?.fromId) return prev;
287
+ return next;
288
+ });
289
+ } catch {
290
+ setFeedScrollPhase(null);
291
+ }
292
+ });
293
+
294
+ if (!item) {
295
+ return null;
296
+ }
297
+
298
+ return (
299
+ <OverlayErrorBoundary surfaceId={sid} overlayName={props.overlayName}>
300
+ <Component
301
+ item={item}
302
+ isActive={isActive}
303
+ playerState={playerState}
304
+ time={time}
305
+ isMuted={isMuted}
306
+ playbackRate={playbackRate}
307
+ captionsEnabled={captionsEnabled}
308
+ activeCue={activeCue}
309
+ feedScrollPhase={feedScrollPhase}
310
+ />
311
+ </OverlayErrorBoundary>
312
+ );
313
+ }
@@ -1,28 +1,29 @@
1
- import React, { useContext, useMemo } from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { View, StyleSheet } from 'react-native';
3
3
  import type { ShortKitPlayerProps } from './types';
4
4
  import ShortKitPlayerView from './specs/ShortKitPlayerViewNativeComponent';
5
- import { ShortKitContext } from './ShortKitContext';
6
5
 
7
6
  /**
8
7
  * Single-video player component. Displays one video with thumbnail fallback
9
8
  * and optional overlay. Wraps a native Fabric view.
10
9
  *
11
10
  * Must be rendered inside a `<ShortKitProvider>`.
11
+ *
12
+ * NOTE: Removed useContext(ShortKitContext) subscription — it was only used
13
+ * for an existence check but caused re-renders on every TIME update (~4x/sec)
14
+ * from the feed, flickering all grid tiles. The provider check is the host
15
+ * app's responsibility.
12
16
  */
13
17
  export function ShortKitPlayer(props: ShortKitPlayerProps) {
14
18
  const { config, contentItem, active, style } = props;
15
19
 
16
- const context = useContext(ShortKitContext);
17
- if (!context) {
18
- throw new Error('ShortKitPlayer must be used within a ShortKitProvider');
19
- }
20
+ const clickAction = config?.clickAction ?? 'feed';
20
21
 
21
22
  const serializedConfig = useMemo(() => {
22
23
  const cfg = config ?? {};
23
24
  return JSON.stringify({
24
25
  cornerRadius: cfg.cornerRadius ?? 12,
25
- clickAction: cfg.clickAction ?? 'feed',
26
+ clickAction: clickAction,
26
27
  autoplay: cfg.autoplay ?? true,
27
28
  loop: cfg.loop ?? true,
28
29
  muteOnStart: cfg.muteOnStart ?? true,
@@ -32,20 +33,40 @@ export function ShortKitPlayer(props: ShortKitPlayerProps) {
32
33
  : { type: 'custom' }
33
34
  : 'none',
34
35
  });
35
- }, [config]);
36
+ }, [config, clickAction]);
36
37
 
37
38
  const serializedItem = useMemo(() => {
38
39
  if (!contentItem) return undefined;
39
40
  return JSON.stringify(contentItem);
40
41
  }, [contentItem]);
41
42
 
43
+ // When clickAction is "none", the native view should not participate in
44
+ // Fabric's JS-side hit testing. Without this, Fabric on Android sees the
45
+ // native component as the touch target and never routes the touch to a
46
+ // parent Pressable — even though the native view returns false from
47
+ // dispatchTouchEvent. iOS doesn't need this because UIKit's responder
48
+ // chain bubbles unhandled touches automatically.
49
+ //
50
+ // "box-none" on the container means the container View itself is
51
+ // transparent to touches (passes through to parent), but children can
52
+ // still receive touches if needed. "none" on the native view means it
53
+ // is fully invisible to hit testing.
54
+ const nativeTouchPassthrough = clickAction === 'none';
55
+
42
56
  return (
43
- <View style={[styles.container, style]}>
57
+ <View
58
+ style={[styles.container, style]}
59
+ pointerEvents={nativeTouchPassthrough ? 'box-none' : 'auto'}
60
+ onStartShouldSetResponder={() => {
61
+ return false;
62
+ }}
63
+ >
44
64
  <ShortKitPlayerView
45
65
  style={styles.player}
46
66
  config={serializedConfig}
47
67
  contentItem={serializedItem}
48
68
  active={active}
69
+ pointerEvents={nativeTouchPassthrough ? 'none' : 'auto'}
49
70
  />
50
71
  </View>
51
72
  );