@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,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 = "ios/*.{h,m,mm,cpp,swift}"
14
+ s.requires_arc = true
15
+
16
+ s.dependency "ShortKit"
17
+
18
+ install_modules_dependencies(s)
19
+ end
@@ -0,0 +1,34 @@
1
+ plugins {
2
+ id("com.android.library")
3
+ id("org.jetbrains.kotlin.android")
4
+ id("com.facebook.react")
5
+ }
6
+
7
+ android {
8
+ namespace = "com.shortkit.reactnative"
9
+ compileSdk = 35
10
+
11
+ defaultConfig {
12
+ minSdk = 24
13
+ }
14
+
15
+ compileOptions {
16
+ sourceCompatibility = JavaVersion.VERSION_17
17
+ targetCompatibility = JavaVersion.VERSION_17
18
+ }
19
+
20
+ kotlinOptions {
21
+ jvmTarget = "17"
22
+ }
23
+
24
+ sourceSets {
25
+ named("main") {
26
+ java.srcDirs("src/main/java")
27
+ }
28
+ }
29
+ }
30
+
31
+ dependencies {
32
+ implementation("com.facebook.react:react-android")
33
+ implementation("dev.shortkit:shortkit:0.1.0")
34
+ }
@@ -0,0 +1,249 @@
1
+ package com.shortkit.reactnative
2
+
3
+ import android.content.Context
4
+ import android.content.ContextWrapper
5
+ import android.util.Log
6
+ import android.view.View
7
+ import android.view.ViewGroup
8
+ import android.widget.FrameLayout
9
+ import androidx.fragment.app.FragmentActivity
10
+ import androidx.viewpager2.widget.ViewPager2
11
+ import com.facebook.react.uimanager.util.ReactFindViewUtil
12
+ import com.shortkit.ShortKitFeedFragment
13
+
14
+ /**
15
+ * Fabric native view that embeds [ShortKitFeedFragment] using Android
16
+ * Fragment transactions. Props are set by [ShortKitFeedViewManager].
17
+ *
18
+ * Also tracks the feed's scroll offset via [ViewPager2.OnPageChangeCallback]
19
+ * and applies native translation transforms to the sibling RN overlay views
20
+ * so they move with the active cell during swipe transitions.
21
+ *
22
+ * Android equivalent of iOS `ShortKitFeedView.swift`.
23
+ */
24
+ class ShortKitFeedView(context: Context) : FrameLayout(context) {
25
+
26
+ // -----------------------------------------------------------------------
27
+ // Props (set by ViewManager)
28
+ // -----------------------------------------------------------------------
29
+
30
+ var config: String? = null
31
+ var overlayType: String? = null
32
+
33
+ // -----------------------------------------------------------------------
34
+ // Fragment management
35
+ // -----------------------------------------------------------------------
36
+
37
+ private var feedFragment: ShortKitFeedFragment? = null
38
+ private var fragmentContainerId: Int = View.generateViewId()
39
+
40
+ init {
41
+ id = fragmentContainerId
42
+ }
43
+
44
+ // -----------------------------------------------------------------------
45
+ // Scroll tracking
46
+ // -----------------------------------------------------------------------
47
+
48
+ private var viewPager: ViewPager2? = null
49
+ private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
50
+
51
+ /** Overlay for the currently active cell (nativeID="overlay-current"). */
52
+ private var currentOverlayView: View? = null
53
+
54
+ /** Overlay for the upcoming cell (nativeID="overlay-next"). */
55
+ private var nextOverlayView: View? = null
56
+
57
+ /** The page index from which the current scroll gesture started. */
58
+ private var currentPage: Int = 0
59
+
60
+ // -----------------------------------------------------------------------
61
+ // Lifecycle
62
+ // -----------------------------------------------------------------------
63
+
64
+ override fun onAttachedToWindow() {
65
+ super.onAttachedToWindow()
66
+ embedFeedFragmentIfNeeded()
67
+ }
68
+
69
+ override fun onDetachedFromWindow() {
70
+ teardownScrollTracking()
71
+ removeFeedFragment()
72
+ super.onDetachedFromWindow()
73
+ }
74
+
75
+ // -----------------------------------------------------------------------
76
+ // Fragment containment
77
+ // -----------------------------------------------------------------------
78
+
79
+ private fun embedFeedFragmentIfNeeded() {
80
+ if (feedFragment != null) return
81
+
82
+ val sdk = ShortKitModule.shared?.sdk ?: run {
83
+ Log.w(TAG, "ShortKit SDK not initialized. Call ShortKitModule.initialize() first.")
84
+ return
85
+ }
86
+
87
+ val activity = getReactActivity() ?: run {
88
+ Log.w(TAG, "Could not find hosting Activity.")
89
+ return
90
+ }
91
+
92
+ val fragment = ShortKitFeedFragment.newInstance(sdk)
93
+
94
+ try {
95
+ activity.supportFragmentManager
96
+ .beginTransaction()
97
+ .replace(fragmentContainerId, fragment)
98
+ .commitNowAllowingStateLoss()
99
+ this.feedFragment = fragment // Only assign after successful commit
100
+ setupScrollTracking(fragment)
101
+ } catch (e: Exception) {
102
+ Log.e(TAG, "Failed to embed feed fragment", e)
103
+ }
104
+ }
105
+
106
+ private fun removeFeedFragment() {
107
+ val fragment = feedFragment ?: return
108
+ feedFragment = null // Clear immediately to prevent leaks on early return
109
+
110
+ try {
111
+ val activity = getReactActivity() ?: return
112
+ activity.supportFragmentManager
113
+ .beginTransaction()
114
+ .remove(fragment)
115
+ .commitNowAllowingStateLoss()
116
+ } catch (e: IllegalStateException) {
117
+ // Expected during Activity teardown — fragment already being destroyed
118
+ } catch (e: Exception) {
119
+ Log.e(TAG, "Unexpected error removing feed fragment", e)
120
+ }
121
+ }
122
+
123
+ // -----------------------------------------------------------------------
124
+ // Scroll tracking
125
+ // -----------------------------------------------------------------------
126
+
127
+ private fun setupScrollTracking(fragment: ShortKitFeedFragment) {
128
+ val pager = findViewPager2(fragment.view ?: return) ?: run {
129
+ Log.w(TAG, "Could not find ViewPager2 in feed fragment hierarchy.")
130
+ return
131
+ }
132
+ viewPager = pager
133
+
134
+ val callback = object : ViewPager2.OnPageChangeCallback() {
135
+ override fun onPageScrolled(
136
+ position: Int,
137
+ positionOffset: Float,
138
+ positionOffsetPixels: Int
139
+ ) {
140
+ handleScrollOffset(position, positionOffset, positionOffsetPixels)
141
+ }
142
+ }
143
+ pageChangeCallback = callback
144
+ pager.registerOnPageChangeCallback(callback)
145
+ }
146
+
147
+ private fun teardownScrollTracking() {
148
+ pageChangeCallback?.let { viewPager?.unregisterOnPageChangeCallback(it) }
149
+ pageChangeCallback = null
150
+ viewPager = null
151
+ currentOverlayView?.translationY = 0f
152
+ nextOverlayView?.translationY = 0f
153
+ currentOverlayView = null
154
+ nextOverlayView = null
155
+ }
156
+
157
+ private fun handleScrollOffset(
158
+ position: Int,
159
+ positionOffset: Float,
160
+ positionOffsetPixels: Int
161
+ ) {
162
+ val cellHeight = height.toFloat()
163
+ if (cellHeight <= 0) return
164
+
165
+ // positionOffset is 0.0 when settled, approaches 1.0 during forward scroll.
166
+ // position is the index of the page currently filling most of the screen.
167
+ val delta: Float = if (position >= currentPage) {
168
+ // Forward scroll (or idle)
169
+ positionOffsetPixels.toFloat()
170
+ } else {
171
+ // Backward scroll: position dropped below currentPage
172
+ -(cellHeight - positionOffsetPixels.toFloat())
173
+ }
174
+
175
+ // Update currentPage when the scroll settles
176
+ if (positionOffset == 0f) {
177
+ currentPage = position
178
+ }
179
+
180
+ // Find the overlay views if not cached
181
+ if (currentOverlayView == null || nextOverlayView == null) {
182
+ findOverlayViews()
183
+ }
184
+
185
+ // Current overlay follows the active cell
186
+ currentOverlayView?.translationY = -delta
187
+
188
+ // Next overlay: positioned one page ahead in the scroll direction
189
+ if (delta >= 0) {
190
+ // Forward scroll (or idle): next cell is below
191
+ nextOverlayView?.translationY = cellHeight - delta
192
+ } else {
193
+ // Backward scroll: next cell is above
194
+ nextOverlayView?.translationY = -cellHeight - delta
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Find the sibling RN overlay views by nativeID. In the Fabric hierarchy,
200
+ * the ShortKitFeedView and the overlay containers are children of the
201
+ * same parent (wrapped by ShortKitFeed.tsx with overflow: hidden).
202
+ */
203
+ private fun findOverlayViews() {
204
+ val parent = parent as? ViewGroup ?: return
205
+
206
+ for (i in 0 until parent.childCount) {
207
+ val sibling = parent.getChildAt(i)
208
+ if (sibling === this) continue
209
+ if (currentOverlayView == null) {
210
+ currentOverlayView = ReactFindViewUtil.findView(sibling, "overlay-current")
211
+ }
212
+ if (nextOverlayView == null) {
213
+ nextOverlayView = ReactFindViewUtil.findView(sibling, "overlay-next")
214
+ }
215
+ }
216
+ }
217
+
218
+ /** Recursively find the first [ViewPager2] in a view hierarchy. */
219
+ private fun findViewPager2(view: View): ViewPager2? {
220
+ if (view is ViewPager2) return view
221
+ if (view is ViewGroup) {
222
+ for (i in 0 until view.childCount) {
223
+ findViewPager2(view.getChildAt(i))?.let { return it }
224
+ }
225
+ }
226
+ return null
227
+ }
228
+
229
+ // -----------------------------------------------------------------------
230
+ // Helpers
231
+ // -----------------------------------------------------------------------
232
+
233
+ /**
234
+ * Walk the context wrapper chain to find the hosting [FragmentActivity].
235
+ * React Native's [ThemedReactContext] wraps the Activity, so we unwrap it.
236
+ */
237
+ private fun getReactActivity(): FragmentActivity? {
238
+ var ctx = context
239
+ while (ctx is ContextWrapper) {
240
+ if (ctx is FragmentActivity) return ctx
241
+ ctx = ctx.baseContext
242
+ }
243
+ return null
244
+ }
245
+
246
+ companion object {
247
+ private const val TAG = "ShortKitFeedView"
248
+ }
249
+ }
@@ -0,0 +1,32 @@
1
+ package com.shortkit.reactnative
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+ import com.facebook.react.module.annotations.ReactModule
5
+ import com.facebook.react.uimanager.SimpleViewManager
6
+ import com.facebook.react.uimanager.ThemedReactContext
7
+ import com.facebook.react.uimanager.annotations.ReactProp
8
+
9
+ @ReactModule(name = ShortKitFeedViewManager.REACT_CLASS)
10
+ class ShortKitFeedViewManager : SimpleViewManager<ShortKitFeedView>() {
11
+
12
+ override fun getName(): String = REACT_CLASS
13
+
14
+ override fun createViewInstance(context: ThemedReactContext): ShortKitFeedView {
15
+ return ShortKitFeedView(context)
16
+ }
17
+
18
+ @ReactProp(name = "config")
19
+ fun setConfig(view: ShortKitFeedView, config: String?) {
20
+ view.config = config
21
+ }
22
+
23
+ @ReactProp(name = "overlayType", defaultString = "none")
24
+ fun setOverlayType(view: ShortKitFeedView, overlayType: String?) {
25
+ view.overlayType = overlayType
26
+ }
27
+
28
+
29
+ companion object {
30
+ const val REACT_CLASS = "ShortKitFeedView"
31
+ }
32
+ }