@shortkitsdk/react-native 0.2.6 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/ShortKitReactNative.podspec +1 -0
  2. package/android/build.gradle.kts +17 -1
  3. package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +379 -0
  4. package/android/src/main/java/com/shortkit/reactnative/ReactLoadingHost.kt +40 -0
  5. package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +570 -0
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +1029 -0
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +212 -219
  8. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +17 -3
  9. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +157 -742
  10. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +11 -2
  11. package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +2 -2
  12. package/ios/ReactCarouselOverlayHost.swift +177 -0
  13. package/ios/ReactLoadingHost.swift +38 -0
  14. package/ios/ReactOverlayHost.swift +444 -0
  15. package/ios/SKFabricSurfaceWrapper.h +18 -0
  16. package/ios/SKFabricSurfaceWrapper.mm +57 -0
  17. package/ios/ShortKitBridge.swift +220 -63
  18. package/ios/ShortKitFeedView.swift +82 -228
  19. package/ios/ShortKitFeedViewManager.mm +3 -2
  20. package/ios/ShortKitModule.mm +69 -37
  21. package/ios/ShortKitPlayerNativeView.swift +39 -8
  22. package/ios/ShortKitReactNative-Bridging-Header.h +2 -0
  23. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
  24. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +3683 -1249
  25. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +56 -15
  26. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  27. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +56 -15
  28. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  29. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
  30. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +3683 -1249
  31. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +56 -15
  32. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  33. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +56 -15
  34. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  35. package/ios/ShortKitSDK.xcframework.bak/Info.plist +43 -0
  36. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
  37. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
  38. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +28917 -0
  39. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +824 -0
  40. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  41. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +824 -0
  42. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  43. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  44. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
  45. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
  46. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +28917 -0
  47. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +824 -0
  48. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  49. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +824 -0
  50. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  51. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  52. package/ios/ShortKitWidgetNativeView.swift +3 -3
  53. package/package.json +1 -1
  54. package/src/ShortKitCarouselOverlaySurface.tsx +55 -0
  55. package/src/ShortKitCommands.ts +31 -0
  56. package/src/ShortKitContext.ts +6 -24
  57. package/src/ShortKitFeed.tsx +124 -41
  58. package/src/ShortKitLoadingSurface.tsx +24 -0
  59. package/src/ShortKitOverlaySurface.tsx +313 -0
  60. package/src/ShortKitPlayer.tsx +30 -9
  61. package/src/ShortKitProvider.tsx +28 -285
  62. package/src/index.ts +9 -3
  63. package/src/serialization.ts +20 -39
  64. package/src/specs/NativeShortKitModule.ts +74 -45
  65. package/src/specs/ShortKitFeedViewNativeComponent.ts +3 -2
  66. package/src/types.ts +84 -16
  67. package/src/useShortKit.ts +1 -3
  68. package/src/useShortKitPlayer.ts +7 -7
  69. package/android/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +0 -48
  70. package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +0 -128
  71. package/ios/ShortKitCarouselOverlayBridge.swift +0 -219
  72. package/ios/ShortKitOverlayBridge.swift +0 -111
  73. package/src/CarouselOverlayManager.tsx +0 -70
  74. package/src/OverlayManager.tsx +0 -87
  75. package/src/useShortKitCarousel.ts +0 -29
@@ -11,6 +11,7 @@ Pod::Spec.new do |s|
11
11
  s.platforms = { :ios => "16.0" }
12
12
  s.source = { :git => "https://github.com/shortkit/shortkit.git", :tag => "v#{s.version}" }
13
13
  s.source_files = "ios/*.{h,m,mm,cpp,swift}"
14
+ s.public_header_files = "ios/ShortKitModule.h", "ios/SKFabricSurfaceWrapper.h"
14
15
  s.requires_arc = true
15
16
 
16
17
  # When the vendored XCFramework is present (npm published package), use it.
@@ -1,6 +1,7 @@
1
1
  plugins {
2
2
  id("com.android.library")
3
3
  id("org.jetbrains.kotlin.android")
4
+ id("org.jetbrains.kotlin.plugin.serialization")
4
5
  id("com.facebook.react")
5
6
  }
6
7
 
