@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.
- package/ShortKitReactNative.podspec +19 -0
- package/android/build.gradle.kts +34 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +249 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +32 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +769 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +101 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPackage.kt +40 -0
- package/app.plugin.js +1 -0
- package/ios/ShortKitBridge.swift +537 -0
- package/ios/ShortKitFeedView.swift +207 -0
- package/ios/ShortKitFeedViewManager.mm +29 -0
- package/ios/ShortKitModule.h +25 -0
- package/ios/ShortKitModule.mm +204 -0
- package/ios/ShortKitOverlayBridge.swift +91 -0
- package/ios/ShortKitReactNative-Bridging-Header.h +3 -0
- package/ios/ShortKitReactNative.podspec +19 -0
- package/package.json +50 -0
- package/plugin/build/index.d.ts +3 -0
- package/plugin/build/index.js +13 -0
- package/plugin/build/withShortKitAndroid.d.ts +8 -0
- package/plugin/build/withShortKitAndroid.js +32 -0
- package/plugin/build/withShortKitIOS.d.ts +8 -0
- package/plugin/build/withShortKitIOS.js +29 -0
- package/react-native.config.js +8 -0
- package/src/OverlayManager.tsx +87 -0
- package/src/ShortKitContext.ts +51 -0
- package/src/ShortKitFeed.tsx +203 -0
- package/src/ShortKitProvider.tsx +526 -0
- package/src/index.ts +26 -0
- package/src/serialization.ts +95 -0
- package/src/specs/NativeShortKitModule.ts +201 -0
- package/src/specs/ShortKitFeedViewNativeComponent.ts +13 -0
- package/src/types.ts +167 -0
- package/src/useShortKit.ts +20 -0
- package/src/useShortKitPlayer.ts +29 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import ShortKit
|
|
3
|
+
|
|
4
|
+
/// Fabric native view that embeds `ShortKitFeedViewController` using
|
|
5
|
+
/// UIViewController containment. Props are set by the view manager via
|
|
6
|
+
/// `@objc` setters.
|
|
7
|
+
///
|
|
8
|
+
/// Also tracks the feed's scroll offset via KVO and applies a native
|
|
9
|
+
/// transform to the sibling RN overlay view so it moves with the active
|
|
10
|
+
/// cell during swipe transitions.
|
|
11
|
+
@objc public class ShortKitFeedView: UIView {
|
|
12
|
+
|
|
13
|
+
// MARK: - Props (set by RCTViewManager)
|
|
14
|
+
|
|
15
|
+
@objc public var config: String? {
|
|
16
|
+
didSet { /* reserved for future use */ }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@objc public var overlayType: String? {
|
|
20
|
+
didSet { /* overlay mode is determined at ShortKit init time */ }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// MARK: - Child VC
|
|
24
|
+
|
|
25
|
+
private var feedViewController: ShortKitFeedViewController?
|
|
26
|
+
|
|
27
|
+
// MARK: - Scroll Tracking
|
|
28
|
+
|
|
29
|
+
private var scrollObservation: NSKeyValueObservation?
|
|
30
|
+
/// Overlay for the currently active cell (nativeID="overlay-current").
|
|
31
|
+
private weak var currentOverlayView: UIView?
|
|
32
|
+
/// Overlay for the upcoming cell (nativeID="overlay-next").
|
|
33
|
+
private weak var nextOverlayView: UIView?
|
|
34
|
+
/// The page index from which the current scroll gesture started.
|
|
35
|
+
private var currentPage: Int = 0
|
|
36
|
+
|
|
37
|
+
// MARK: - Lifecycle
|
|
38
|
+
|
|
39
|
+
public override func didMoveToWindow() {
|
|
40
|
+
super.didMoveToWindow()
|
|
41
|
+
|
|
42
|
+
if window != nil {
|
|
43
|
+
embedFeedViewControllerIfNeeded()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public override func willMove(toWindow newWindow: UIWindow?) {
|
|
48
|
+
super.willMove(toWindow: newWindow)
|
|
49
|
+
|
|
50
|
+
if newWindow == nil {
|
|
51
|
+
removeFeedViewController()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public override func layoutSubviews() {
|
|
56
|
+
super.layoutSubviews()
|
|
57
|
+
feedViewController?.view.frame = bounds
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// MARK: - VC Containment
|
|
61
|
+
|
|
62
|
+
private func embedFeedViewControllerIfNeeded() {
|
|
63
|
+
// Already embedded
|
|
64
|
+
guard feedViewController == nil else { return }
|
|
65
|
+
|
|
66
|
+
guard let sdk = ShortKitBridge.shared?.sdk else {
|
|
67
|
+
NSLog("[ShortKitFeedView] ShortKit SDK not initialized. Call ShortKitModule.initialize() first.")
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
guard let parentVC = findParentViewController() else {
|
|
72
|
+
NSLog("[ShortKitFeedView] Could not find parent UIViewController.")
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let feedVC = ShortKitFeedViewController(shortKit: sdk)
|
|
77
|
+
self.feedViewController = feedVC
|
|
78
|
+
|
|
79
|
+
parentVC.addChild(feedVC)
|
|
80
|
+
feedVC.view.frame = bounds
|
|
81
|
+
feedVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
82
|
+
addSubview(feedVC.view)
|
|
83
|
+
feedVC.didMove(toParent: parentVC)
|
|
84
|
+
|
|
85
|
+
setupScrollTracking(feedVC)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private func removeFeedViewController() {
|
|
89
|
+
scrollObservation?.invalidate()
|
|
90
|
+
scrollObservation = nil
|
|
91
|
+
currentOverlayView?.transform = .identity
|
|
92
|
+
nextOverlayView?.transform = .identity
|
|
93
|
+
currentOverlayView = nil
|
|
94
|
+
nextOverlayView = nil
|
|
95
|
+
|
|
96
|
+
guard let feedVC = feedViewController else { return }
|
|
97
|
+
|
|
98
|
+
feedVC.willMove(toParent: nil)
|
|
99
|
+
feedVC.view.removeFromSuperview()
|
|
100
|
+
feedVC.removeFromParent()
|
|
101
|
+
|
|
102
|
+
self.feedViewController = nil
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// MARK: - Scroll Tracking
|
|
106
|
+
|
|
107
|
+
private func setupScrollTracking(_ feedVC: ShortKitFeedViewController) {
|
|
108
|
+
guard let scrollView = findScrollView(in: feedVC.view) else {
|
|
109
|
+
NSLog("[ShortKitFeedView] Could not find scroll view in feed VC hierarchy.")
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
scrollObservation = scrollView.observe(\.contentOffset, options: [.new]) { [weak self] sv, _ in
|
|
114
|
+
self?.handleScrollOffset(sv)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private func handleScrollOffset(_ scrollView: UIScrollView) {
|
|
119
|
+
let cellHeight = scrollView.bounds.height
|
|
120
|
+
guard cellHeight > 0 else { return }
|
|
121
|
+
|
|
122
|
+
let offset = scrollView.contentOffset.y
|
|
123
|
+
let delta = offset - CGFloat(currentPage) * cellHeight
|
|
124
|
+
|
|
125
|
+
// Update currentPage when the scroll settles near a page boundary
|
|
126
|
+
let nearestPage = Int(round(offset / cellHeight))
|
|
127
|
+
if abs(offset - CGFloat(nearestPage) * cellHeight) < 1.0 {
|
|
128
|
+
currentPage = nearestPage
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Find the overlay views if not cached
|
|
132
|
+
if currentOverlayView == nil || nextOverlayView == nil {
|
|
133
|
+
findOverlayViews()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Current overlay follows the active cell
|
|
137
|
+
currentOverlayView?.transform = CGAffineTransform(translationX: 0, y: -delta)
|
|
138
|
+
|
|
139
|
+
// Next overlay: positioned one page ahead in the scroll direction
|
|
140
|
+
if delta >= 0 {
|
|
141
|
+
// Forward scroll (or idle): next cell is below
|
|
142
|
+
nextOverlayView?.transform = CGAffineTransform(translationX: 0, y: cellHeight - delta)
|
|
143
|
+
} else {
|
|
144
|
+
// Backward scroll: next cell is above
|
|
145
|
+
nextOverlayView?.transform = CGAffineTransform(translationX: 0, y: -cellHeight - delta)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Find the sibling RN overlay views by nativeID (mapped to
|
|
150
|
+
/// accessibilityIdentifier on iOS). In the Fabric hierarchy, the
|
|
151
|
+
/// ShortKitFeedView and the overlay containers are children of the
|
|
152
|
+
/// same parent (wrapped by ShortKitFeed.tsx with overflow: hidden).
|
|
153
|
+
private func findOverlayViews() {
|
|
154
|
+
guard let parent = superview else { return }
|
|
155
|
+
|
|
156
|
+
// The nativeID views may be nested inside intermediate Fabric
|
|
157
|
+
// wrapper views, so we search recursively within siblings.
|
|
158
|
+
for sibling in parent.subviews where sibling !== self {
|
|
159
|
+
if let found = findView(withNativeID: "overlay-current", in: sibling) {
|
|
160
|
+
currentOverlayView = found
|
|
161
|
+
}
|
|
162
|
+
if let found = findView(withNativeID: "overlay-next", in: sibling) {
|
|
163
|
+
nextOverlayView = found
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Recursively find a view by its accessibilityIdentifier (nativeID).
|
|
169
|
+
private func findView(withNativeID nativeID: String, in view: UIView) -> UIView? {
|
|
170
|
+
if view.accessibilityIdentifier == nativeID {
|
|
171
|
+
return view
|
|
172
|
+
}
|
|
173
|
+
for subview in view.subviews {
|
|
174
|
+
if let found = findView(withNativeID: nativeID, in: subview) {
|
|
175
|
+
return found
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return nil
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Recursively find the first UIScrollView in a view hierarchy.
|
|
182
|
+
private func findScrollView(in view: UIView) -> UIScrollView? {
|
|
183
|
+
if let sv = view as? UIScrollView {
|
|
184
|
+
return sv
|
|
185
|
+
}
|
|
186
|
+
for subview in view.subviews {
|
|
187
|
+
if let sv = findScrollView(in: subview) {
|
|
188
|
+
return sv
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return nil
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// MARK: - Helpers
|
|
195
|
+
|
|
196
|
+
/// Walk the responder chain to find the nearest UIViewController.
|
|
197
|
+
private func findParentViewController() -> UIViewController? {
|
|
198
|
+
var responder: UIResponder? = self
|
|
199
|
+
while let next = responder?.next {
|
|
200
|
+
if let vc = next as? UIViewController {
|
|
201
|
+
return vc
|
|
202
|
+
}
|
|
203
|
+
responder = next
|
|
204
|
+
}
|
|
205
|
+
return nil
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#import <React/RCTViewManager.h>
|
|
2
|
+
|
|
3
|
+
// Import the Swift-generated header for ShortKitFeedView
|
|
4
|
+
#if __has_include(<ShortKitReactNative/ShortKitReactNative-Swift.h>)
|
|
5
|
+
#import <ShortKitReactNative/ShortKitReactNative-Swift.h>
|
|
6
|
+
#else
|
|
7
|
+
#import "ShortKitReactNative-Swift.h"
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
@interface ShortKitFeedViewManager : RCTViewManager
|
|
11
|
+
@end
|
|
12
|
+
|
|
13
|
+
@implementation ShortKitFeedViewManager
|
|
14
|
+
|
|
15
|
+
RCT_EXPORT_MODULE(ShortKitFeedView)
|
|
16
|
+
|
|
17
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
18
|
+
return YES;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
- (UIView *)view {
|
|
22
|
+
return [[ShortKitFeedView alloc] init];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
RCT_EXPORT_VIEW_PROPERTY(config, NSString)
|
|
26
|
+
RCT_EXPORT_VIEW_PROPERTY(overlayType, NSString)
|
|
27
|
+
RCT_EXPORT_VIEW_PROPERTY(templateName, NSString)
|
|
28
|
+
|
|
29
|
+
@end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#import <React/RCTEventEmitter.h>
|
|
2
|
+
|
|
3
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
4
|
+
#import <ShortKitSpecs/ShortKitSpecs.h>
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
@protocol ShortKitBridgeDelegateProtocol;
|
|
8
|
+
|
|
9
|
+
/// TurboModule bridge for the ShortKit iOS SDK.
|
|
10
|
+
/// Receives commands from JS, emits events back via codegen EventEmitter.
|
|
11
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
12
|
+
@interface ShortKitModule : RCTEventEmitter <NativeShortKitModuleSpec, ShortKitBridgeDelegateProtocol> {
|
|
13
|
+
@protected
|
|
14
|
+
facebook::react::EventEmitterCallback _eventEmitterCallback;
|
|
15
|
+
}
|
|
16
|
+
#else
|
|
17
|
+
@interface ShortKitModule : RCTEventEmitter <RCTBridgeModule, ShortKitBridgeDelegateProtocol>
|
|
18
|
+
#endif
|
|
19
|
+
|
|
20
|
+
@end
|
|
21
|
+
|
|
22
|
+
/// Protocol for ShortKitBridge (Swift) to emit events back to the module.
|
|
23
|
+
@protocol ShortKitBridgeDelegateProtocol <NSObject>
|
|
24
|
+
- (void)emitEvent:(NSString * _Nonnull)name body:(NSDictionary * _Nonnull)body;
|
|
25
|
+
@end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#import "ShortKitModule.h"
|
|
2
|
+
#import <React/RCTLog.h>
|
|
3
|
+
|
|
4
|
+
// Import the Swift-generated header for ShortKitBridge
|
|
5
|
+
#if __has_include(<ShortKitReactNative/ShortKitReactNative-Swift.h>)
|
|
6
|
+
#import <ShortKitReactNative/ShortKitReactNative-Swift.h>
|
|
7
|
+
#else
|
|
8
|
+
#import "ShortKitReactNative-Swift.h"
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
@implementation ShortKitModule {
|
|
12
|
+
ShortKitBridge *_shortKitBridge;
|
|
13
|
+
BOOL _hasListeners;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
RCT_EXPORT_MODULE(ShortKitModule)
|
|
17
|
+
|
|
18
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
19
|
+
return YES;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// All TurboModule methods run on the main queue.
|
|
23
|
+
/// This ensures thread safety for UIKit-backed ShortKit operations
|
|
24
|
+
/// and eliminates races on _hasListeners / _shortKitBridge.
|
|
25
|
+
- (dispatch_queue_t)methodQueue {
|
|
26
|
+
return dispatch_get_main_queue();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
- (instancetype)init {
|
|
30
|
+
self = [super init];
|
|
31
|
+
return self;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Called when the RN bridge tears down (hot reload, app shutdown).
|
|
35
|
+
- (void)invalidate {
|
|
36
|
+
[_shortKitBridge teardown];
|
|
37
|
+
_shortKitBridge = nil;
|
|
38
|
+
[super invalidate];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// MARK: - RCTEventEmitter
|
|
42
|
+
|
|
43
|
+
- (NSArray<NSString *> *)supportedEvents {
|
|
44
|
+
return @[
|
|
45
|
+
@"onPlayerStateChanged",
|
|
46
|
+
@"onCurrentItemChanged",
|
|
47
|
+
@"onTimeUpdate",
|
|
48
|
+
@"onMutedChanged",
|
|
49
|
+
@"onPlaybackRateChanged",
|
|
50
|
+
@"onCaptionsEnabledChanged",
|
|
51
|
+
@"onActiveCaptionTrackChanged",
|
|
52
|
+
@"onActiveCueChanged",
|
|
53
|
+
@"onDidLoop",
|
|
54
|
+
@"onFeedTransition",
|
|
55
|
+
@"onFormatChange",
|
|
56
|
+
@"onPrefetchedAheadCountChanged",
|
|
57
|
+
@"onError",
|
|
58
|
+
@"onShareTapped",
|
|
59
|
+
@"onSurveyResponse",
|
|
60
|
+
@"onArticleTapped",
|
|
61
|
+
@"onCommentTapped",
|
|
62
|
+
@"onOverlayShareTapped",
|
|
63
|
+
@"onSaveTapped",
|
|
64
|
+
@"onLikeTapped",
|
|
65
|
+
@"onOverlayConfigure",
|
|
66
|
+
@"onOverlayActivate",
|
|
67
|
+
@"onOverlayReset",
|
|
68
|
+
@"onOverlayFadeOut",
|
|
69
|
+
@"onOverlayRestore",
|
|
70
|
+
@"onOverlayTap",
|
|
71
|
+
@"onOverlayDoubleTap",
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
- (void)startObserving {
|
|
76
|
+
_hasListeners = YES;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
- (void)stopObserving {
|
|
80
|
+
_hasListeners = NO;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// MARK: - Event Emitter Callback (New Architecture)
|
|
84
|
+
|
|
85
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
86
|
+
- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper {
|
|
87
|
+
_eventEmitterCallback = std::move(eventEmitterCallbackWrapper->_eventEmitterCallback);
|
|
88
|
+
}
|
|
89
|
+
#endif
|
|
90
|
+
|
|
91
|
+
// MARK: - ShortKitBridgeDelegateProtocol
|
|
92
|
+
|
|
93
|
+
- (void)emitEvent:(NSString *)name body:(NSDictionary *)body {
|
|
94
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
95
|
+
// New Architecture: use the codegen EventEmitterCallback directly
|
|
96
|
+
if (_eventEmitterCallback) {
|
|
97
|
+
_eventEmitterCallback(std::string([name UTF8String]), body);
|
|
98
|
+
}
|
|
99
|
+
#else
|
|
100
|
+
if (_hasListeners) {
|
|
101
|
+
[self sendEventWithName:name body:body];
|
|
102
|
+
}
|
|
103
|
+
#endif
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// MARK: - Lifecycle
|
|
107
|
+
|
|
108
|
+
RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
|
|
109
|
+
config:(NSString *)config
|
|
110
|
+
clientAppName:(NSString *)clientAppName
|
|
111
|
+
clientAppVersion:(NSString *)clientAppVersion
|
|
112
|
+
customDimensions:(NSString *)customDimensions) {
|
|
113
|
+
// Tear down any existing instance to prevent leaks on re-initialize
|
|
114
|
+
[_shortKitBridge teardown];
|
|
115
|
+
|
|
116
|
+
_shortKitBridge = [[ShortKitBridge alloc] initWithApiKey:apiKey
|
|
117
|
+
config:config
|
|
118
|
+
clientAppName:clientAppName
|
|
119
|
+
clientAppVersion:clientAppVersion
|
|
120
|
+
customDimensions:customDimensions
|
|
121
|
+
delegate:self];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
RCT_EXPORT_METHOD(destroy) {
|
|
125
|
+
[_shortKitBridge teardown];
|
|
126
|
+
_shortKitBridge = nil;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
RCT_EXPORT_METHOD(setUserId:(NSString *)userId) {
|
|
130
|
+
[_shortKitBridge setUserId:userId];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
RCT_EXPORT_METHOD(clearUserId) {
|
|
134
|
+
[_shortKitBridge clearUserId];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
RCT_EXPORT_METHOD(onPause) {
|
|
138
|
+
[_shortKitBridge onPause];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
RCT_EXPORT_METHOD(onResume) {
|
|
142
|
+
[_shortKitBridge onResume];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// MARK: - Player Controls
|
|
146
|
+
|
|
147
|
+
RCT_EXPORT_METHOD(play) {
|
|
148
|
+
[_shortKitBridge play];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
RCT_EXPORT_METHOD(pause) {
|
|
152
|
+
[_shortKitBridge doPause];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
RCT_EXPORT_METHOD(seek:(double)seconds) {
|
|
156
|
+
[_shortKitBridge seekTo:seconds];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
RCT_EXPORT_METHOD(seekAndPlay:(double)seconds) {
|
|
160
|
+
[_shortKitBridge seekAndPlayTo:seconds];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
RCT_EXPORT_METHOD(skipToNext) {
|
|
164
|
+
[_shortKitBridge skipToNext];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
RCT_EXPORT_METHOD(skipToPrevious) {
|
|
168
|
+
[_shortKitBridge skipToPrevious];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
RCT_EXPORT_METHOD(setMuted:(BOOL)muted) {
|
|
172
|
+
[_shortKitBridge setMuted:muted];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
RCT_EXPORT_METHOD(setPlaybackRate:(double)rate) {
|
|
176
|
+
[_shortKitBridge setPlaybackRate:rate];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
RCT_EXPORT_METHOD(setCaptionsEnabled:(BOOL)enabled) {
|
|
180
|
+
[_shortKitBridge setCaptionsEnabled:enabled];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
RCT_EXPORT_METHOD(selectCaptionTrack:(NSString *)language) {
|
|
184
|
+
[_shortKitBridge selectCaptionTrack:language];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
RCT_EXPORT_METHOD(sendContentSignal:(NSString *)signal) {
|
|
188
|
+
[_shortKitBridge sendContentSignal:signal];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
RCT_EXPORT_METHOD(setMaxBitrate:(double)bitrate) {
|
|
192
|
+
[_shortKitBridge setMaxBitrate:bitrate];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// MARK: - New Architecture (TurboModule)
|
|
196
|
+
|
|
197
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
198
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
199
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params {
|
|
200
|
+
return std::make_shared<facebook::react::NativeShortKitModuleSpecJSI>(params);
|
|
201
|
+
}
|
|
202
|
+
#endif
|
|
203
|
+
|
|
204
|
+
@end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import ShortKit
|
|
3
|
+
|
|
4
|
+
/// A transparent UIView that conforms to `FeedOverlay` and bridges overlay
|
|
5
|
+
/// lifecycle calls to JS events via `ShortKitBridge`.
|
|
6
|
+
///
|
|
7
|
+
/// The actual overlay UI is rendered by React Native on the JS side through
|
|
8
|
+
/// the `OverlayManager` component. This view simply relays the SDK lifecycle
|
|
9
|
+
/// events so the JS overlay knows when to configure, activate, reset, etc.
|
|
10
|
+
@objc public class ShortKitOverlayBridge: UIView, @unchecked Sendable, FeedOverlay {
|
|
11
|
+
|
|
12
|
+
// MARK: - Bridge Reference
|
|
13
|
+
|
|
14
|
+
/// Weak reference to the bridge, set by the factory closure in `parseFeedConfig`.
|
|
15
|
+
weak var bridge: ShortKitBridge?
|
|
16
|
+
|
|
17
|
+
// MARK: - State
|
|
18
|
+
|
|
19
|
+
/// Stores the last configured ContentItem so we can pass it with lifecycle
|
|
20
|
+
/// events that don't receive the item as a parameter.
|
|
21
|
+
private var currentItem: ContentItem?
|
|
22
|
+
|
|
23
|
+
// MARK: - Init
|
|
24
|
+
|
|
25
|
+
override init(frame: CGRect) {
|
|
26
|
+
super.init(frame: frame)
|
|
27
|
+
backgroundColor = .clear
|
|
28
|
+
isUserInteractionEnabled = true
|
|
29
|
+
setupGestures()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
required init?(coder: NSCoder) {
|
|
33
|
+
fatalError("init(coder:) is not supported")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// MARK: - Gestures
|
|
37
|
+
|
|
38
|
+
private func setupGestures() {
|
|
39
|
+
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
|
|
40
|
+
doubleTap.numberOfTapsRequired = 2
|
|
41
|
+
addGestureRecognizer(doubleTap)
|
|
42
|
+
|
|
43
|
+
let singleTap = UITapGestureRecognizer(target: self, action: #selector(handleSingleTap))
|
|
44
|
+
singleTap.require(toFail: doubleTap)
|
|
45
|
+
addGestureRecognizer(singleTap)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@objc private func handleSingleTap() {
|
|
49
|
+
bridge?.emitOverlayEvent("onOverlayTap", body: [:])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@objc private func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
|
|
53
|
+
let location = gesture.location(in: self)
|
|
54
|
+
bridge?.emitOverlayEvent("onOverlayDoubleTap", body: [
|
|
55
|
+
"x": location.x,
|
|
56
|
+
"y": location.y,
|
|
57
|
+
])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// MARK: - FeedOverlay
|
|
61
|
+
|
|
62
|
+
public func attach(player: ShortKitPlayer) {
|
|
63
|
+
// No-op on the native side. The JS overlay subscribes to player
|
|
64
|
+
// state via the TurboModule's Combine publishers.
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public func configure(with item: ContentItem) {
|
|
68
|
+
currentItem = item
|
|
69
|
+
bridge?.emitOverlayEvent("onOverlayConfigure", item: item)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public func resetPlaybackProgress() {
|
|
73
|
+
guard let item = currentItem else { return }
|
|
74
|
+
bridge?.emitOverlayEvent("onOverlayReset", item: item)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public func activatePlayback() {
|
|
78
|
+
guard let item = currentItem else { return }
|
|
79
|
+
bridge?.emitOverlayEvent("onOverlayActivate", item: item)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public func fadeOutForTransition() {
|
|
83
|
+
guard let item = currentItem else { return }
|
|
84
|
+
bridge?.emitOverlayEvent("onOverlayFadeOut", item: item)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public func restoreFromTransition() {
|
|
88
|
+
guard let item = currentItem else { return }
|
|
89
|
+
bridge?.emitOverlayEvent("onOverlayRestore", item: item)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
3
|
+
|
|
4
|
+
Pod::Spec.new do |s|
|
|
5
|
+
s.name = "ShortKitReactNative"
|
|
6
|
+
s.version = package["version"]
|
|
7
|
+
s.summary = package["description"]
|
|
8
|
+
s.homepage = "https://github.com/shortkit/shortkit"
|
|
9
|
+
s.license = package["license"]
|
|
10
|
+
s.author = "ShortKit"
|
|
11
|
+
s.platforms = { :ios => "16.0" }
|
|
12
|
+
s.source = { :git => "https://github.com/shortkit/shortkit.git", :tag => "v#{s.version}" }
|
|
13
|
+
s.source_files = "*.{h,m,mm,cpp,swift}"
|
|
14
|
+
s.requires_arc = true
|
|
15
|
+
|
|
16
|
+
s.dependency "ShortKit"
|
|
17
|
+
|
|
18
|
+
install_modules_dependencies(s)
|
|
19
|
+
end
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shortkitsdk/react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ShortKit React Native SDK — short-form video feed",
|
|
5
|
+
"react-native": "src/index",
|
|
6
|
+
"source": "src/index",
|
|
7
|
+
"main": "src/index.ts",
|
|
8
|
+
"types": "src/index.ts",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"ios",
|
|
13
|
+
"android",
|
|
14
|
+
"ShortKitReactNative.podspec",
|
|
15
|
+
"app.plugin.js",
|
|
16
|
+
"plugin/build",
|
|
17
|
+
"react-native.config.js",
|
|
18
|
+
"!android/build",
|
|
19
|
+
"!ios/build",
|
|
20
|
+
"!**/__tests__",
|
|
21
|
+
"!**/__mocks__"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build:plugin": "tsc --project plugin/tsconfig.json",
|
|
25
|
+
"test": "jest",
|
|
26
|
+
"lint": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">= 18.3.1",
|
|
30
|
+
"react-native": ">= 0.76.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@expo/config-plugins": "^9.0.0",
|
|
34
|
+
"@types/react": "^19.0.0",
|
|
35
|
+
"react": "^19.0.0",
|
|
36
|
+
"react-native": "^0.76.0",
|
|
37
|
+
"typescript": "^5.8.0",
|
|
38
|
+
"jest": "^29.0.0",
|
|
39
|
+
"@testing-library/react-native": "^12.0.0",
|
|
40
|
+
"react-test-renderer": "^19.0.0"
|
|
41
|
+
},
|
|
42
|
+
"codegenConfig": {
|
|
43
|
+
"name": "ShortKitSpecs",
|
|
44
|
+
"type": "all",
|
|
45
|
+
"jsSrcsDir": "./src/specs",
|
|
46
|
+
"android": {
|
|
47
|
+
"javaPackageName": "com.shortkit.reactnative"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const config_plugins_1 = require("@expo/config-plugins");
|
|
4
|
+
const withShortKitIOS_1 = require("./withShortKitIOS");
|
|
5
|
+
const withShortKitAndroid_1 = require("./withShortKitAndroid");
|
|
6
|
+
// Read version from package.json for the plugin wrapper
|
|
7
|
+
const pkg = require('../../package.json');
|
|
8
|
+
const withShortKit = (config) => {
|
|
9
|
+
config = (0, withShortKitIOS_1.withShortKitIOS)(config);
|
|
10
|
+
config = (0, withShortKitAndroid_1.withShortKitAndroid)(config);
|
|
11
|
+
return config;
|
|
12
|
+
};
|
|
13
|
+
exports.default = (0, config_plugins_1.createRunOncePlugin)(withShortKit, pkg.name, pkg.version);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ConfigPlugin } from '@expo/config-plugins';
|
|
2
|
+
/**
|
|
3
|
+
* Android config plugin for ShortKit.
|
|
4
|
+
*
|
|
5
|
+
* - Ensures INTERNET permission is present (usually already added by Expo).
|
|
6
|
+
* - Sets minSdkVersion to 24 if lower.
|
|
7
|
+
*/
|
|
8
|
+
export declare const withShortKitAndroid: ConfigPlugin;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withShortKitAndroid = void 0;
|
|
4
|
+
const config_plugins_1 = require("@expo/config-plugins");
|
|
5
|
+
/**
|
|
6
|
+
* Android config plugin for ShortKit.
|
|
7
|
+
*
|
|
8
|
+
* - Ensures INTERNET permission is present (usually already added by Expo).
|
|
9
|
+
* - Sets minSdkVersion to 24 if lower.
|
|
10
|
+
*/
|
|
11
|
+
const withShortKitAndroid = (config) => {
|
|
12
|
+
// Ensure minSdkVersion >= 24
|
|
13
|
+
config = config_plugins_1.AndroidConfig.Version.withBuildScriptExtMinimumVersion(config, {
|
|
14
|
+
name: 'minSdkVersion',
|
|
15
|
+
minVersion: 24,
|
|
16
|
+
});
|
|
17
|
+
// Ensure INTERNET permission
|
|
18
|
+
return (0, config_plugins_1.withAndroidManifest)(config, (mod) => {
|
|
19
|
+
const manifest = mod.modResults.manifest;
|
|
20
|
+
if (!manifest['uses-permission']) {
|
|
21
|
+
manifest['uses-permission'] = [];
|
|
22
|
+
}
|
|
23
|
+
const hasInternet = manifest['uses-permission'].some((p) => p.$?.['android:name'] === 'android.permission.INTERNET');
|
|
24
|
+
if (!hasInternet) {
|
|
25
|
+
manifest['uses-permission'].push({
|
|
26
|
+
$: { 'android:name': 'android.permission.INTERNET' },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return mod;
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
exports.withShortKitAndroid = withShortKitAndroid;
|