@shortkitsdk/react-native 0.1.0

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 (35) hide show
  1. package/ShortKitReactNative.podspec +19 -0
  2. package/android/build.gradle.kts +34 -0
  3. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +249 -0
  4. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +32 -0
  5. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +769 -0
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +101 -0
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitPackage.kt +40 -0
  8. package/app.plugin.js +1 -0
  9. package/ios/ShortKitBridge.swift +537 -0
  10. package/ios/ShortKitFeedView.swift +207 -0
  11. package/ios/ShortKitFeedViewManager.mm +29 -0
  12. package/ios/ShortKitModule.h +25 -0
  13. package/ios/ShortKitModule.mm +204 -0
  14. package/ios/ShortKitOverlayBridge.swift +91 -0
  15. package/ios/ShortKitReactNative-Bridging-Header.h +3 -0
  16. package/ios/ShortKitReactNative.podspec +19 -0
  17. package/package.json +50 -0
  18. package/plugin/build/index.d.ts +3 -0
  19. package/plugin/build/index.js +13 -0
  20. package/plugin/build/withShortKitAndroid.d.ts +8 -0
  21. package/plugin/build/withShortKitAndroid.js +32 -0
  22. package/plugin/build/withShortKitIOS.d.ts +8 -0
  23. package/plugin/build/withShortKitIOS.js +29 -0
  24. package/react-native.config.js +8 -0
  25. package/src/OverlayManager.tsx +87 -0
  26. package/src/ShortKitContext.ts +51 -0
  27. package/src/ShortKitFeed.tsx +203 -0
  28. package/src/ShortKitProvider.tsx +526 -0
  29. package/src/index.ts +26 -0
  30. package/src/serialization.ts +95 -0
  31. package/src/specs/NativeShortKitModule.ts +201 -0
  32. package/src/specs/ShortKitFeedViewNativeComponent.ts +13 -0
  33. package/src/types.ts +167 -0
  34. package/src/useShortKit.ts +20 -0
  35. package/src/useShortKitPlayer.ts +29 -0