@@ -30,5 +31,20 @@ android {
30
31
 
31
32
  dependencies {
32
33
  implementation("com.facebook.react:react-android")
33
- implementation("dev.shortkit:shortkit:0.1.0")
34
+ // When Reco's settings.gradle has the composite build, Gradle substitutes
35
+ // this with the live source from android_sdk/shortkit automatically.
36
+ // implementation("dev.shortkit:shortkit:0.2.11")
37
+ implementation(files("/Users/michaelseleman/shortkit/android_sdk/shortkit/build/outputs/aar/shortkit-release.aar"))
38
+ // Transitive deps needed when using local AAR (Maven artifact bundles these automatically)
39
+ implementation("androidx.media3:media3-exoplayer:1.5.1")
40
+ implementation("androidx.media3:media3-exoplayer-hls:1.5.1")
41
+ implementation("androidx.media3:media3-datasource:1.5.1")
42
+ implementation("androidx.media3:media3-ui:1.5.1")
43
+ implementation("androidx.recyclerview:recyclerview:1.4.0")
44
+ implementation("androidx.viewpager2:viewpager2:1.1.0")
45
+ implementation("androidx.fragment:fragment-ktx:1.8.5")
46
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
47
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
48
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
49
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
34
50
  }
@@ -0,0 +1,379 @@
1
+ package com.shortkit.reactnative
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.os.Bundle
6
+ import android.widget.FrameLayout
7
+ import com.facebook.react.ReactApplication
8
+ import com.facebook.react.bridge.Arguments
9
+ import com.facebook.react.interfaces.fabric.ReactSurface
10
+ import com.facebook.react.runtime.ReactSurfaceImpl
11
+ import com.shortkit.sdk.model.CarouselImage
12
+ import com.shortkit.sdk.model.ImageCarouselItem
13
+ import com.shortkit.sdk.overlay.CarouselOverlay
14
+ import kotlinx.coroutines.CoroutineScope
15
+ import kotlinx.coroutines.Dispatchers
16
+ import kotlinx.coroutines.Job
17
+ import kotlinx.coroutines.SupervisorJob
18
+ import kotlinx.coroutines.launch
19
+ import kotlinx.coroutines.withContext
20
+ import kotlinx.serialization.encodeToString
21
+ import kotlinx.serialization.json.Json
22
+ import java.io.File
23
+ import java.io.FileOutputStream
24
+
25
+ /**
26
+ * A [FrameLayout] that conforms to [CarouselOverlay] and hosts a React Native
27
+ * Fabric [ReactSurface] for rendering the developer's React carousel overlay
28
+ * component inside a feed cell.
29
+ *
30
+ * Unlike [ReactOverlayHost], this does not subscribe to player state (carousels
31
+ * are image-based). It pushes [ImageCarouselItem] data as surface properties.
32
+ *
33
+ * Android equivalent of `react_native_sdk/ios/ReactCarouselOverlayHost.swift`.
34
+ */
35
+ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), CarouselOverlay {
36
+
37
+ private companion object {
38
+ const val TAG = "SK:CarouselHost"
39
+ }
40
+
41
+ // ------------------------------------------------------------------
42
+ // Configuration
43
+ // ------------------------------------------------------------------
44
+
45
+ /** Suffix for the surface module name (e.g. "news" -> "ShortKitCarouselOverlay_news"). */
46
+ var carouselOverlayName: String = "Default"
47
+
48
+ // ------------------------------------------------------------------
49
+ // State
50
+ // ------------------------------------------------------------------
51
+
52
+ private var surface: ReactSurface? = null
53
+ private var pendingProps: Bundle? = null
54
+ private var isInLayoutPass: Boolean = false
55
+ private val ioScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
56
+ private var pendingWriteJob: Job? = null
57
+ private var configureGeneration: Int = 0
58
+
59
+ // ------------------------------------------------------------------
60
+ // Fabric layout workaround
61
+ // ------------------------------------------------------------------
62
+
63
+ private val layoutHandler = android.os.Handler(android.os.Looper.getMainLooper())
64
+
65
+ private val layoutRunnable = Runnable {
66
+ measure(
67
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
68
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
69
+ )
70
+ layout(left, top, right, bottom)
71
+ }
72
+
73
+ override fun requestLayout() {
74
+ super.requestLayout()
75
+ if (!isInLayoutPass) {
76
+ @Suppress("UNNECESSARY_SAFE_CALL")
77
+ layoutHandler?.post(layoutRunnable)
78
+ }
79
+ }
80
+
81
+ // ------------------------------------------------------------------
82
+ // Init
83
+ // ------------------------------------------------------------------
84
+
85
+ init {
86
+ setBackgroundColor(android.graphics.Color.TRANSPARENT)
87
+ }
88
+
89
+ // ------------------------------------------------------------------
90
+ // Surface warmup
91
+ // ------------------------------------------------------------------
92
+
93
+ /**
94
+ * Eagerly create the Fabric surface so it's mounted and ready before
95
+ * the cell scrolls into view. Called from the overlay factory closure
96
+ * at pool creation time — mirrors iOS ReactCarouselOverlayHost.prepareSurface().
97
+ */
98
+ fun prepareSurface() {
99
+ createSurfaceIfNeeded()
100
+
101
+ // Pre-measure the host and surface view to screen dimensions so they
102
+ // have non-zero size before being attached to a cell. This prevents
103
+ // the black flash that occurs when the host is added at 0x0 and needs
104
+ // an async layout pass to become visible.
105
+ val displayW = context.resources.displayMetrics.widthPixels
106
+ val displayH = context.resources.displayMetrics.heightPixels
107
+ val wSpec = MeasureSpec.makeMeasureSpec(displayW, MeasureSpec.EXACTLY)
108
+ val hSpec = MeasureSpec.makeMeasureSpec(displayH, MeasureSpec.EXACTLY)
109
+ measure(wSpec, hSpec)
110
+ layout(0, 0, displayW, displayH)
111
+ measureAndLayoutSurfaceView(displayW, displayH)
112
+ }
113
+
114
+ // ------------------------------------------------------------------
115
+ // CarouselOverlay — native image cache
116
+ // ------------------------------------------------------------------
117
+
118
+ /** Injected by the SDK's PrefetchManager. Returns a cached Bitmap or null. */
119
+ override var cachedImage: ((String) -> Bitmap?)? = null
120
+
121
+ // ------------------------------------------------------------------
122
+ // CarouselOverlay
123
+ // ------------------------------------------------------------------
124
+
125
+ override fun configure(item: ImageCarouselItem) {
126
+ // Increment generation — any in-flight coroutine from a previous
127
+ // configure() call will see a stale generation and bail out.
128
+ val gen = ++configureGeneration
129
+
130
+ // Cancel any in-flight write job from a previous configure() call.
131
+ pendingWriteJob?.cancel()
132
+
133
+ // Fast path: check for pre-existing temp files synchronously (just
134
+ // file.exists(), no I/O). This avoids the flicker of remote→local
135
+ // URL swap for images that were cached in a previous session.
136
+ val lookup = cachedImage
137
+ val fastItem = if (lookup != null) {
138
+ val fastImages = item.images.map { image ->
139
+ val hash = image.url.hashCode()
140
+ val fileName = "sk-carousel-${kotlin.math.abs(hash)}.jpg"
141
+ val file = File(context.cacheDir, fileName)
142
+ if (file.exists()) CarouselImage(url = "file://${file.absolutePath}", alt = image.alt)
143
+ else image
144
+ }
145
+ ImageCarouselItem(
146
+ id = item.id,
147
+ images = fastImages,
148
+ caption = item.caption,
149
+ title = item.title,
150
+ description = item.description,
151
+ author = item.author,
152
+ section = item.section,
153
+ articleUrl = item.articleUrl,
154
+ )
155
+ } else item
156
+
157
+ val json = Json.encodeToString(fastItem)
158
+ val bundle = Bundle().apply { putString("item", json) }
159
+ applySurfaceProps(bundle)
160
+
161
+ // Pre-size the surface view NOW — before the overlay is attached to a cell.
162
+ val parentView = parent as? android.view.View
163
+ val w = if (width > 0) width
164
+ else if (parentView != null && parentView.width > 0) parentView.width
165
+ else context.resources.displayMetrics.widthPixels
166
+ val h = if (height > 0) height
167
+ else if (parentView != null && parentView.height > 0) parentView.height
168
+ else context.resources.displayMetrics.heightPixels
169
+ measureAndLayoutSurfaceView(w, h)
170
+
171
+ // Background: write any newly-cached images to temp files.
172
+ // Only runs if there are remote URLs remaining (not all were fast-pathed).
173
+ if (lookup == null) return
174
+ val hasRemoteUrls = fastItem.images.any { !it.url.startsWith("file://") }
175
+ if (!hasRemoteUrls) return
176
+
177
+ pendingWriteJob = ioScope.launch {
178
+ val modifiedItem = withContext(Dispatchers.IO) {
179
+ val localImages = item.images.map { image ->
180
+ val bitmap = lookup(image.url)
181
+ if (bitmap != null) {
182
+ val localUrl = writeTempImage(bitmap, image.url)
183
+ if (localUrl != null) CarouselImage(url = localUrl, alt = image.alt)
184
+ else image
185
+ } else {
186
+ image
187
+ }
188
+ }
189
+ ImageCarouselItem(
190
+ id = item.id,
191
+ images = localImages,
192
+ caption = item.caption,
193
+ title = item.title,
194
+ description = item.description,
195
+ author = item.author,
196
+ section = item.section,
197
+ articleUrl = item.articleUrl,
198
+ )
199
+ }
200
+
201
+ // Back on Main — only update if this configure() is still current
202
+ if (gen != configureGeneration) return@launch
203
+ val hasLocalImages = modifiedItem.images.any { it.url.startsWith("file://") }
204
+ if (hasLocalImages) {
205
+ val localJson = Json.encodeToString(modifiedItem)
206
+ val localBundle = Bundle().apply { putString("item", localJson) }
207
+ applySurfaceProps(localBundle)
208
+ }
209
+ }
210
+ }
211
+
212
+ /** Apply props to the surface, handling all lifecycle states. */
213
+ private fun applySurfaceProps(bundle: Bundle) {
214
+ val s = surface
215
+ if (s != null && s.isRunning) {
216
+ (s as? ReactSurfaceImpl)?.updateInitProps(bundle)
217
+ } else if (s != null && !s.isRunning) {
218
+ (s as? ReactSurfaceImpl)?.updateInitProps(bundle)
219
+ s.start()
220
+ } else {
221
+ pendingProps = bundle
222
+ createSurfaceIfNeeded()
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Write a Bitmap to the temp directory as JPEG, returning a file:// URL.
228
+ * Uses a hash of the remote URL as filename to avoid duplicates.
229
+ * Matches iOS ReactCarouselOverlayHost.writeTempImage().
230
+ */
231
+ private fun writeTempImage(bitmap: Bitmap, remoteUrl: String): String? {
232
+ val hash = remoteUrl.hashCode()
233
+ val fileName = "sk-carousel-${kotlin.math.abs(hash)}.jpg"
234
+ val file = File(context.cacheDir, fileName)
235
+
236
+ // Skip if already written
237
+ if (file.exists()) return "file://${file.absolutePath}"
238
+
239
+ return try {
240
+ FileOutputStream(file).use { out ->
241
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
242
+ }
243
+ "file://${file.absolutePath}"
244
+ } catch (e: Exception) {
245
+ android.util.Log.w(TAG, "writeTempImage failed for $remoteUrl", e)
246
+ null
247
+ }
248
+ }
249
+
250
+ override fun resetState() {
251
+ val surfaceImpl = surface as? ReactSurfaceImpl ?: return
252
+ surfaceImpl.updateInitProps(Bundle())
253
+ }
254
+
255
+ override fun fadeOutForTransition() {
256
+ ShortKitBridge.shared?.emitCarouselOverlayEvent(
257
+ "onCarouselOverlayFadeOut",
258
+ Arguments.createMap()
259
+ )
260
+ }
261
+
262
+ override fun restoreFromTransition() {
263
+ ShortKitBridge.shared?.emitCarouselOverlayEvent(
264
+ "onCarouselOverlayRestore",
265
+ Arguments.createMap()
266
+ )
267
+ }
268
+
269
+ // ------------------------------------------------------------------
270
+ // Surface Creation
271
+ // ------------------------------------------------------------------
272
+
273
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
274
+ super.onSizeChanged(w, h, oldw, oldh)
275
+ if (w > 0 && h > 0) {
276
+ measureAndLayoutSurfaceView(w, h)
277
+ }
278
+ }
279
+
280
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
281
+ isInLayoutPass = true
282
+ try {
283
+ super.onLayout(changed, left, top, right, bottom)
284
+ val w = right - left
285
+ val h = bottom - top
286
+ if (w > 0 && h > 0) {
287
+ measureAndLayoutSurfaceView(w, h)
288
+ } else {
289
+ val parentView = parent as? android.view.View
290
+ val pw = parentView?.width ?: 0
291
+ val ph = parentView?.height ?: 0
292
+ if (pw > 0 && ph > 0) {
293
+ measureAndLayoutSurfaceView(pw, ph)
294
+ }
295
+ }
296
+ } finally {
297
+ isInLayoutPass = false
298
+ }
299
+ }
300
+
301
+ private fun measureAndLayoutSurfaceView(w: Int, h: Int) {
302
+ val sv = surface?.view ?: return
303
+ if (sv.width == w && sv.height == h) return
304
+ val wSpec = android.view.View.MeasureSpec.makeMeasureSpec(w, android.view.View.MeasureSpec.EXACTLY)
305
+ val hSpec = android.view.View.MeasureSpec.makeMeasureSpec(h, android.view.View.MeasureSpec.EXACTLY)
306
+ sv.measure(wSpec, hSpec)
307
+ sv.layout(0, 0, w, h)
308
+ }
309
+
310
+ override fun onAttachedToWindow() {
311
+ super.onAttachedToWindow()
312
+
313
+ val parentView = parent as? android.view.View
314
+ if (parentView != null && parentView.width > 0 && parentView.height > 0 && (width == 0 || height == 0)) {
315
+ val wSpec = android.view.View.MeasureSpec.makeMeasureSpec(parentView.width, android.view.View.MeasureSpec.EXACTLY)
316
+ val hSpec = android.view.View.MeasureSpec.makeMeasureSpec(parentView.height, android.view.View.MeasureSpec.EXACTLY)
317
+ measure(wSpec, hSpec)
318
+ layout(0, 0, parentView.width, parentView.height)
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Create the surface object and attach its view, but do NOT start it yet.
324
+ * Called from prepareSurface() to warm the surface ahead of configure().
325
+ */
326
+ private fun createSurfaceIfNeeded() {
327
+ if (surface != null) return
328
+
329
+ val reactHost = (context.applicationContext as? ReactApplication)?.reactHost
330
+ if (reactHost == null) {
331
+ android.util.Log.e(TAG, "createSurface FAILED: reactHost is null")
332
+ return
333
+ }
334
+ val moduleName = "ShortKitCarouselOverlay_$carouselOverlayName"
335
+
336
+ // Pass pending props as initial props so the JS component has data on first render.
337
+ // If called from prepareSurface() (no item yet), pendingProps is null — the surface
338
+ // is created but NOT started. It will be started in configure() with real data.
339
+ val initialProps = pendingProps
340
+ pendingProps = null
341
+
342
+ val newSurface = reactHost.createSurface(context, moduleName, initialProps)
343
+ surface = newSurface
344
+
345
+ newSurface.view?.let { surfaceView ->
346
+ surfaceView.layoutParams = LayoutParams(
347
+ LayoutParams.MATCH_PARENT,
348
+ LayoutParams.MATCH_PARENT
349
+ )
350
+ addView(surfaceView)
351
+ }
352
+
353
+ // Only start if we have item data — avoids rendering a blank frame.
354
+ if (initialProps != null) {
355
+ newSurface.start()
356
+ }
357
+
358
+ // Use parent dimensions if host hasn't been laid out yet
359
+ val parentView = parent as? android.view.View
360
+ val w = if (width > 0) width else parentView?.width ?: 0
361
+ val h = if (height > 0) height else parentView?.height ?: 0
362
+ if (w > 0 && h > 0) {
363
+ measureAndLayoutSurfaceView(w, h)
364
+ }
365
+ }
366
+
367
+ // ------------------------------------------------------------------
368
+ // Cleanup
369
+ // ------------------------------------------------------------------
370
+
371
+ override fun onDetachedFromWindow() {
372
+ super.onDetachedFromWindow()
373
+ pendingWriteJob?.cancel()
374
+ pendingWriteJob = null
375
+ if (surface?.isRunning == true) {
376
+ surface?.stop()
377
+ }
378
+ }
379
+ }
@@ -0,0 +1,40 @@
1
+ package com.shortkit.reactnative
2
+
3
+ import android.content.Context
4
+ import android.widget.FrameLayout
5
+ import com.facebook.react.ReactApplication
6
+ import com.facebook.react.interfaces.fabric.ReactSurface
7
+
8
+ /**
9
+ * A [FrameLayout] that hosts a React Native Fabric surface for the custom
10
+ * loading component. Used as the loading view provider for the ShortKit SDK.
11
+ *
12
+ * Android equivalent of `react_native_sdk/ios/ReactLoadingHost.swift`.
13
+ */
14
+ class ReactLoadingHost(context: Context) : FrameLayout(context) {
15
+
16
+ private var surface: ReactSurface? = null
17
+
18
+ override fun onAttachedToWindow() {
19
+ super.onAttachedToWindow()
20
+ if (surface == null) {
21
+ val reactHost = (context.applicationContext as? ReactApplication)?.reactHost ?: return
22
+ val newSurface = reactHost.createSurface(context, "ShortKitLoading", null)
23
+ newSurface.view?.let { surfaceView ->
24
+ surfaceView.layoutParams = LayoutParams(
25
+ LayoutParams.MATCH_PARENT,
26
+ LayoutParams.MATCH_PARENT
27
+ )
28
+ addView(surfaceView)
29
+ }
30
+ newSurface.start()
31
+ surface = newSurface
32
+ }
33
+ }
34
+
35
+ override fun onDetachedFromWindow() {
36
+ super.onDetachedFromWindow()
37
+ surface?.stop()
38
+ surface = null
39
+ }
40
+ }