@momo-kits/native-kits 0.160.1-beta.9-debug → 0.160.1-container.1-debug

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 (26) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/compose/build.gradle.kts +9 -3
  3. package/compose/build.gradle.kts.backup +8 -2
  4. package/compose/compose.podspec +1 -1
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +7 -1
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +324 -117
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +1 -0
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +11 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +14 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +68 -71
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +62 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +9 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +12 -4
  17. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +33 -0
  18. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +7 -1
  19. package/gradle/libs.versions.toml +2 -0
  20. package/gradle.properties +1 -1
  21. package/ios/StatusBarTap/StatusBarTap.h +13 -0
  22. package/ios/StatusBarTap/StatusBarTap.m +75 -0
  23. package/ios/native-kits.podspec +2 -1
  24. package/local.properties +8 -0
  25. package/package.json +1 -1
  26. package/settings.gradle.kts +15 -3
@@ -8,6 +8,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition
8
8
  import androidx.compose.animation.core.tween
9
9
  import androidx.compose.foundation.background
10
10
  import androidx.compose.foundation.border
11
+ import androidx.compose.ui.draw.alpha
11
12
  import androidx.compose.foundation.clickable
12
13
  import androidx.compose.foundation.layout.Arrangement
13
14
  import androidx.compose.foundation.layout.Box
@@ -131,7 +132,9 @@ fun InputOTP(
131
132
  },