@@ -0,0 +1,8 @@
1
+ import { ConfigPlugin } from '@expo/config-plugins';
2
+ /**
3
+ * iOS config plugin for ShortKit.
4
+ *
5
+ * - Sets the minimum iOS deployment target to 16.0 if lower.
6
+ * - No special permissions are required for video streaming playback.
7
+ */
8
+ export declare const withShortKitIOS: ConfigPlugin;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withShortKitIOS = void 0;
4
+ const config_plugins_1 = require("@expo/config-plugins");
5
+ /**
6
+ * iOS config plugin for ShortKit.
7
+ *
8
+ * - Sets the minimum iOS deployment target to 16.0 if lower.
9
+ * - No special permissions are required for video streaming playback.
10
+ */
11
+ const withShortKitIOS = (config) => {
12
+ return (0, config_plugins_1.withXcodeProject)(config, (mod) => {
13
+ const project = mod.modResults;
14
+ // Ensure minimum deployment target is iOS 16.0
15
+ const buildConfigs = project.pbxXCBuildConfigurationSection();
16
+ for (const key of Object.keys(buildConfigs)) {
17
+ const buildConfig = buildConfigs[key];
18
+ if (typeof buildConfig === 'object' &&
19
+ buildConfig.buildSettings?.IPHONEOS_DEPLOYMENT_TARGET) {
20
+ const current = parseFloat(buildConfig.buildSettings.IPHONEOS_DEPLOYMENT_TARGET);
21
+ if (current < 16.0) {
22
+ buildConfig.buildSettings.IPHONEOS_DEPLOYMENT_TARGET = '16.0';
23
+ }
24
+ }
25
+ }
26
+ return mod;
27
+ });
28
+ };
29
+ exports.withShortKitIOS = withShortKitIOS;
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {},
5
+ ios: {},
6
+ },
7
+ },
8
+ };
@@ -0,0 +1,87 @@
1
+ import React, { useContext, useMemo } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import type { OverlayConfig, PlayerState } from './types';
4
+ import { ShortKitContext } from './ShortKitContext';
5
+ import type { ShortKitContextValue } from './ShortKitContext';
6
+
7
+ interface OverlayManagerProps {
8
+ overlay: OverlayConfig;
9
+ }
10
+
11
+ /**
12
+ * Internal component that renders TWO instances of the developer's custom
13
+ * overlay component — one for the current cell and one for the next cell.
14
+ *
15
+ * On the native side, `ShortKitFeedView` finds these views by their
16
+ * `nativeID` and applies scroll-tracking transforms so each overlay moves
17
+ * with its respective cell during swipe transitions — matching the native
18
+ * iOS/Android SDK behavior where each cell has its own overlay instance.
19
+ *
20
+ * The "current" overlay uses the actual provider context.
21
+ * The "next" overlay uses a context override where `currentItem = nextItem`
22
+ * with initial playback state, so it renders the upcoming video's overlay
23
+ * content before the user scrolls to it.
24
+ */
25
+ export function OverlayManager({ overlay }: OverlayManagerProps) {
26
+ if (
27
+ overlay === 'none' ||
28
+ typeof overlay === 'string' ||
29
+ overlay.type !== 'custom' ||
30
+ !('component' in overlay)
31
+ ) {
32
+ return null;
33
+ }
34
+
35
+ const OverlayComponent = overlay.component;
36
+
37
+ return (
38
+ <>
39
+ <View style={StyleSheet.absoluteFill} nativeID="overlay-current" pointerEvents="box-none">
40
+ <OverlayComponent />
41
+ </View>
42
+ <View style={StyleSheet.absoluteFill} nativeID="overlay-next" pointerEvents="box-none">
43
+ <NextOverlayProvider>
44
+ <OverlayComponent />
45
+ </NextOverlayProvider>
46
+ </View>
47
+ </>
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Wraps children with a modified ShortKitContext where `currentItem` is
53
+ * set to the provider's `nextItem` (the item configured for the upcoming
54
+ * cell). Other state reflects an "about to play" initial state so the
55
+ * overlay renders correctly before playback starts on that cell.
56
+ */
57
+ function NextOverlayProvider({ children }: { children: React.ReactNode }) {
58
+ const context = useContext(ShortKitContext);
59
+
60
+ const nextValue: ShortKitContextValue | null = useMemo(() => {
61
+ if (!context) return null;
62
+
63
+ return {
64
+ ...context,
65
+ currentItem: context.nextItem,
66
+ isActive: context.nextItem != null,
67
+ time: {
68
+ current: 0,
69
+ duration: context.nextItem?.duration ?? 0,
70
+ buffered: 0,
71
+ },
72
+ playerState: 'idle' as PlayerState,
73
+ isTransitioning: false,
74
+ activeCue: null,
75
+ lastOverlayTap: 0,
76
+ lastOverlayDoubleTap: null,
77
+ };
78
+ }, [context]);
79
+
80
+ if (!nextValue) return <>{children}</>;
81
+
82
+ return (
83
+ <ShortKitContext.Provider value={nextValue}>
84
+ {children}
85
+ </ShortKitContext.Provider>
86
+ );
87
+ }
@@ -0,0 +1,51 @@
1
+ import { createContext } from 'react';
2
+ import type {
3
+ ContentItem,
4
+ PlayerTime,
5
+ PlayerState,
6
+ CaptionTrack,
7
+ ContentSignal,
8
+ OverlayConfig,
9
+ } from './types';
10
+
11
+ export interface ShortKitContextValue {
12
+ // Player state
13
+ playerState: PlayerState;
14
+ currentItem: ContentItem | null;
15
+ nextItem: ContentItem | null;
16
+ time: PlayerTime;
17
+ isMuted: boolean;
18
+ playbackRate: number;
19
+ captionsEnabled: boolean;
20
+ activeCaptionTrack: CaptionTrack | null;
21
+ activeCue: { text: string; startTime: number; endTime: number } | null;
22
+ prefetchedAheadCount: number;
23
+ isActive: boolean;
24
+ isTransitioning: boolean;
25
+ lastOverlayTap: number;
26
+ lastOverlayDoubleTap: { x: number; y: number; id: number } | null;
27
+
28
+ // Player commands
29
+ play: () => void;
30
+ pause: () => void;
31
+ seek: (seconds: number) => void;
32
+ seekAndPlay: (seconds: number) => void;
33
+ skipToNext: () => void;
34
+ skipToPrevious: () => void;
35
+ setMuted: (muted: boolean) => void;
36
+ setPlaybackRate: (rate: number) => void;
37
+ setCaptionsEnabled: (enabled: boolean) => void;
38
+ selectCaptionTrack: (language: string) => void;
39
+ sendContentSignal: (signal: ContentSignal) => void;
40
+ setMaxBitrate: (bitrate: number) => void;
41
+
42
+ // SDK operations
43
+ setUserId: (id: string) => void;
44
+ clearUserId: () => void;
45
+
46
+ // Internal — used by ShortKitFeed to render custom overlays
47
+ /** @internal */
48
+ _overlayConfig: OverlayConfig;
49
+ }
50
+
51
+ export const ShortKitContext = createContext<ShortKitContextValue | null>(null);
@@ -0,0 +1,203 @@
1
+ import React, { useContext, useEffect } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import type { ShortKitFeedProps } from './types';
4
+ import ShortKitFeedView from './specs/ShortKitFeedViewNativeComponent';
5
+ import NativeShortKitModule from './specs/NativeShortKitModule';
6
+ import { OverlayManager } from './OverlayManager';
7
+ import { ShortKitContext } from './ShortKitContext';
8
+ import { deserializeContentItem } from './serialization';
9
+
10
+ /**
11
+ * Renders the native ShortKit video feed and forwards feed-level events to
12
+ * callback props.
13
+ *
14
+ * Must be rendered inside a `<ShortKitProvider>`. The native feed view
15
+ * receives its configuration from the `NativeShortKitModule.initialize()` call
16
+ * made by the provider, so the view props here are placeholders required by
17
+ * the Fabric codegen contract.
18
+ *
19
+ * When the overlay config is of type `custom`, the `OverlayManager` is
20
+ * rendered alongside the native view to display the developer's React overlay
21
+ * component.
22
+ */
23
+ export function ShortKitFeed(props: ShortKitFeedProps) {
24
+ const {
25
+ style,
26
+ onError,
27
+ onShareTapped,
28
+ onSurveyResponse,
29
+ onLoop,
30
+ onFeedTransition,
31
+ onFormatChange,
32
+ onArticleTapped,
33
+ onCommentTapped,
34
+ onOverlayShareTapped,
35
+ onSaveTapped,
36
+ onLikeTapped,
37
+ } = props;
38
+
39
+ const context = useContext(ShortKitContext);
40
+ if (!context) {
41
+ throw new Error('ShortKitFeed must be used within a ShortKitProvider');
42
+ }
43
+
44
+ const overlayConfig = context._overlayConfig;
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Subscribe to feed-level events and forward to callback props
48
+ // ---------------------------------------------------------------------------
49
+ useEffect(() => {
50
+ if (!NativeShortKitModule) return;
51
+
52
+ const subscriptions: Array<{ remove: () => void }> = [];
53
+
54
+ if (onError) {
55
+ subscriptions.push(
56
+ NativeShortKitModule.onError((event) => {
57
+ onError({ code: event.code, message: event.message });
58
+ }),
59
+ );
60
+ }
61
+
62
+ if (onShareTapped) {
63
+ subscriptions.push(
64
+ NativeShortKitModule.onShareTapped((event) => {
65
+ const item = deserializeContentItem(event.item);
66
+ if (item) onShareTapped(item);
67
+ }),
68
+ );
69
+ }
70
+
71
+ if (onSurveyResponse) {
72
+ subscriptions.push(
73
+ NativeShortKitModule.onSurveyResponse((event) => {
74
+ onSurveyResponse(event.surveyId, {
75
+ id: event.optionId,
76
+ text: event.optionText,
77
+ });
78
+ }),
79
+ );
80
+ }
81
+
82
+ if (onLoop) {
83
+ subscriptions.push(
84
+ NativeShortKitModule.onDidLoop((event) => {
85
+ onLoop({ contentId: event.contentId, loopCount: event.loopCount });
86
+ }),
87
+ );
88
+ }
89
+
90
+ if (onFeedTransition) {
91
+ subscriptions.push(
92
+ NativeShortKitModule.onFeedTransition((event) => {
93
+ onFeedTransition({
94
+ phase: event.phase as 'began' | 'ended',
95
+ from: deserializeContentItem(event.fromItem ?? null),
96
+ to: deserializeContentItem(event.toItem ?? null),
97
+ direction: event.direction as 'forward' | 'backward',
98
+ });
99
+ }),
100
+ );
101
+ }
102
+
103
+ if (onFormatChange) {
104
+ subscriptions.push(
105
+ NativeShortKitModule.onFormatChange((event) => {
106
+ onFormatChange({
107
+ contentId: event.contentId,
108
+ fromBitrate: event.fromBitrate,
109
+ toBitrate: event.toBitrate,
110
+ fromResolution: event.fromResolution,
111
+ toResolution: event.toResolution,
112
+ });
113
+ }),
114
+ );
115
+ }
116
+
117
+ if (onArticleTapped) {
118
+ subscriptions.push(
119
+ NativeShortKitModule.onArticleTapped((event) => {
120
+ const item = deserializeContentItem(event.item);
121
+ if (item) onArticleTapped(item);
122
+ }),
123
+ );
124
+ }
125
+
126
+ if (onCommentTapped) {
127
+ subscriptions.push(
128
+ NativeShortKitModule.onCommentTapped((event) => {
129
+ const item = deserializeContentItem(event.item);
130
+ if (item) onCommentTapped(item);
131
+ }),
132
+ );
133
+ }
134
+
135
+ if (onOverlayShareTapped) {
136
+ subscriptions.push(
137
+ NativeShortKitModule.onOverlayShareTapped((event) => {
138
+ const item = deserializeContentItem(event.item);
139
+ if (item) onOverlayShareTapped(item);
140
+ }),
141
+ );
142
+ }
143
+
144
+ if (onSaveTapped) {
145
+ subscriptions.push(
146
+ NativeShortKitModule.onSaveTapped((event) => {
147
+ const item = deserializeContentItem(event.item);
148
+ if (item) onSaveTapped(item);
149
+ }),
150
+ );
151
+ }
152
+
153
+ if (onLikeTapped) {
154
+ subscriptions.push(
155
+ NativeShortKitModule.onLikeTapped((event) => {
156
+ const item = deserializeContentItem(event.item);
157
+ if (item) onLikeTapped(item);
158
+ }),
159
+ );
160
+ }
161
+
162
+ return () => {
163
+ for (const sub of subscriptions) {
164
+ sub.remove();
165
+ }
166
+ };
167
+ }, [
168
+ onError,
169
+ onShareTapped,
170
+ onSurveyResponse,
171
+ onLoop,
172
+ onFeedTransition,
173
+ onFormatChange,
174
+ onArticleTapped,
175
+ onCommentTapped,
176
+ onOverlayShareTapped,
177
+ onSaveTapped,
178
+ onLikeTapped,
179
+ ]);
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Render
183
+ // ---------------------------------------------------------------------------
184
+ return (
185
+ <View style={[feedStyles.container, style]}>
186
+ <ShortKitFeedView
187
+ style={feedStyles.feed}
188
+ config="{}"
189
+ />
190
+ <OverlayManager overlay={overlayConfig} />
191
+ </View>
192
+ );
193
+ }
194
+
195
+ const feedStyles = StyleSheet.create({
196
+ container: {
197
+ flex: 1,
198
+ overflow: 'hidden',
199
+ },
200
+ feed: {
201
+ flex: 1,
202
+ },
203
+ });