@shortkitsdk/react-native 0.2.37 → 0.2.39

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 (23) hide show
  1. package/android/build.gradle.kts +3 -3
  2. package/android/libs/shortkit-release.aar +0 -0
  3. package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +27 -7
  4. package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +1 -0
  5. package/android/src/main/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHost.kt +32 -14
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +39 -1
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +1 -0
  8. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +1 -1
  9. package/android/src/test/java/com/shortkit/reactnative/ReactCarouselOverlayHostLifecycleTest.kt +155 -0
  10. package/android/src/test/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHostLifecycleTest.kt +200 -0
  11. package/android/src/test/java/com/shortkit/reactnative/ShortKitBridgeLongPressTest.kt +148 -0
  12. package/ios/ShortKitBridge.swift +2 -1
  13. package/ios/ShortKitSDK.xcframework/Info.plist +5 -5
  14. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +2 -2
  15. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +3 -3
  16. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  17. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +3 -3
  18. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Info.plist +2 -2
  19. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +3 -3
  20. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +3 -3
  21. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  22. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +5 -5
  23. package/package.json +1 -1
@@ -35,9 +35,9 @@ android {
35
35
 
36
36
  dependencies {
37
37
  implementation("com.facebook.react:react-android")
38
- // When Reco's settings.gradle has the composite build, Gradle substitutes
39
- // this with the live source from android_sdk/shortkit automatically.
40
- // implementation("dev.shortkit:shortkit:0.2.11")
38
+ // Composite build in sample/android/settings.gradle substitutes this with
39
+ // live android_sdk source automatically (dev.shortkit:shortkit :shortkit project).
40
+ // implementation("dev.shortkit:shortkit:0.2.37")
41
41
  implementation(files("libs/shortkit-release.aar"))
42
42
  // Transitive deps needed when using local AAR (Maven artifact bundles these automatically)
43
43
  implementation("androidx.media3:media3-exoplayer:1.5.1")
Binary file
@@ -15,6 +15,8 @@ import kotlinx.coroutines.CoroutineScope
15
15
  import kotlinx.coroutines.Dispatchers
16
16
  import kotlinx.coroutines.Job
17
17
  import kotlinx.coroutines.SupervisorJob
18
+ import kotlinx.coroutines.cancel
19
+ import kotlinx.coroutines.isActive
18
20
  import kotlinx.coroutines.launch
19
21
  import kotlinx.coroutines.withContext
20
22
  import kotlinx.serialization.encodeToString
@@ -52,10 +54,20 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
52
54
  private var surface: ReactSurface? = null
53
55
  private var pendingProps: Bundle? = null
54
56
  private var isInLayoutPass: Boolean = false
55
- private val ioScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
57
+ private var ioScope: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
56
58
  private var pendingWriteJob: Job? = null
57
59
  private var configureGeneration: Int = 0
58
60
 
61
+ /** Test-only accessor for ioScope. */
62
+ @androidx.annotation.VisibleForTesting
63
+ internal val ioScopeForTest: CoroutineScope get() = ioScope
64
+
65
+ /** Test-only triggers for window lifecycle (protected in View). */
66
+ @androidx.annotation.VisibleForTesting
67
+ internal fun callOnDetachedFromWindow() = onDetachedFromWindow()
68
+ @androidx.annotation.VisibleForTesting
69
+ internal fun callOnAttachedToWindow() = onAttachedToWindow()
70
+
59
71
  /** Unique identifier for this overlay instance, used for event routing. */
60
72
  val surfaceId: String = java.util.UUID.randomUUID().toString()
61
73
 
@@ -343,8 +355,9 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
343
355
  }
344
356
 
345
357
  override fun resetState() {
346
- val surfaceImpl = surface as? ReactSurfaceImpl ?: return
347
- surfaceImpl.updateInitProps(Bundle())
358
+ // Non-destructive: configure() will push new props on next bind.
359
+ // iOS parity: ReactCarouselOverlayHost.swift resetState() is a no-op.
360
+ // Mirrors ReactVideoCarouselOverlayHost.resetState() which is also a no-op.
348
361
  }
349
362
 
350
363
  override fun fadeOutForTransition() {
@@ -404,6 +417,9 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
404
417
 
405
418
  override fun onAttachedToWindow() {
406
419
  super.onAttachedToWindow()
420
+ if (!ioScope.isActive) {
421
+ ioScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
422
+ }
407
423
 
408
424
  val parentView = parent as? android.view.View
409
425
  if (parentView != null && parentView.width > 0 && parentView.height > 0 && (width == 0 || height == 0)) {
@@ -464,12 +480,16 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
464
480
  // Cleanup
465
481
  // ------------------------------------------------------------------
466
482
 
467
- override fun onDetachedFromWindow() {
483
+ public override fun onDetachedFromWindow() {
468
484
  super.onDetachedFromWindow()
485
+ layoutHandler.removeCallbacks(layoutRunnable)
469
486
  pendingWriteJob?.cancel()
470
487
  pendingWriteJob = null
471
- if (surface?.isRunning == true) {
472
- surface?.stop()
473
- }
488
+ ioScope.cancel()
489
+ // Non-destructive: surface survives cell recycle and view transitions.
490
+ // iOS parity: ReactCarouselOverlayHost.swift has no willMove/didMoveToWindow
491
+ // override — surface stops only in deinit. Mirrors the non-destructive
492
+ // contract enforced on ReactVideoCarouselOverlayHost.
493
+ // ioScope is recreated in onAttachedToWindow if needed.
474
494
  }
475
495
  }
@@ -300,6 +300,7 @@ class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
300
300
 
301
301
  override fun onDetachedFromWindow() {
302
302
  super.onDetachedFromWindow()
303
+ layoutHandler.removeCallbacks(layoutRunnable)
303
304
  // Cancel flow subscriptions to avoid emitting events for off-screen cells.
304
305
  // Do NOT stop the surface — keep JS alive so subscriptions persist across
305
306
  // RecyclerView detach/reattach cycles. This matches iOS behavior where the
@@ -133,7 +133,8 @@ class ReactVideoCarouselOverlayHost(context: Context) : FrameLayout(context), Vi
133
133
 
134
134
  // Player state
135
135
  private var player: ShortKitPlayer? = null
136
- private var isActive: Boolean = false
136
+ @androidx.annotation.VisibleForTesting
137
+ internal var isActive: Boolean = false
137
138
  private var cachedPlayerState: String = "idle"
138
139
  private var cachedIsMuted: Boolean = true
139
140
  private var cachedCurrentTime: Double = 0.0
@@ -155,6 +156,17 @@ class ReactVideoCarouselOverlayHost(context: Context) : FrameLayout(context), Vi
155
156
 
156
157
  private var flowScope: CoroutineScope? = null
157
158
 
159
+ /** Test-only read-write accessor for flowScope. Write path lets tests inject a scope
160
+ * without needing a full ShortKitPlayer mock. */
161
+ @androidx.annotation.VisibleForTesting
162
+ internal var flowScopeForTest: CoroutineScope?
163
+ get() = flowScope
164
+ set(value) { flowScope = value }
165
+
166
+ /** Test-only trigger for onDetachedFromWindow (protected in View). */
167
+ @androidx.annotation.VisibleForTesting
168
+ internal fun callOnDetachedFromWindow() = onDetachedFromWindow()
169
+
158
170
  // ------------------------------------------------------------------
159
171
  // Fabric layout workaround
160
172
  // ------------------------------------------------------------------
@@ -276,9 +288,10 @@ class ReactVideoCarouselOverlayHost(context: Context) : FrameLayout(context), Vi
276
288
  putBoolean("isActive", false)
277
289
  putString("playerState", "idle")
278
290
  putBoolean("isMuted", cachedIsMuted)
279
- if (item.videos.isNotEmpty()) {
280
- putString("activeVideo", ShortKitBridge.serializeContentItemToJSON(item.videos.first()))
281
- putInt("activeVideoIndex", 0)
291
+ val initialIdx = item.clampedInitialPageIndex
292
+ if (item.videos.indices.contains(initialIdx)) {
293
+ putString("activeVideo", ShortKitBridge.serializeContentItemToJSON(item.videos[initialIdx]))
294
+ putInt("activeVideoIndex", initialIdx)
282
295
  }
283
296
  }
284
297
 
@@ -298,11 +311,12 @@ class ReactVideoCarouselOverlayHost(context: Context) : FrameLayout(context), Vi
298
311
  putBoolean("isActive", false)
299
312
  putString("playerState", "idle")
300
313
  putBoolean("isMuted", cachedIsMuted)
301
- putInt("activeVideoIndex", 0)
302
- if (item.videos.isNotEmpty()) {
314
+ val initialIdx = item.clampedInitialPageIndex
315
+ putInt("activeVideoIndex", initialIdx)
316
+ if (item.videos.indices.contains(initialIdx)) {
303
317
  putString(
304
318
  "activeVideo",
305
- ShortKitBridge.serializeContentItemToJSON(item.videos.first()),
319
+ ShortKitBridge.serializeContentItemToJSON(item.videos[initialIdx]),
306
320
  )
307
321
  } else {
308
322
  putNull("activeVideo")
@@ -630,15 +644,19 @@ class ReactVideoCarouselOverlayHost(context: Context) : FrameLayout(context), Vi
630
644
  // Cleanup
631
645
  // ------------------------------------------------------------------
632
646
 
633
- override fun onDetachedFromWindow() {
647
+ public override fun onDetachedFromWindow() {
634
648
  super.onDetachedFromWindow()
649
+ layoutHandler.removeCallbacks(layoutRunnable)
650
+ // Cancel flow subscriptions so StateFlow collectors don't retain this host
651
+ // after permanent cell eviction. onAttachedToWindow re-subscribes via the
652
+ // existing `if (flowScope == null)` guard; attach(player) calls
653
+ // resubscribeToPlayer unconditionally, which is the canonical re-entry point.
654
+ //
655
+ // Do NOT call stopTimeCoalescing() here — the time coalescer is owned
656
+ // exclusively by activatePlayback/deactivatePlayback. Stopping it on detach
657
+ // breaks the kept-attached-zone scenario where activatePlayback won't re-fire.
635
658
  flowScope?.cancel()
636
659
  flowScope = null
637
- isActive = false
638
- currentCarouselItem = null
639
- stopTimeCoalescing()
640
- if (surface?.isRunning == true) {
641
- surface?.stop()
642
- }
643
660
  }
661
+
644
662
  }
@@ -33,6 +33,7 @@ import com.shortkit.sdk.feed.ShortKitRefreshState
33
33
  import com.shortkit.sdk.config.VideoOverlayMode
34
34
  import com.shortkit.sdk.feed.FeedPreload
35
35
  import com.shortkit.sdk.feed.ShortKitFeedFragment
36
+ import com.shortkit.sdk.feed.VideoCarouselCellLongPressState
36
37
  import kotlinx.coroutines.CoroutineScope
37
38
  import kotlinx.coroutines.Dispatchers
38
39
  import kotlinx.coroutines.SupervisorJob
@@ -446,6 +447,7 @@ class ShortKitBridge(
446
447
  videos = videoInputs,
447
448
  title = itemObj.optString("title", null),
448
449
  description = itemObj.optString("description", null),
450
+ initialPageIndex = itemObj.optInt("initialPageIndex", -1).takeIf { it >= 0 },
449
451
  author = itemObj.optString("author", null),
450
452
  section = itemObj.optString("section", null),
451
453
  articleUrl = itemObj.optString("articleUrl", null),
@@ -551,6 +553,13 @@ class ShortKitBridge(
551
553
  private var shortKit: ShortKit? = null
552
554
  private var scope: CoroutineScope? = null
553
555
 
556
+ /**
557
+ * Factory for [WritableMap] instances. Overridden in unit tests with
558
+ * `{ JavaOnlyMap() }` to avoid the JNI initialisation that
559
+ * [Arguments.createMap] requires. Production code always uses the default.
560
+ */
561
+ internal var createMap: () -> WritableMap = { Arguments.createMap() }
562
+
554
563
  /** Preload handles keyed by UUID, consumed by feed views via preloadId prop. */
555
564
  var preloadHandles = mutableMapOf<String, FeedPreload>()
556
565
  private set
@@ -584,7 +593,8 @@ class ShortKitBridge(
584
593
  customDimensions = dims,
585
594
  debugPanelEnabled = debugPanel,
586
595
  serverTracingEnabled = serverTracingEnabled,
587
- consoleTracingEnabled = consoleTracingEnabled
596
+ consoleTracingEnabled = consoleTracingEnabled,
597
+ poolDebugEnabled = consoleTracingEnabled,
588
598
  )
589
599
  this.shortKit = sdk
590
600
  shared = this
@@ -732,6 +742,11 @@ class ShortKitBridge(
732
742
  fragment.onVideoCarouselCellTap = { payload ->
733
743
  emitVideoCarouselCellTap(payload.id, payload.index, payload.pageIndex)
734
744
  }
745
+ fragment.onVideoCarouselCellLongPress = { payload ->
746
+ emitVideoCarouselCellLongPress(
747
+ payload.id, payload.index, payload.pageIndex, payload.state
748
+ )
749
+ }
735
750
  // Per-feed transition event. Fires once per transition, bound to this
736
751
  // fragment. Replaces the global player.feedTransition.collect pattern
737
752
  // (which fanned every feed's transitions out to every mounted
@@ -933,6 +948,29 @@ class ShortKitBridge(
933
948
  emitEventOnMain("onVideoCarouselCellTap", params)
934
949
  }
935
950
 
951
+ /**
952
+ * Emit `onVideoCarouselCellLongPress` with the active surface's feedId.
953
+ * Mirrors iOS ShortKitBridge.emitOnMain("onVideoCarouselCellLongPress").
954
+ * State enum is lowercased so JS receives "began" | "ended" | "cancelled"
955
+ * matching the iOS bridge output.
956
+ */
957
+ internal fun emitVideoCarouselCellLongPress(
958
+ itemId: String,
959
+ index: Int,
960
+ pageIndex: Int,
961
+ state: VideoCarouselCellLongPressState,
962
+ ) {
963
+ val feedId = activeSurfaceFeedId()
964
+ val params = createMap().apply {
965
+ putString("feedId", feedId)
966
+ putString("id", itemId)
967
+ putInt("index", index)
968
+ putInt("pageIndex", pageIndex)
969
+ putString("state", state.name.lowercase())
970
+ }
971
+ emitEventOnMain("onVideoCarouselCellLongPress", params)
972
+ }
973
+
936
974
  // ------------------------------------------------------------------
937
975
  // Player commands
938
976
  // ------------------------------------------------------------------
@@ -205,6 +205,7 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
205
205
  synchronized(ShortKitBridge.staticPendingFeedViews) {
206
206
  ShortKitBridge.staticPendingFeedViews.remove(this)
207
207
  }
208
+ handler.removeCallbacks(constrainRunnable)
208
209
  // Mirror iOS willMove(toWindow:) at react_native_sdk/ios/ShortKitFeedView.swift:91-99 —
209
210
  // suspend only if leaving the window AND the prop says we should be active.
210
211
  // When active=false the host has already driven the fragment into deactivate
@@ -352,7 +352,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
352
352
  "onVideoCarouselCellTap" -> emitOnVideoCarouselCellTap(params)
353
353
  "onVideoCarouselItemChanged" -> emitOnVideoCarouselItemChanged(params)
354
354
  "onCarouselActiveVideoCompleted" -> emitOnCarouselActiveVideoCompleted(params)
355
- "onVideoCarouselCellTap" -> emitOnVideoCarouselCellTap(params)
355
+ "onVideoCarouselCellLongPress" -> emitOnVideoCarouselCellLongPress(params)
356
356
  else -> {
357
357
  android.util.Log.w("SK:Module", "sendEvent: unknown event name '$name', using legacy emitter")
358
358
  reactApplicationContext
@@ -0,0 +1,155 @@
1
+ package com.shortkit.reactnative
2
+
3
+ import android.content.Context
4
+ import android.view.ViewGroup
5
+ import androidx.test.core.app.ApplicationProvider
6
+ import com.facebook.react.bridge.JavaOnlyMap
7
+ import com.facebook.react.interfaces.TaskInterface
8
+ import com.facebook.react.interfaces.fabric.ReactSurface
9
+ import com.shortkit.sdk.model.CarouselImage
10
+ import com.shortkit.sdk.model.ImageCarouselItem
11
+ import kotlinx.coroutines.isActive
12
+ import org.junit.Assert.assertEquals
13
+ import org.junit.Assert.assertFalse
14
+ import org.junit.Assert.assertNotNull
15
+ import org.junit.Assert.assertTrue
16
+ import org.junit.Test
17
+ import org.junit.runner.RunWith
18
+ import org.robolectric.RobolectricTestRunner
19
+ import java.util.concurrent.TimeUnit
20
+
21
+ /**
22
+ * Lifecycle contract for ReactCarouselOverlayHost: surface must survive
23
+ * onDetachedFromWindow / onAttachedToWindow cycles (iOS-parity), and
24
+ * resetState() must be a no-op.
25
+ *
26
+ * iOS reference: ReactCarouselOverlayHost.swift has no willMove/didMoveToWindow
27
+ * override — surface only stops in deinit. resetState() is also a no-op on iOS:
28
+ * "Don't clear surface props — configure() will overwrite."
29
+ *
30
+ * Mirrors the contract enforced by ReactVideoCarouselOverlayHostLifecycleTest
31
+ * for the video carousel host.
32
+ */
33
+ @RunWith(RobolectricTestRunner::class)
34
+ class ReactCarouselOverlayHostLifecycleTest {
35
+
36
+ private object NoOpTask : TaskInterface<Void> {
37
+ override fun waitForCompletion() = Unit
38
+ override fun waitForCompletion(duration: Long, timeUnit: TimeUnit): Boolean = true
39
+ override fun getResult(): Void? = null
40
+ override fun getError(): Exception? = null
41
+ override fun isCompleted(): Boolean = true
42
+ override fun isCancelled(): Boolean = false
43
+ override fun isFaulted(): Boolean = false
44
+ }
45
+
46
+ /**
47
+ * Mutable-state ReactSurface stub tracking isRunning and stop call count.
48
+ * Mirrors TrackingSurface in ReactVideoCarouselOverlayHostLifecycleTest.
49
+ */
50
+ private class TrackingSurface : ReactSurface {
51
+ private val ctx: Context = ApplicationProvider.getApplicationContext()
52
+ private var _running: Boolean = false
53
+ var stopCount: Int = 0
54
+
55
+ override val surfaceID: Int = 0
56
+ override val moduleName: String = "TrackingSurface"
57
+ override val isRunning: Boolean get() = _running
58
+ override val view: ViewGroup? = null
59
+ override val context: Context get() = ctx
60
+ override fun prerender(): TaskInterface<Void> = NoOpTask
61
+ override fun start(): TaskInterface<Void> { _running = true; return NoOpTask }
62
+ override fun stop(): TaskInterface<Void> { _running = false; stopCount++; return NoOpTask }
63
+ override fun clear() = Unit
64
+ override fun detach() = Unit
65
+ }
66
+
67
+ private fun makeHost(surface: TrackingSurface): ReactCarouselOverlayHost {
68
+ val ctx = ApplicationProvider.getApplicationContext<Context>()
69
+ return ReactCarouselOverlayHost(ctx).also {
70
+ it.createMap = { JavaOnlyMap() }
71
+ it.createSurface = { _, _ -> surface }
72
+ it.emitEvent = { _, _ -> }
73
+ }
74
+ }
75
+
76
+ private fun item(id: String) = ImageCarouselItem(
77
+ id = id,
78
+ images = listOf(CarouselImage(url = "https://example.com/$id.jpg", alt = null)),
79
+ )
80
+
81
+ @Test
82
+ fun `onDetachedFromWindow does not stop surface`() {
83
+ val surface = TrackingSurface()
84
+ val host = makeHost(surface)
85
+ host.configure(item("A"))
86
+ assertTrue("surface must be running after configure", surface.isRunning)
87
+ val stopsBefore = surface.stopCount
88
+
89
+ host.onDetachedFromWindow()
90
+
91
+ assertTrue(
92
+ "surface must remain running after onDetachedFromWindow (iOS-parity)",
93
+ surface.isRunning,
94
+ )
95
+ assertEquals(
96
+ "onDetachedFromWindow must not call surface.stop()",
97
+ stopsBefore,
98
+ surface.stopCount,
99
+ )
100
+ }
101
+
102
+ @Test
103
+ fun `onDetachedFromWindow preserves currentItemId`() {
104
+ val surface = TrackingSurface()
105
+ val host = makeHost(surface)
106
+ host.configure(item("A"))
107
+ assertEquals("A", host.currentItemId)
108
+
109
+ host.onDetachedFromWindow()
110
+
111
+ assertEquals(
112
+ "currentItemId must survive onDetachedFromWindow so configure() can detect same vs different item",
113
+ "A",
114
+ host.currentItemId,
115
+ )
116
+ }
117
+
118
+ @Test
119
+ fun `resetState does not stop surface`() {
120
+ // resetState() must be a no-op: iOS ReactCarouselOverlayHost.swift
121
+ // resetState() body is "// Don't clear surface props — configure() will overwrite."
122
+ // Mirrors ReactVideoCarouselOverlayHost.resetState() which is also a no-op.
123
+ // Note: the current production cast (surface as? ReactSurfaceImpl) silently
124
+ // no-ops in Robolectric (stub isn't ReactSurfaceImpl), so this test
125
+ // documents intent and guards against future regressions that add
126
+ // a surface?.stop() call.
127
+ val surface = TrackingSurface()
128
+ val host = makeHost(surface)
129
+ host.configure(item("A"))
130
+ assertTrue("surface must be running after configure", surface.isRunning)
131
+ val stopsBefore = surface.stopCount
132
+
133
+ host.resetState()
134
+
135
+ assertTrue("surface must remain running after resetState", surface.isRunning)
136
+ assertEquals("resetState must not call surface.stop()", stopsBefore, surface.stopCount)
137
+ }
138
+
139
+ @Test
140
+ fun `onDetachedFromWindow cancels ioScope and it is recreated on reattach`() {
141
+ val ctx = ApplicationProvider.getApplicationContext<Context>()
142
+ val host = ReactCarouselOverlayHost(ctx)
143
+
144
+ assertTrue("ioScope must be active initially", host.ioScopeForTest.isActive)
145
+
146
+ host.callOnDetachedFromWindow()
147
+
148
+ assertFalse("ioScope must be cancelled after detach", host.ioScopeForTest.isActive)
149
+
150
+ // Simulate reattach
151
+ host.callOnAttachedToWindow()
152
+
153
+ assertTrue("ioScope must be active again after reattach", host.ioScopeForTest.isActive)
154
+ }
155
+ }
@@ -0,0 +1,200 @@
1
+ package com.shortkit.reactnative
2
+
3
+ import android.content.Context
4
+ import android.view.ViewGroup
5
+ import androidx.test.core.app.ApplicationProvider
6
+ import com.facebook.react.bridge.JavaOnlyMap
7
+ import com.facebook.react.interfaces.TaskInterface
8
+ import com.facebook.react.interfaces.fabric.ReactSurface
9
+ import com.shortkit.sdk.model.ContentItem
10
+ import com.shortkit.sdk.model.VideoCarouselItem
11
+ import kotlinx.coroutines.CoroutineScope
12
+ import kotlinx.coroutines.Dispatchers
13
+ import kotlinx.coroutines.SupervisorJob
14
+ import kotlinx.coroutines.isActive
15
+ import org.junit.Assert.assertEquals
16
+ import org.junit.Assert.assertFalse
17
+ import org.junit.Assert.assertNotNull
18
+ import org.junit.Assert.assertNull
19
+ import org.junit.Assert.assertTrue
20
+ import org.junit.Test
21
+ import org.junit.runner.RunWith
22
+ import org.robolectric.RobolectricTestRunner
23
+ import java.util.concurrent.TimeUnit
24
+
25
+ /**
26
+ * Lifecycle contract for ReactVideoCarouselOverlayHost: surface and state
27
+ * must survive onDetachedFromWindow / onAttachedToWindow cycles
28
+ * (iOS-parity), and only release() should perform true teardown.
29
+ *
30
+ * iOS reference: swift_sdk/.../VideoCarouselCell.swift:472-476 keeps the
31
+ * overlay alive across prepareForReuse; the host's deinit performs final
32
+ * cleanup. Android equivalent: this lifecycle test enforces the same
33
+ * contract on the RN host.
34
+ */
35
+ @RunWith(RobolectricTestRunner::class)
36
+ class ReactVideoCarouselOverlayHostLifecycleTest {
37
+
38
+ private object NoOpTask : TaskInterface<Void> {
39
+ override fun waitForCompletion() = Unit
40
+ override fun waitForCompletion(duration: Long, timeUnit: TimeUnit): Boolean = true
41
+ override fun getResult(): Void? = null
42
+ override fun getError(): Exception? = null
43
+ override fun isCompleted(): Boolean = true
44
+ override fun isCancelled(): Boolean = false
45
+ override fun isFaulted(): Boolean = false
46
+ }
47
+
48
+ /**
49
+ * Mutable-state ReactSurface stub that tracks isRunning + start/stop
50
+ * calls. Mirrors the StubReactSurface shape used by
51
+ * ReactVideoCarouselOverlayHostEmitTest, but with mutable state via
52
+ * a backing field + custom getter (the interface declares isRunning
53
+ * as `val`, so we can't override with `var`).
54
+ */
55
+ private class TrackingSurface : ReactSurface {
56
+ private val ctx: Context = ApplicationProvider.getApplicationContext()
57
+ private var _running: Boolean = false
58
+ var stopCount: Int = 0
59
+
60
+ override val surfaceID: Int = 0
61
+ override val moduleName: String = "TrackingSurface"
62
+ override val isRunning: Boolean get() = _running
63
+ override val view: ViewGroup? = null
64
+ override val context: Context get() = ctx
65
+ override fun prerender(): TaskInterface<Void> = NoOpTask
66
+ override fun start(): TaskInterface<Void> { _running = true; return NoOpTask }
67
+ override fun stop(): TaskInterface<Void> { _running = false; stopCount++; return NoOpTask }
68
+ override fun clear() = Unit
69
+ override fun detach() = Unit
70
+ }
71
+
72
+ private fun newHost(surface: TrackingSurface): ReactVideoCarouselOverlayHost {
73
+ val host = ReactVideoCarouselOverlayHost(
74
+ ApplicationProvider.getApplicationContext()
75
+ )
76
+ host.videoCarouselOverlayName = "test"
77
+ host.createSurface = { _, _ -> surface }
78
+ host.createMap = { JavaOnlyMap() }
79
+ host.emitEvent = { _, _ -> /* no-op for lifecycle tests */ }
80
+ return host
81
+ }
82
+
83
+ private fun video(id: String) = ContentItem(
84
+ id = id,
85
+ title = "t",
86
+ duration = 5.0,
87
+ streamingUrl = "https://example.com/$id.m3u8",
88
+ thumbnailUrl = "https://example.com/$id.jpg",
89
+ captionTracks = emptyList(),
90
+ )
91
+
92
+ private fun item(id: String, videoCount: Int = 1) = VideoCarouselItem(
93
+ id = id,
94
+ videos = (0 until videoCount).map { video("$id-$it") },
95
+ )
96
+
97
+ @Test
98
+ fun `onDetachedFromWindow does not stop surface`() {
99
+ val surface = TrackingSurface()
100
+ val host = newHost(surface)
101
+ host.configure(item("c1")) // first mount: applies props, starts surface
102
+ assertTrue("surface must be running after configure", surface.isRunning)
103
+ val stopsBefore = surface.stopCount
104
+
105
+ host.onDetachedFromWindow()
106
+
107
+ assertTrue(
108
+ "surface must remain running after onDetachedFromWindow (iOS-parity)",
109
+ surface.isRunning,
110
+ )
111
+ assertEquals(
112
+ "onDetachedFromWindow must not call surface.stop()",
113
+ stopsBefore,
114
+ surface.stopCount,
115
+ )
116
+ }
117
+
118
+ @Test
119
+ fun `onDetachedFromWindow preserves currentCarouselItem`() {
120
+ val surface = TrackingSurface()
121
+ val host = newHost(surface)
122
+ host.configure(item("c1"))
123
+ assertNotNull(host.currentCarouselItem)
124
+
125
+ host.onDetachedFromWindow()
126
+
127
+ assertNotNull(
128
+ "currentCarouselItem must survive onDetachedFromWindow so re-attach can detect same vs different item",
129
+ host.currentCarouselItem,
130
+ )
131
+ }
132
+
133
+ @Test
134
+ fun `onDetachedFromWindow preserves isActive`() {
135
+ // Suspend/resume scenario: active cell never changes, so the feed's
136
+ // active-cell propagation does not re-fire activatePlayback on
137
+ // resume. If detach flipped isActive false, every subsequent
138
+ // updateActiveVideo emit would be gated out and per-page swipes
139
+ // wouldn't update the overlay. iOS-parity: isActive is owned
140
+ // exclusively by activatePlayback / deactivatePlayback; lifecycle
141
+ // hooks must not touch it.
142
+ val surface = TrackingSurface()
143
+ val host = newHost(surface)
144
+ host.configure(item("c1"))
145
+ host.activatePlayback()
146
+ assertTrue(host.isActive)
147
+
148
+ host.onDetachedFromWindow()
149
+
150
+ assertTrue(
151
+ "isActive must survive onDetachedFromWindow — only activatePlayback/deactivatePlayback own it",
152
+ host.isActive,
153
+ )
154
+ }
155
+
156
+ @Test
157
+ fun `onDetachedFromWindow does not stop time coalescer`() {
158
+ // If detach stopped the coalescer, on resume nothing restarts it
159
+ // (activatePlayback is the only caller and doesn't re-fire when
160
+ // the active cell never changed). Time emits would silently stop.
161
+ // Mirrors the isActive ownership rule: detach is a no-op for
162
+ // playback-driven state.
163
+ val surface = TrackingSurface()
164
+ val host = newHost(surface)
165
+ host.configure(item("c1"))
166
+ host.activatePlayback()
167
+ // activatePlayback() is what kicks off time coalescing — once
168
+ // running, it should survive detach.
169
+
170
+ // No assertion on internals; the contract is observable via the
171
+ // updateActiveVideo regression test above. This test exists to
172
+ // document the non-destructive-detach intent: onDetachedFromWindow
173
+ // must not call stopTimeCoalescing().
174
+ host.onDetachedFromWindow()
175
+
176
+ // host.isActive still true => the coalesce runnable would still
177
+ // tick if it weren't cancelled. We cannot easily inspect the
178
+ // handler queue from here, but we can assert the public state
179
+ // hasn't been disturbed:
180
+ assertTrue("isActive untouched", host.isActive)
181
+ assertNotNull("currentCarouselItem untouched", host.currentCarouselItem)
182
+ }
183
+
184
+ @Test
185
+ fun `onDetachedFromWindow cancels flowScope`() {
186
+ val ctx = ApplicationProvider.getApplicationContext<Context>()
187
+ val host = ReactVideoCarouselOverlayHost(ctx)
188
+
189
+ // Inject a live scope directly via the writable test accessor.
190
+ val injected = CoroutineScope(Dispatchers.Main + SupervisorJob())
191
+ host.flowScopeForTest = injected
192
+
193
+ assertTrue("injected scope must be active before detach", injected.isActive)
194
+
195
+ host.callOnDetachedFromWindow()
196
+
197
+ assertNull("flowScope must be null after detach", host.flowScopeForTest)
198
+ assertFalse("injected scope must be cancelled after detach", injected.isActive)
199
+ }
200
+ }
@@ -0,0 +1,148 @@
1
+ package com.shortkit.reactnative
2
+
3
+ import androidx.test.core.app.ApplicationProvider
4
+ import com.facebook.react.bridge.JavaOnlyMap
5
+ import com.facebook.react.bridge.WritableMap
6
+ import com.shortkit.sdk.feed.VideoCarouselCellLongPressState
7
+ import org.junit.Assert.assertEquals
8
+ import org.junit.Test
9
+ import org.junit.runner.RunWith
10
+ import org.robolectric.RobolectricTestRunner
11
+
12
+ /**
13
+ * JVM unit tests for [ShortKitBridge.emitVideoCarouselCellLongPress].
14
+ *
15
+ * Verifies the event name, state-string lowercasing for all three enum values,
16
+ * and the full payload shape (feedId, id, index, pageIndex).
17
+ *
18
+ * The [JavaOnlyMap] seam avoids the JNI initialisation that
19
+ * [com.facebook.react.bridge.Arguments.createMap] requires in host tests
20
+ * (same pattern used in ReactVideoCarouselOverlayHostEmitTest).
21
+ */
22
+ @RunWith(RobolectricTestRunner::class)
23
+ class ShortKitBridgeLongPressTest {
24
+
25
+ /**
26
+ * Construct a minimal [ShortKitBridge] with a captured [emitEvent] lambda.
27
+ * [JavaOnlyMap] is injected via the [ShortKitBridge.createMap] seam so
28
+ * [Arguments.createMap] (which requires JNI) is never called in tests.
29
+ */
30
+ private fun makeBridge(
31
+ onEmit: (String, WritableMap) -> Unit,
32
+ ): ShortKitBridge {
33
+ val bridge = ShortKitBridge(
34
+ apiKey = "pk_test_unit",
35
+ context = ApplicationProvider.getApplicationContext(),
36
+ hasLoadingView = false,
37
+ clientAppName = null,
38
+ clientAppVersion = null,
39
+ customDimensionsJSON = null,
40
+ debugPanel = false,
41
+ serverTracingEnabled = false,
42
+ consoleTracingEnabled = false,
43
+ emitEvent = onEmit,
44
+ )
45
+ // Override the map factory before any emit call so the bridge never
46
+ // reaches Arguments.createMap() in emitVideoCarouselCellLongPress.
47
+ bridge.createMap = { JavaOnlyMap() }
48
+ return bridge
49
+ }
50
+
51
+ // ------------------------------------------------------------------
52
+ // State-string lowercasing
53
+ // ------------------------------------------------------------------
54
+
55
+ @Test
56
+ fun emitsBegan() {
57
+ val emitted = mutableListOf<Pair<String, WritableMap>>()
58
+ val bridge = makeBridge { name, params -> emitted.add(name to params) }
59
+
60
+ bridge.emitVideoCarouselCellLongPress(
61
+ itemId = "item-1",
62
+ index = 0,
63
+ pageIndex = 0,
64
+ state = VideoCarouselCellLongPressState.BEGAN,
65
+ )
66
+
67
+ assertEquals(1, emitted.size)
68
+ assertEquals("onVideoCarouselCellLongPress", emitted[0].first)
69
+ assertEquals("began", emitted[0].second.getString("state"))
70
+ }
71
+
72
+ @Test
73
+ fun emitsEnded() {
74
+ val emitted = mutableListOf<Pair<String, WritableMap>>()
75
+ val bridge = makeBridge { name, params -> emitted.add(name to params) }
76
+
77
+ bridge.emitVideoCarouselCellLongPress(
78
+ itemId = "item-1",
79
+ index = 0,
80
+ pageIndex = 0,
81
+ state = VideoCarouselCellLongPressState.ENDED,
82
+ )
83
+
84
+ assertEquals(1, emitted.size)
85
+ assertEquals("onVideoCarouselCellLongPress", emitted[0].first)
86
+ assertEquals("ended", emitted[0].second.getString("state"))
87
+ }
88
+
89
+ @Test
90
+ fun emitsCancelled() {
91
+ val emitted = mutableListOf<Pair<String, WritableMap>>()
92
+ val bridge = makeBridge { name, params -> emitted.add(name to params) }
93
+
94
+ bridge.emitVideoCarouselCellLongPress(
95
+ itemId = "item-1",
96
+ index = 0,
97
+ pageIndex = 0,
98
+ state = VideoCarouselCellLongPressState.CANCELLED,
99
+ )
100
+
101
+ assertEquals(1, emitted.size)
102
+ assertEquals("onVideoCarouselCellLongPress", emitted[0].first)
103
+ assertEquals("cancelled", emitted[0].second.getString("state"))
104
+ }
105
+
106
+ // ------------------------------------------------------------------
107
+ // Payload shape
108
+ // ------------------------------------------------------------------
109
+
110
+ @Test
111
+ fun emitsCorrectPayloadFields() {
112
+ val emitted = mutableListOf<Pair<String, WritableMap>>()
113
+ val bridge = makeBridge { name, params -> emitted.add(name to params) }
114
+
115
+ bridge.emitVideoCarouselCellLongPress(
116
+ itemId = "car-1",
117
+ index = 3,
118
+ pageIndex = 2,
119
+ state = VideoCarouselCellLongPressState.BEGAN,
120
+ )
121
+
122
+ val params = emitted[0].second
123
+ assertEquals("car-1", params.getString("id"))
124
+ assertEquals(3, params.getInt("index"))
125
+ assertEquals(2, params.getInt("pageIndex"))
126
+ assertEquals("began", params.getString("state"))
127
+ }
128
+
129
+ @Test
130
+ fun emitsFeedId() {
131
+ val emitted = mutableListOf<Pair<String, WritableMap>>()
132
+ val bridge = makeBridge { name, params -> emitted.add(name to params) }
133
+
134
+ bridge.emitVideoCarouselCellLongPress(
135
+ itemId = "item-1",
136
+ index = 0,
137
+ pageIndex = 0,
138
+ state = VideoCarouselCellLongPressState.BEGAN,
139
+ )
140
+
141
+ // activeSurfaceFeedId() returns "" when no active surface is registered
142
+ // (no ShortKitFeedFragment has been attached in this unit-test environment).
143
+ // The important guarantee is that the "feedId" key is present in the map,
144
+ // matching the iOS bridge payload shape.
145
+ val params = emitted[0].second
146
+ assertEquals("", params.getString("feedId"))
147
+ }
148
+ }
@@ -208,7 +208,8 @@ import ShortKitSDK
208
208
  customDimensions: dimensions,
209
209
  debugPanelEnabled: debugPanel,
210
210
  serverTracingEnabled: serverTracingEnabled,
211
- consoleTracingEnabled: consoleTracingEnabled
211
+ consoleTracingEnabled: consoleTracingEnabled,
212
+ poolDebugEnabled: false
212
213
  )
213
214
  self.shortKit = sdk
214
215
 
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>ShortKitSDK.framework/ShortKitSDK</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64</string>
11
+ <string>ios-arm64_x86_64-simulator</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>ShortKitSDK.framework</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
+ <string>x86_64</string>
17
18
  </array>
18
19
  <key>SupportedPlatform</key>
19
20
  <string>ios</string>
21
+ <key>SupportedPlatformVariant</key>
22
+ <string>simulator</string>
20
23
  </dict>
21
24
  <dict>
22
25
  <key>BinaryPath</key>
23
26
  <string>ShortKitSDK.framework/ShortKitSDK</string>
24
27
  <key>LibraryIdentifier</key>
25
- <string>ios-arm64_x86_64-simulator</string>
28
+ <string>ios-arm64</string>
26
29
  <key>LibraryPath</key>
27
30
  <string>ShortKitSDK.framework</string>
28
31
  <key>SupportedArchitectures</key>
29
32
  <array>
30
33
  <string>arm64</string>
31
- <string>x86_64</string>
32
34
  </array>
33
35
  <key>SupportedPlatform</key>
34
36
  <string>ios</string>
35
- <key>SupportedPlatformVariant</key>
36
- <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>
@@ -11,9 +11,9 @@
11
11
  <key>CFBundlePackageType</key>
12
12
  <string>FMWK</string>
13
13
  <key>CFBundleVersion</key>
14
- <string>0.2.37</string>
14
+ <string>0.2.39</string>
15
15
  <key>CFBundleShortVersionString</key>
16
- <string>0.2.37</string>
16
+ <string>0.2.39</string>
17
17
  <key>MinimumOSVersion</key>
18
18
  <string>16.0</string>
19
19
  </dict>
@@ -52850,14 +52850,14 @@
52850
52850
  {
52851
52851
  "filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/Feed\/VideoCarouselCell.swift",
52852
52852
  "kind": "BooleanLiteral",
52853
- "offset": 31591,
52853
+ "offset": 45026,
52854
52854
  "length": 4,
52855
52855
  "value": "true"
52856
52856
  },
52857
52857
  {
52858
52858
  "filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/Feed\/VideoCarouselCell.swift",
52859
52859
  "kind": "IntegerLiteral",
52860
- "offset": 38856,
52860
+ "offset": 52291,
52861
52861
  "length": 1,
52862
52862
  "value": "0"
52863
52863
  },
@@ -53517,7 +53517,7 @@
53517
53517
  "kind": "StringLiteral",
53518
53518
  "offset": 154,
53519
53519
  "length": 8,
53520
- "value": "\"0.2.37\""
53520
+ "value": "\"0.2.39\""
53521
53521
  },
53522
53522
  {
53523
53523
  "filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/ShortKit.swift",
@@ -10,11 +10,11 @@
10
10
  </data>
11
11
  <key>Info.plist</key>
12
12
  <data>
13
- Rmlks05t/ZAWDJyrlYCLZI2hLt4=
13
+ DfvOpn/tL7HwpstLyexoHO9tmlc=
14
14
  </data>
15
15
  <key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json</key>
16
16
  <data>
17
- o4q5PMYrrSkfEuV3X5DKLlP/Ic8=
17
+ AhyuvP9Jp9aQmkTfkyn18bZWgYE=
18
18
  </data>
19
19
  <key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface</key>
20
20
  <data>
@@ -50,7 +50,7 @@
50
50
  <dict>
51
51
  <key>hash2</key>
52
52
  <data>
53
- GPD6LqlhW6OfAta/UbVY9JS6SMSbk402F6pveuST4rY=
53
+ NxZxhP9cPb/bnY8osKT6QvOK/iH3xHbDQNIHRYJGfQ0=
54
54
  </data>
55
55
  </dict>
56
56
  <key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface</key>
@@ -11,9 +11,9 @@
11
11
  <key>CFBundlePackageType</key>
12
12
  <string>FMWK</string>
13
13
  <key>CFBundleVersion</key>
14
- <string>0.2.37</string>
14
+ <string>0.2.39</string>
15
15
  <key>CFBundleShortVersionString</key>
16
- <string>0.2.37</string>
16
+ <string>0.2.39</string>
17
17
  <key>MinimumOSVersion</key>
18
18
  <string>16.0</string>
19
19
  </dict>
@@ -52850,14 +52850,14 @@
52850
52850
  {
52851
52851
  "filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/Feed\/VideoCarouselCell.swift",
52852
52852
  "kind": "BooleanLiteral",
52853
- "offset": 31591,
52853
+ "offset": 45026,
52854
52854
  "length": 4,
52855
52855
  "value": "true"
52856
52856
  },
52857
52857
  {
52858
52858
  "filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/Feed\/VideoCarouselCell.swift",
52859
52859
  "kind": "IntegerLiteral",
52860
- "offset": 38856,
52860
+ "offset": 52291,
52861
52861
  "length": 1,
52862
52862
  "value": "0"
52863
52863
  },
@@ -53517,7 +53517,7 @@
53517
53517
  "kind": "StringLiteral",
53518
53518
  "offset": 154,
53519
53519
  "length": 8,
53520
- "value": "\"0.2.37\""
53520
+ "value": "\"0.2.39\""
53521
53521
  },
53522
53522
  {
53523
53523
  "filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/ShortKit.swift",
@@ -52850,14 +52850,14 @@
52850
52850
  {
52851
52851
  "filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/Feed\/VideoCarouselCell.swift",
52852
52852
  "kind": "BooleanLiteral",
52853
- "offset": 31591,
52853
+ "offset": 45026,
52854
52854
  "length": 4,
52855
52855
  "value": "true"
52856
52856
  },
52857
52857
  {
52858
52858
  "filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/Feed\/VideoCarouselCell.swift",
52859
52859
  "kind": "IntegerLiteral",
52860
- "offset": 38856,
52860
+ "offset": 52291,
52861
52861
  "length": 1,
52862
52862
  "value": "0"
52863
52863
  },
@@ -53517,7 +53517,7 @@
53517
53517
  "kind": "StringLiteral",
53518
53518
  "offset": 154,
53519
53519
  "length": 8,
53520
- "value": "\"0.2.37\""
53520
+ "value": "\"0.2.39\""
53521
53521
  },
53522
53522
  {
53523
53523
  "filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/ShortKit.swift",
@@ -10,11 +10,11 @@
10
10
  </data>
11
11
  <key>Info.plist</key>
12
12
  <data>
13
- Rmlks05t/ZAWDJyrlYCLZI2hLt4=
13
+ DfvOpn/tL7HwpstLyexoHO9tmlc=
14
14
  </data>
15
15
  <key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json</key>
16
16
  <data>
17
- o4q5PMYrrSkfEuV3X5DKLlP/Ic8=
17
+ AhyuvP9Jp9aQmkTfkyn18bZWgYE=
18
18
  </data>
19
19
  <key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
20
20
  <data>
@@ -30,7 +30,7 @@
30
30
  </data>
31
31
  <key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
32
32
  <data>
33
- o4q5PMYrrSkfEuV3X5DKLlP/Ic8=
33
+ AhyuvP9Jp9aQmkTfkyn18bZWgYE=
34
34
  </data>
35
35
  <key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
36
36
  <data>
@@ -66,7 +66,7 @@
66
66
  <dict>
67
67
  <key>hash2</key>
68
68
  <data>
69
- GPD6LqlhW6OfAta/UbVY9JS6SMSbk402F6pveuST4rY=
69
+ NxZxhP9cPb/bnY8osKT6QvOK/iH3xHbDQNIHRYJGfQ0=
70
70
  </data>
71
71
  </dict>
72
72
  <key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
@@ -94,7 +94,7 @@
94
94
  <dict>
95
95
  <key>hash2</key>
96
96
  <data>
97
- GPD6LqlhW6OfAta/UbVY9JS6SMSbk402F6pveuST4rY=
97
+ NxZxhP9cPb/bnY8osKT6QvOK/iH3xHbDQNIHRYJGfQ0=
98
98
  </data>
99
99
  </dict>
100
100
  <key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shortkitsdk/react-native",
3
- "version": "0.2.37",
3
+ "version": "0.2.39",
4
4
  "description": "ShortKit React Native SDK — short-form video feed",
5
5
  "react-native": "src/index",
6
6
  "source": "src/index",