132
133
  decorationBox = { innerTextField ->
133
134
  Box {
134
- Box(modifier = Modifier.size(0.dp)) { innerTextField() }
135
+ Box(
136
+ modifier = Modifier.fillMaxWidth().height(56.dp).alpha(0f)
137
+ ) { innerTextField() }
135
138
  if (floatingValue.isNotEmpty()) {
136
139
  Box(
137
140
  modifier = Modifier.wrapContentSize()
@@ -96,4 +96,5 @@ data class KeyboardOptions(
96
96
  data class ScrollData(
97
97
  val scrollable: Boolean = true,
98
98
  val scrollState: ScrollableState? = null,
99
+ val scrollToTopCallback: (() -> Unit)? = null,
99
100
  )
@@ -3,6 +3,9 @@ package vn.momo.kits.navigation
3
3
  import androidx.compose.animation.*
4
4
  import androidx.compose.animation.core.tween
5
5
  import androidx.compose.runtime.*
6
+ // AI-GENERATED START: import rememberSaveable for stable screen id across nav back-stack save/restore
7
+ import androidx.compose.runtime.saveable.rememberSaveable
8
+ // AI-GENERATED END: import rememberSaveable for stable screen id across nav back-stack save/restore
6
9
  import androidx.compose.ui.unit.Dp
7
10
  import androidx.navigation.compose.NavHost
8
11
  import androidx.navigation.compose.composable
@@ -49,7 +52,11 @@ fun NavigationContainer(
49
52
  }
50
53
  }
51
54
 
52
- val startDestination = DynamicScreenRegistry.register(initialScreenName, initialScreen, options)
55
+ // AI-GENERATED START: keep initial screen id stable with restored nav back stack and refresh its live content
56
+ val screenId = rememberSaveable { DynamicScreenRegistry.nextId() }
57
+ DynamicScreenRegistry.bind(screenId, initialScreenName, initialScreen, options)
58
+ val startDestination = remember(screenId) { DynamicScreenRoute(screenId) }
59
+ // AI-GENERATED END: keep initial screen id stable with restored nav back stack and refresh its live content
53
60
 
54
61
  ProvideNavigationEventDispatcherOwner {
55
62
  CompositionLocalProvider(
@@ -132,6 +139,9 @@ fun NavigationContainer(
132
139
  DisposableEffect(Unit) {
133
140
  onDispose {
134
141
  navigator.dispose()
142
+ // AI-GENERATED START: unregister initial screen on dispose to avoid leaking a stale registry entry
143
+ DynamicScreenRegistry.unregisterScreen(screenId)
144
+ // AI-GENERATED END: unregister initial screen on dispose to avoid leaking a stale registry entry
135
145
  }
136
146
  }
137
147
  }
@@ -196,6 +196,20 @@ object DynamicScreenRegistry {
196
196
  return DynamicScreenRoute(id)
197
197
  }
198
198
 
199
+ // AI-GENERATED START: stable-id registration so a saveable screen id stays in sync with restored nav back stack
200
+ fun nextId(): Int = idCounter++
201
+
202
+ fun bind(id: Int, screenName: String, content: @Composable () -> Unit, options: NavigationOptions?) {
203
+ screens[id] = DynamicScreen(
204
+ id = id,
205
+ name = screenName,
206
+ content = content,
207
+ options = options
208
+ )
209
+ if (id >= idCounter) idCounter = id + 1 // keep counter ahead after process-death restore
210
+ }
211
+ // AI-GENERATED END: stable-id registration so a saveable screen id stays in sync with restored nav back stack
212
+
199
213
  fun unregisterScreen(id: Int) {
200
214
  screens.remove(id)
201
215
  }
@@ -0,0 +1,8 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.runtime.Composable
4
+
5
+ @Composable
6
+ internal expect fun RegisterScrollToTop(callback: (() -> Unit)?)
7
+
8
+
@@ -150,6 +150,10 @@ internal fun StackScreen(
150
150
  val footerHeightPx = remember { mutableIntStateOf(0) }
151
151
  val headerRightWidthPx = remember { mutableIntStateOf(0) }
152
152
 
153
+ if (navigation.options.scrollData.scrollToTopCallback != null) {
154
+ RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
155
+ }
156
+
153
157
  BackHandler(true) { navigator.onBackSafe() }
154
158
 
155
159
  CompositionLocalProvider(
@@ -160,29 +164,26 @@ internal fun StackScreen(
160
164
  LocalFooterHeightPx provides footerHeightPx,
161
165
  LocalHeaderRightWidthPx provides headerRightWidthPx
162
166
  ) {
163
- Box(Modifier
164
- .fillMaxSize()
165
- .background(options.backgroundColor ?: AppTheme.current.colors.background.default)
166
- .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
167
- hideKeyboardOnTap()
168
- }
169
- .conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
170
- imePadding()
171
- }
167
+ Box(
168
+ Modifier
169
+ .fillMaxSize()
170
+ .background(options.backgroundColor ?: AppTheme.current.colors.background.default)
171
+ .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
172
+ hideKeyboardOnTap()
173
+ }
174
+ .conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
175
+ imePadding()
176
+ }
172
177
  ) {
173
178
  Box(Modifier.zIndex(1f)) {
174
179
  HeaderBackground()
175
180
  }
176
181
 
177
- Box(Modifier.zIndex(1.5f)) {
178
- AnimatedHeaderImage()
179
- }
180
-
181
182
  Box(Modifier.zIndex(5f)) {
182
183
  Header(onBackHandler)
183
184
  }
184
185
 
185
- Column (Modifier.zIndex(6f)) {
186
+ Column(Modifier.zIndex(6f)) {
186
187
  SearchAnimated(isScrollInProgress = scrollInProcess)
187
188
  }
188
189
 
@@ -194,7 +195,7 @@ internal fun StackScreen(
194
195
  FooterContent()
195
196
  }
196
197
 
197
- Box(Modifier.zIndex(7f)){
198
+ Box(Modifier.zIndex(7f)) {
198
199
  FloatingContent()
199
200
  }
200
201
 
@@ -204,15 +205,15 @@ internal fun StackScreen(
204
205
  }
205
206
 
206
207
  @Composable
207
- fun FloatingContent(){
208
+ fun FloatingContent() {
208
209
  val options = LocalOptions.current
209
210
  val density = LocalDensity.current
210
211
  val footerHeightPx = LocalFooterHeightPx.current
211
212
  val scrollState = LocalScrollState.current
212
213
 
213
214
  val fabProps = options.floatingButtonProps ?: return
214
- val bottomPadding = fabProps.bottom ?:
215
- (Spacing.M + if (options.footerComponent != null) with(density){ footerHeightPx.intValue.toDp() } else 0.dp)
215
+ val bottomPadding = fabProps.bottom
216
+ ?: (Spacing.M + if (options.footerComponent != null) with(density) { footerHeightPx.intValue.toDp() } else 0.dp)
216
217
 
217
218
  FloatingButton(
218
219
  scrollPosition = fabProps.scrollState?.value ?: scrollState.value,
@@ -228,17 +229,19 @@ fun FloatingContent(){
228
229
  }
229
230
 
230
231
  @Composable
231
- fun ColumnScope.MainContent(content: @Composable ()-> Unit){
232
+ fun ColumnScope.MainContent(content: @Composable () -> Unit) {
232
233
  val options = LocalOptions.current
233
234
  val inputSearchType = getInputSearchType(options)
234
235
  val density = LocalDensity.current
235
236
  val scrollState = LocalScrollState.current
236
237
 
237
- Spacer(Modifier.height(
238
- if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent) || options.headerType is HeaderType.Animated)
239
- AppStatusBar.current + HEADER_HEIGHT.dp else 0.dp)
238
+ Spacer(
239
+ Modifier.height(
240
+ if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
241
+ AppStatusBar.current + HEADER_HEIGHT.dp else 0.dp
242
+ )
240
243
  )
241
- if (inputSearchType == InputSearchType.Animated){
244
+ if (inputSearchType == InputSearchType.Animated) {
242
245
  val scrollDp = with(density) { scrollState.value.toDp() }
243
246
 
244
247
  val animatedTopPadding by animateDpAsState(
@@ -247,17 +250,18 @@ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
247
250
  )
248
251
  Spacer(Modifier.height(animatedTopPadding))
249
252
  }
250
- Column (Modifier
251
- .fillMaxWidth()
252
- .weight(1f)
253
- .conditional(options.scrollData.scrollable && options.scrollData.scrollState !is LazyListState) {
254
- verticalScroll(scrollState)
255
- }
253
+ Column(
254
+ Modifier
255
+ .fillMaxWidth()
256
+ .weight(1f)
257
+ .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
258
+ verticalScroll(scrollState)
259
+ }
256
260
  ) {
257
261
  ScreenContent(content = content)
258
262
  }
259
263
 
260
- if (options.footerComponent != null){
264
+ if (options.footerComponent != null) {
261
265
  val footerHeight = LocalFooterHeightPx.current
262
266
  val footerHeightDp = with(density) { footerHeight.value.toDp() }
263
267
  Spacer(Modifier.height(footerHeightDp))
@@ -265,14 +269,19 @@ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
265
269
  }
266
270
 
267
271
  @Composable
268
- fun ScreenContent(content: @Composable () -> Unit){
272
+ fun ScreenContent(content: @Composable () -> Unit) {
273
+ val scrollState = LocalScrollState.current
269
274
  val options = LocalOptions.current
270
275
 
271
- if (options.headerType is HeaderType.Animated){
276
+ if (options.headerType is HeaderType.Animated) {
272
277
  val animatedHeader = options.headerType
273
- Column {
274
- Spacer(Modifier.height(animatedHeader.layoutOffSet))
275
- content()
278
+ Box {
279
+ Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)) {
280
+ animatedHeader.composable.invoke(scrollState.value)
281
+ }
282
+ Box(Modifier.offset(x = 0.dp, y = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)) {
283
+ content()
284
+ }
276
285
  }
277
286
  } else {
278
287
  content()
@@ -280,27 +289,9 @@ fun ScreenContent(content: @Composable () -> Unit){
280
289
  }
281
290
 
282
291
  @Composable
283
- fun AnimatedHeaderImage() {
284
- val options = LocalOptions.current
285
- val scrollState = LocalScrollState.current
286
- val animatedHeader = options.headerType as? HeaderType.Animated ?: return
287
- val density = LocalDensity.current
288
- val scrollDp = with(density) { scrollState.value.toDp() }
289
-
290
- Box(
291
- Modifier
292
- .fillMaxWidth()
293
- .offset(y = -scrollDp)
294
- .aspectRatio(animatedHeader.aspectRatio.value)
295
- ) {
296
- animatedHeader.composable.invoke(scrollState.value)
297
- }
298
- }
299
-
300
- @Composable
301
- fun FooterContent(){
292
+ fun FooterContent() {
302
293
  val options = LocalOptions.current
303
- if (options.footerComponent != null){
294
+ if (options.footerComponent != null) {
304
295
  val ime = WindowInsets.ime
305
296
  val density = LocalDensity.current
306
297
  val imeBottom = ime.getBottom(density)
@@ -312,11 +303,11 @@ fun FooterContent(){
312
303
  }
313
304
 
314
305
  @Composable
315
- fun OverplayView(bottomTabIndex: Int, id: Int){
306
+ fun OverplayView(bottomTabIndex: Int, id: Int) {
316
307
  val overplayType = OverplayComponentRegistry.getOverplayType()
317
308
 
318
309
  if (overplayType != null) {
319
- Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 8f).fillMaxSize()){
310
+ Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 8f).fillMaxSize()) {
320
311
  if (bottomTabIndex != -1) return@Box
321
312
  if (OverplayComponentRegistry.currentRootId() != id) return@Box
322
313
  OverplayComponentRegistry.OverlayComponent()
@@ -339,22 +330,24 @@ fun Footer(footerComponent: @Composable (() -> Unit)?, bottomPadding: Dp) {
339
330
  Box(Modifier.onGloballyPositioned {
340
331
  if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
341
332
  }) {
342
- Box(Modifier
343
- .fillMaxWidth()
344
- .background(AppTheme.current.colors.background.surface)
345
- .conditional(IsShowBaseLineDebug) {
346
- border(1.dp, Colors.blue_03)
347
- }
348
- .padding(top = Spacing.S, start = Spacing.M, end = Spacing.M, bottom = bottomPadding + Spacing.S)
349
- ){
333
+ Box(
334
+ Modifier
335
+ .fillMaxWidth()
336
+ .background(AppTheme.current.colors.background.surface)
337
+ .conditional(IsShowBaseLineDebug) {
338
+ border(1.dp, Colors.blue_03)
339
+ }
340
+ .padding(top = Spacing.S, start = Spacing.M, end = Spacing.M, bottom = bottomPadding + Spacing.S)
341
+ ) {
350
342
  footerComponent.invoke()
351
343
  }
352
344
 
353
- Box(modifier = Modifier
354
- .fillMaxWidth()
355
- .height(6.dp)
356
- .offset(x = 0.dp, y = (-6).dp)
357
- .background(shadowBrush)
345
+ Box(
346
+ modifier = Modifier
347
+ .fillMaxWidth()
348
+ .height(6.dp)
349
+ .offset(x = 0.dp, y = (-6).dp)
350
+ .background(shadowBrush)
358
351
  )
359
352
  }
360
353
  }
@@ -364,6 +357,7 @@ data class InputSearchLayoutParams(
364
357
  val startPadding: Dp,
365
358
  val endPadding: Dp
366
359
  )
360
+
367
361
  @Composable
368
362
  fun SearchAnimated(
369
363
  isScrollInProgress: Boolean
@@ -478,6 +472,7 @@ internal fun isKeyboardVisible(): Boolean {
478
472
  val density = LocalDensity.current
479
473
  return ime.getBottom(density) > 0
480
474
  }
475
+
481
476
  private fun quantize(value: Int, stepPx: Int): Int {
482
477
  if (stepPx <= 1) return value
483
478
  return (value / stepPx) * stepPx
@@ -557,13 +552,15 @@ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error(
557
552
 
558
553
  val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
559
554
 
560
- internal fun getInputSearchType(options: NavigationOptions): InputSearchType{
555
+ internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
561
556
  return when (val headerType = options.headerType) {
562
557
  is HeaderType.DefaultOrExtended -> when {
563
558
  headerType.inputSearchProps == null -> InputSearchType.None
564
559
  headerType.useAnimated -> InputSearchType.Animated
565
560
  else -> InputSearchType.Header
566
561
  }
562
+
567
563
  else -> InputSearchType.None
568
564
  }
569
565
  }
566
+
@@ -0,0 +1,62 @@
1
+ package vn.momo.kits.platform
2
+
3
+ import androidx.compose.foundation.Image
4
+ import androidx.compose.foundation.background
5
+ import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.fillMaxSize
7
+ import androidx.compose.runtime.Composable
8
+ import androidx.compose.runtime.getValue
9
+ import androidx.compose.ui.Modifier
10
+ import androidx.compose.ui.graphics.Color
11
+ import io.github.alexzhirkevich.compottie.Compottie
12
+ import io.github.alexzhirkevich.compottie.ExperimentalCompottieApi
13
+ import io.github.alexzhirkevich.compottie.LottieCompositionSpec
14
+ import io.github.alexzhirkevich.compottie.dynamic.rememberLottieDynamicProperties
15
+ import io.github.alexzhirkevich.compottie.rememberLottieComposition
16
+ import io.github.alexzhirkevich.compottie.rememberLottiePainter
17
+ import vn.momo.kits.utils.readJson
18
+
19
+ @OptIn(ExperimentalCompottieApi::class)
20
+ @Composable
21
+ internal fun ComposeLottieAnimation(
22
+ path: String,
23
+ tintColor: Color?,
24
+ bgColor: Color?,
25
+ modifier: Modifier,
26
+ ) {
27
+ val json = readJson(path)
28
+
29
+ if (json.isEmpty()) {
30
+ Box(modifier.background(bgColor ?: Color.Transparent))
31
+ return
32
+ }
33
+
34
+ val composition by rememberLottieComposition(json) {
35
+ LottieCompositionSpec.JsonString(json)
36
+ }
37
+
38
+ // Recolor only the fill/stroke color slots (matching the native lottie-ios per-keypath tint)
39
+ // instead of a global ColorFilter.tint, which would flatten a multicolor animation to a silhouette.
40
+ val dynamicProperties = if (tintColor != null) {
41
+ rememberLottieDynamicProperties(tintColor) {
42
+ shapeLayer("**") {
43
+ fill("**") { color { tintColor } }
44
+ stroke("**") { color { tintColor } }
45
+ }
46
+ }
47
+ } else {
48
+ null
49
+ }
50
+
51
+ Box(modifier.background(bgColor ?: Color.Transparent)) {
52
+ Image(
53
+ painter = rememberLottiePainter(
54
+ composition = composition,
55
+ iterations = Compottie.IterateForever,
56
+ dynamicProperties = dynamicProperties,
57
+ ),
58
+ contentDescription = null,
59
+ modifier = Modifier.fillMaxSize(),
60
+ )
61
+ }
62
+ }
@@ -44,13 +44,21 @@ fun supportsImePadding(): Boolean = when (val v = getOSVersion()) {
44
44
  is OSVersion.IOS -> true
45
45
  }
46
46
 
47
+ /**
48
+ * @param useCompose when `true`, renders via the pure-Compose Compottie engine instead of the
49
+ * platform-native engine (airbnb-lottie on Android, lottie-ios on iOS). Defaults to `false` for
50
+ * backward compatibility. Note: when `useCompose = true`, `tintColor` recolors only the fill/stroke
51
+ * color slots (matching the native lottie-ios per-keypath tint), so gradient and text-layer colors
52
+ * are left untinted; `placedAsOverlay` is a no-op.
53
+ */
47
54
  @Composable
48
55
  expect fun LottieAnimation(
49
56
  path: String,
50
57
  tintColor: Color? = null,
51
58
  bgColor: Color? = null,
52
59
  placedAsOverlay: Boolean = false,
53
- modifier: Modifier = Modifier
60
+ modifier: Modifier = Modifier,
61
+ useCompose: Boolean = false
54
62
  )
55
63
 
56
64
  expect fun NativePaint.setColor(
@@ -24,13 +24,21 @@ fun getResource(name: String): DrawableResource {
24
24
  @Composable
25
25
  fun readJson(name: String): String {
26
26
  val path = name.plus(".json")
27
+ val candidatePaths = listOf(
28
+ path,
29
+ "composeResources/vn.momo.compose.resources/$path",
30
+ )
27
31
 
28
32
  val jsonContent by rememberState(path, { "" }) {
29
33
  val cached = resourceCache.getOrPut(path) {
30
- val content = try {
31
- readResourceBytes(path).decodeToString()
32
- } catch (_: Exception) {
33
- ""
34
+ var content = ""
35
+ for (candidate in candidatePaths) {
36
+ try {
37
+ content = readResourceBytes(candidate).decodeToString()
38
+ break
39
+ } catch (_: Exception) {
40
+ // try next candidate
41
+ }
34
42
  }
35
43
  ResourceCache.JSON(content)
36
44
  } as ResourceCache.JSON
@@ -0,0 +1,33 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.DisposableEffect
5
+ import platform.Foundation.NSNotification
6
+ import platform.Foundation.NSNotificationCenter
7
+ import platform.Foundation.NSOperationQueue
8
+ import platform.darwin.NSObjectProtocol
9
+
10
+ private const val STATUS_BAR_TAPPED_NOTIFICATION = "statusBarSelected"
11
+
12
+ @Composable
13
+ internal actual fun RegisterScrollToTop(callback: (() -> Unit)?) {
14
+ val maxApi = LocalMaxApi.current
15
+ DisposableEffect(callback) {
16
+ val cb = callback
17
+ val token: NSObjectProtocol? = if (cb != null) {
18
+ NSNotificationCenter.defaultCenter.addObserverForName(
19
+ name = STATUS_BAR_TAPPED_NOTIFICATION,
20
+ `object` = null,
21
+ queue = NSOperationQueue.mainQueue,
22
+ ) { _: NSNotification? ->
23
+ runCatching { cb() }.onFailure {
24
+ maxApi?.logFile("[ScrollToTop] callback threw: ${it.message}") {}
25
+
26
+ }
27
+ }
28
+ } else null
29
+ onDispose {
30
+ token?.let { NSNotificationCenter.defaultCenter.removeObserver(it) }
31
+ }
32
+ }
33
+ }
@@ -98,8 +98,14 @@ actual fun LottieAnimation(
98
98
  tintColor: Color?,
99
99
  bgColor: Color?,
100
100
  placedAsOverlay: Boolean,
101
- modifier: Modifier
101
+ modifier: Modifier,
102
+ useCompose: Boolean
102
103
  ) {
104
+ if (useCompose) {
105
+ ComposeLottieAnimation(path, tintColor, bgColor, modifier)
106
+ return
107
+ }
108
+
103
109
  var animation by remember { mutableStateOf<CompatibleAnimation?>(null) }
104
110
 
105
111
  LaunchedEffect(Unit) {
@@ -13,6 +13,7 @@ androidGradlePlugin = "8.13.2"
13
13
  r8 = "8.9.35"
14
14
  kotlinx-datetime = "0.7.1"
15
15
  airbnb-lottie = "5.2.0"
16
+ compottie = "2.2.0"
16
17
  androidx-activity = "1.9.1"
17
18
  androidx-appcompat = "1.7.0"
18
19
  material = "1.10.0"
@@ -38,6 +39,7 @@ coil-multiplatform-compose = { module = "io.coil-kt.coil3:coil-compose", version
38
39
  coil-multiplatform-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil3-multiplatform" }
39
40
  r8 = { module = "com.android.tools:r8", version.ref = "r8" }
40
41
  airbnb-lottie = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "airbnb-lottie" }
42
+ compottie = { module = "io.github.alexzhirkevich:compottie", version.ref = "compottie" }
41
43
  native-max-api = { group = "vn.momo.maxapi", name = "NativeMaxApi", version.ref = "nativemaxapi" }
42
44
  kits = { module = "vn.momo.kits:kits", version.ref = "kits" }
43
45
  androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
package/gradle.properties CHANGED
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.159.1-beta.16
21
+ version=0.160.1-container.1
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
@@ -0,0 +1,13 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /// Posts `MoMoStatusBarTap.notificationName` on every status-bar tap.
6
+ /// The swizzle installs automatically at framework load (`+load`) — calling
7
+ /// `install` is a no-op kept for explicit/manual triggering.
8
+ @interface MoMoStatusBarTap : NSObject
9
+ @property (class, nonatomic, readonly) NSNotificationName notificationName;
10
+ + (void)install;
11
+ @end
12
+
13
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,75 @@
1
+ #import <UIKit/UIKit.h>
2
+ #import <objc/message.h>
3
+ #import <objc/runtime.h>
4
+ #import "StatusBarTap.h"
5
+
6
+ // Performs the actual swizzle once per process. Safe to call multiple times.
7
+ static void mm_installStatusBarTapSwizzleOnce(void) {
8
+ static dispatch_once_t onceToken;
9
+ dispatch_once(&onceToken, ^{
10
+ Class cls = [UIStatusBarManager class];
11
+ SEL originalSelector = NSSelectorFromString(@"handleTapAction:");
12
+ SEL swizzledSelector = NSSelectorFromString(@"mm_handleTapAction:");
13
+
14
+ Method originalMethod = class_getInstanceMethod(cls, originalSelector);
15
+ Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
16
+ if (!originalMethod || !swizzledMethod) return;
17
+
18
+ // Verify the original method matches the (id) -> void shape we expect.
19
+ // Encoding may include byte offsets (e.g. "v32@0:8@16") so we filter
20
+ // digits out before comparing.
21
+ const char *encPtr = method_getTypeEncoding(originalMethod);
22
+ if (encPtr) {
23
+ NSMutableString *enc = [NSMutableString string];
24
+ for (const char *p = encPtr; *p; p++) {
25
+ if (!(*p >= '0' && *p <= '9')) [enc appendFormat:@"%c", *p];
26
+ }
27
+ if (![enc isEqualToString:@"v@:@"]) return;
28
+ }
29
+
30
+ BOOL didAdd = class_addMethod(cls,
31
+ originalSelector,
32
+ method_getImplementation(swizzledMethod),
33
+ method_getTypeEncoding(swizzledMethod));
34
+ if (didAdd) {
35
+ class_replaceMethod(cls,
36
+ swizzledSelector,
37
+ method_getImplementation(originalMethod),
38
+ method_getTypeEncoding(originalMethod));
39
+ } else {
40
+ method_exchangeImplementations(originalMethod, swizzledMethod);
41
+ }
42
+ });
43
+ }
44
+
45
+ @implementation MoMoStatusBarTap
46
+ + (NSNotificationName)notificationName { return @"statusBarSelected"; }
47
+ + (void)install { mm_installStatusBarTapSwizzleOnce(); }
48
+ @end
49
+
50
+ // MARK: - Swizzle target on UIStatusBarManager
51
+ // `mm_handleTapAction:` is the replacement IMP. After the add/exchange,
52
+ // this selector points at the original UIKit IMP — calling it inside
53
+ // the body invokes UIKit's real handler so UIScrollView.scrollsToTop
54
+ // and any other side effects continue to work.
55
+
56
+ @interface UIStatusBarManager (MoMoStatusBarTap)
57
+ @end
58
+
59
+ @implementation UIStatusBarManager (MoMoStatusBarTap)
60
+
61
+ // Auto-install at framework load. Commented out so the swizzle stays
62
+ // opt-in via `[MoMoStatusBarTap install]`. Uncomment to revert to
63
+ // auto-installation before main().
64
+ //
65
+ // + (void)load {
66
+ // mm_installStatusBarTapSwizzleOnce();
67
+ // }
68
+
69
+ - (void)mm_handleTapAction:(id)action {
70
+ [self mm_handleTapAction:action];
71
+ [[NSNotificationCenter defaultCenter] postNotificationName:MoMoStatusBarTap.notificationName
72
+ object:nil];
73
+ }
74
+
75
+ @end
@@ -10,7 +10,8 @@ Pod::Spec.new do |s|
10
10
  s.ios.deployment_target = '15.0'
11
11
  s.swift_version = '5.0'
12
12
 
13
- s.source_files = "**/*.swift"
13
+ s.source_files = "**/*.{swift,m,h}"
14
+ s.public_header_files = "StatusBarTap/*.h"
14
15
  s.framework = 'SwiftUI', 'Combine'
15
16
  s.dependency 'SDWebImageSwiftUI'
16
17
  s.dependency 'lottie-ios'
@@ -0,0 +1,8 @@
1
+ ## This file must *NOT* be checked into Version Control Systems,
2
+ # as it contains information specific to your local configuration.
3
+ #
4
+ # Location of the SDK. This is only used by Gradle.
5
+ # For customization when using a Version Control System, please read the
6
+ # header note.
7
+ #Tue May 19 15:41:04 GMT+07:00 2026
8
+ sdk.dir=/Users/phuc/Library/Android/sdk
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.160.1-beta.9-debug",
3
+ "version": "0.160.1-container.1-debug",
4
4
  "private": false,
5
5
  "dependencies": {},
6
6
  "devDependencies": {},