@momo-kits/native-kits 0.168.1-beta.4-debug → 0.168.1-beta.5-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 (31) hide show
  1. package/CLAUDE.md +4 -4
  2. package/README.md +2 -10
  3. package/build.gradle.kts +1 -3
  4. package/compose/build.gradle.kts +20 -19
  5. package/compose/build.gradle.kts.backup +19 -18
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +14 -23
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +5 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +14 -24
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +1 -6
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +1 -6
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +1 -6
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +2 -3
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +124 -59
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +30 -44
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +32 -40
  17. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +2 -6
  18. package/gradle/libs.versions.toml +4 -11
  19. package/gradle/wrapper/gradle-wrapper.properties +1 -3
  20. package/gradle.properties +3 -3
  21. package/ios/Layout/ScreenLayout.swift +5 -0
  22. package/ios/Navigation/BottomTab/DuoBarCompression.swift +2 -3
  23. package/ios/Navigation/BottomTab/DuoTabBar.swift +94 -69
  24. package/ios/Navigation/Component/HeaderBackground.swift +22 -1
  25. package/ios/Navigation/StackScreen.swift +3 -7
  26. package/package.json +1 -1
  27. package/publish.sh +2 -33
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +0 -37
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +0 -41
  30. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +0 -44
  31. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +0 -62
@@ -2,8 +2,12 @@ package vn.momo.kits.navigation.bottomtab
2
2
 
3
3
  import androidx.compose.animation.core.Spring
4
4
  import androidx.compose.animation.core.animateDpAsState
5
+ import androidx.compose.animation.core.animateFloatAsState
5
6
  import androidx.compose.animation.core.spring
6
7
  import androidx.compose.foundation.background
8
+ import androidx.compose.foundation.clickable
9
+ import androidx.compose.foundation.interaction.MutableInteractionSource
10
+ import androidx.compose.foundation.interaction.collectIsPressedAsState
7
11
  import androidx.compose.foundation.border
8
12
  import androidx.compose.foundation.layout.Arrangement
9
13
  import androidx.compose.foundation.layout.Box
@@ -15,6 +19,7 @@ import androidx.compose.foundation.layout.RowScope
15
19
  import androidx.compose.foundation.layout.absolutePadding
16
20
  import androidx.compose.foundation.layout.fillMaxHeight
17
21
  import androidx.compose.foundation.layout.fillMaxSize
22
+ import androidx.compose.foundation.layout.fillMaxWidth
18
23
  import androidx.compose.foundation.layout.height
19
24
  import androidx.compose.foundation.layout.offset
20
25
  import androidx.compose.foundation.layout.padding
@@ -23,25 +28,33 @@ import androidx.compose.foundation.layout.width
23
28
  import androidx.compose.foundation.shape.CircleShape
24
29
  import androidx.compose.foundation.shape.RoundedCornerShape
25
30
  import androidx.compose.runtime.Composable
31
+ import androidx.compose.runtime.CompositionLocalProvider
32
+ import androidx.compose.runtime.LaunchedEffect
33
+ import androidx.compose.runtime.MutableState
26
34
  import androidx.compose.runtime.compositionLocalOf
27
35
  import androidx.compose.runtime.getValue
36
+ import androidx.compose.runtime.mutableStateOf
37
+ import androidx.compose.runtime.remember
28
38
  import androidx.compose.ui.AbsoluteAlignment
29
39
  import androidx.compose.ui.Alignment
30
40
  import androidx.compose.ui.Modifier
31
41
  import androidx.compose.ui.draw.clip
32
42
  import androidx.compose.ui.draw.shadow
43
+ import androidx.compose.ui.graphics.graphicsLayer
33
44
  import androidx.compose.ui.graphics.Color
34
45
  import androidx.compose.ui.graphics.Shape
46
+ import androidx.compose.ui.semantics.Role
35
47
  import androidx.compose.ui.text.style.TextOverflow
36
48
  import androidx.compose.ui.unit.Dp
37
49
  import androidx.compose.ui.unit.dp
50
+ import vn.momo.kits.application.LocalContext
51
+ import vn.momo.kits.application.MiniAppContext
38
52
  import vn.momo.kits.components.Badge
39
53
  import vn.momo.kits.components.BadgeDot
40
54
  import vn.momo.kits.components.DotSize
41
55
  import vn.momo.kits.components.Icon
42
56
  import vn.momo.kits.components.Text
43
57
  import vn.momo.kits.const.AppTheme
44
- import vn.momo.kits.const.Radius
45
58
  import vn.momo.kits.const.Spacing
46
59
  import vn.momo.kits.const.Typography
47
60
  import vn.momo.kits.layout.DuoMetrics
@@ -59,14 +72,18 @@ private val RAIL_GAP = Spacing.XXS
59
72
  // selection bubble overflows the 48 rail cell by 5 above and below
60
73
  private val RAIL_BUBBLE_WIDTH = 44.dp
61
74
  private val RAIL_BUBBLE_HEIGHT = 58.dp
62
- private val FLOATING_HEIGHT = 62.dp
75
+ private val FLOATING_HEIGHT = DuoMetrics.floatingTabBarHeight
63
76
  private val FLOATING_PADDING = Spacing.XS
64
- private val FLOATING_QR_SIZE = 44.dp
65
- // floating bar spans 6 of the 8 portrait grid columns, like the content
66
- private const val GRID_COLUMNS = 8
67
- private const val GRID_SPAN = 6
77
+ // the system bar sizes its pill to the tabs instead of stretching it across the display
78
+ private val FLOATING_ITEM_WIDTH = 80.dp
79
+ // while pressed the selection bubble grows into a lens past the pill's edges
80
+ private const val LENS_SCALE_X = 1.2f
81
+ private const val LENS_SCALE_Y = 1.3f
82
+ // Figma Home-2026 tab bar "Selection": brand pink at 8%
83
+ private const val SELECTION_TINT_ALPHA = 0x14 / 255f
68
84
  // RN reanimated spring { damping: 18, stiffness: 220, mass: 0.8 }
69
85
  private val BUBBLE_SPRING = spring<Dp>(dampingRatio = 0.68f, stiffness = 275f, visibilityThreshold = Dp(0.5f))
86
+ private val LENS_SPRING = spring<Float>(dampingRatio = 0.68f, stiffness = 275f)
70
87
 
71
88
  // Space from the window bottom the rail stack and its gap take, so the header side bar stays above it.
72
89
  internal val LocalDuoRailReserve = compositionLocalOf { 0.dp }
@@ -78,11 +95,15 @@ internal fun duoRailHeight(itemCount: Int, hasQr: Boolean, compressed: Boolean =
78
95
  return rail + if (hasQr) Spacing.M + ITEM_SIZE else 0.dp
79
96
  }
80
97
 
98
+ // Overlaps the home indicator inset like the system tab bar (21pt above the bottom with a 34pt inset).
99
+ private fun floatingTabBarBottom(insetBottom: Dp): Dp =
100
+ maxOf(insetBottom - DuoMetrics.floatingTabBarInsetOverlap, Spacing.S)
101
+
81
102
  // Space tab content leaves under the floating bar; 0 when the rail sits in the side strip or off Duo.
82
103
  internal fun duoFloatingTabBarInset(layout: ScreenLayout): Dp =
83
- if (layout.isIPhoneDuo && layout.sideBar == null) FLOATING_HEIGHT + Spacing.M + layout.insetBottom else 0.dp
104
+ if (layout.isIPhoneDuo && layout.sideBar == null) FLOATING_HEIGHT + floatingTabBarBottom(layout.insetBottom) else 0.dp
84
105
 
85
- // iPhone Duo tab bar: glass rail in the side bar, or a floating glass bar when unfolded portrait.
106
+ // iPhone Duo tab bar: rail in the side bar, or a floating bar when unfolded portrait.
86
107
  @Composable
87
108
  internal fun BoxScope.DuoTabBar(
88
109
  layout: ScreenLayout,
@@ -147,18 +168,19 @@ private fun BoxScope.DuoRailTabBar(
147
168
  }
148
169
  }
149
170
 
150
- // Glass column of tab items with the sliding selection bubble.
171
+ // Column of icon-only tab items with the sliding selection bubble.
151
172
  @Composable
152
173
  private fun Rail(items: List<BottomTabItem>, selectedIndex: Int, onItemClick: (Int) -> Unit) {
174
+ val pressedIndex = remember { mutableStateOf<Int?>(null) }
153
175
  val bubbleOffset by animateDpAsState(
154
- targetValue = (ITEM_SIZE + RAIL_GAP) * selectedIndex.coerceAtLeast(0),
176
+ targetValue = (ITEM_SIZE + RAIL_GAP) * (pressedIndex.value ?: selectedIndex).coerceAtLeast(0),
155
177
  animationSpec = BUBBLE_SPRING,
156
178
  label = "DuoRailBubble",
157
179
  )
158
180
  Box(
159
181
  Modifier
160
182
  .width(ITEM_SIZE)
161
- .tabBarMaterial(RoundedCornerShape(Radius.XL))
183
+ .tabBarMaterial(CircleShape)
162
184
  .setAutomationId("bottom_tab_bar")
163
185
  ) {
164
186
  Box(
@@ -168,7 +190,7 @@ private fun Rail(items: List<BottomTabItem>, selectedIndex: Int, onItemClick: (I
168
190
  y = RAIL_PADDING - (RAIL_BUBBLE_HEIGHT - ITEM_SIZE) / 2 + bubbleOffset,
169
191
  )
170
192
  .size(RAIL_BUBBLE_WIDTH, RAIL_BUBBLE_HEIGHT)
171
- .background(AppTheme.current.colors.background.selected, CircleShape)
193
+ .selectionLens(pressedIndex.value != null)
172
194
  )
173
195
  Column(
174
196
  modifier = Modifier.padding(vertical = RAIL_PADDING),
@@ -178,7 +200,7 @@ private fun Rail(items: List<BottomTabItem>, selectedIndex: Int, onItemClick: (I
178
200
  Box(
179
201
  modifier = Modifier
180
202
  .size(ITEM_SIZE)
181
- .noFeedbackClickable { onItemClick(index) },
203
+ .tabClickable(index, pressedIndex) { onItemClick(index) },
182
204
  contentAlignment = Alignment.Center,
183
205
  ) {
184
206
  DuoTabIcon(item, selected = index == selectedIndex)
@@ -188,6 +210,7 @@ private fun Rail(items: List<BottomTabItem>, selectedIndex: Int, onItemClick: (I
188
210
  }
189
211
  }
190
212
 
213
+ // Floating labelled bar for the unfolded portrait display, QR pinned in its own platter beside it.
191
214
  @Composable
192
215
  private fun BoxScope.DuoFloatingTabBar(
193
216
  layout: ScreenLayout,
@@ -196,73 +219,115 @@ private fun BoxScope.DuoFloatingTabBar(
196
219
  selectedIndex: Int,
197
220
  onTabSelected: (Int) -> Unit,
198
221
  ) {
199
- val columnWidth = (layout.width - Spacing.M * 2 - Spacing.M * (GRID_COLUMNS - 1)) / GRID_COLUMNS
200
- val barWidth = columnWidth * GRID_SPAN + Spacing.M * (GRID_SPAN - 1)
201
- val useQr = floatingButton != null && items.size % 2 == 0
202
- val middle = items.size / 2
203
- val cellCount = items.size + if (useQr) 1 else 0
204
- val visualIndex = if (useQr && selectedIndex >= middle) selectedIndex + 1 else selectedIndex
222
+ val pressedIndex = remember { mutableStateOf<Int?>(null) }
205
223
 
206
- Box(
224
+ Row(
207
225
  modifier = Modifier
208
226
  .align(Alignment.BottomCenter)
209
- .padding(bottom = layout.insetBottom + Spacing.M)
210
- .width(barWidth)
211
- .height(FLOATING_HEIGHT)
212
- .tabBarMaterial(RoundedCornerShape(FLOATING_HEIGHT / 2))
213
- .padding(FLOATING_PADDING)
214
- .setAutomationId("bottom_tab_bar")
215
- ) {
216
- BoxWithConstraints(Modifier.fillMaxSize()) {
217
- val cellWidth = maxWidth / cellCount
218
- val bubbleOffset by animateDpAsState(
219
- targetValue = cellWidth * visualIndex.coerceAtLeast(0),
220
- animationSpec = BUBBLE_SPRING,
221
- label = "DuoFloatingBubble",
227
+ .fillMaxWidth()
228
+ .absolutePadding(
229
+ left = layout.insetLeft + DuoMetrics.floatingTabBarMargin,
230
+ right = layout.insetRight + DuoMetrics.floatingTabBarMargin,
222
231
  )
223
- Box(
224
- Modifier
225
- .offset(x = bubbleOffset)
226
- .width(cellWidth)
227
- .fillMaxHeight()
228
- .background(AppTheme.current.colors.background.selected, CircleShape)
229
- )
230
- Row(Modifier.fillMaxSize()) {
231
- items.forEachIndexed { index, item ->
232
- if (useQr && index == middle) {
233
- Box(Modifier.weight(1f).fillMaxHeight(), contentAlignment = Alignment.Center) {
234
- QrButton(floatingButton, size = FLOATING_QR_SIZE)
232
+ .padding(bottom = floatingTabBarBottom(layout.insetBottom)),
233
+ horizontalArrangement = Arrangement.spacedBy(Spacing.S, Alignment.CenterHorizontally),
234
+ ) {
235
+ Box(
236
+ modifier = Modifier
237
+ .weight(1f, fill = false)
238
+ .width(FLOATING_ITEM_WIDTH * items.size + FLOATING_PADDING * 2)
239
+ .height(FLOATING_HEIGHT)
240
+ .tabBarMaterial(RoundedCornerShape(FLOATING_HEIGHT / 2))
241
+ .padding(FLOATING_PADDING)
242
+ .setAutomationId("bottom_tab_bar")
243
+ ) {
244
+ BoxWithConstraints(Modifier.fillMaxSize()) {
245
+ val cellWidth = maxWidth / items.size.coerceAtLeast(1)
246
+ val bubbleOffset by animateDpAsState(
247
+ targetValue = cellWidth * (pressedIndex.value ?: selectedIndex).coerceAtLeast(0),
248
+ animationSpec = BUBBLE_SPRING,
249
+ label = "DuoFloatingBubble",
250
+ )
251
+ Box(
252
+ Modifier
253
+ .offset(x = bubbleOffset)
254
+ .width(cellWidth)
255
+ .fillMaxHeight()
256
+ .selectionLens(pressedIndex.value != null)
257
+ )
258
+ Row(Modifier.fillMaxSize()) {
259
+ items.forEachIndexed { index, item ->
260
+ FloatingCell(item, selected = index == selectedIndex, index, pressedIndex) {
261
+ onTabSelected(index)
235
262
  }
236
263
  }
237
- FloatingCell(item, selected = index == selectedIndex) { onTabSelected(index) }
238
264
  }
239
265
  }
240
266
  }
267
+ if (floatingButton != null) {
268
+ QrButton(floatingButton, size = FLOATING_HEIGHT, Modifier.shadow(3.dp, CircleShape))
269
+ }
241
270
  }
242
271
  }
243
272
 
244
273
  @Composable
245
- private fun RowScope.FloatingCell(item: BottomTabItem, selected: Boolean, onClick: () -> Unit) {
274
+ private fun RowScope.FloatingCell(
275
+ item: BottomTabItem,
276
+ selected: Boolean,
277
+ index: Int,
278
+ pressedIndex: MutableState<Int?>,
279
+ onClick: () -> Unit,
280
+ ) {
246
281
  val colors = AppTheme.current.colors
247
282
  Column(
248
283
  modifier = Modifier
249
284
  .weight(1f)
250
285
  .fillMaxHeight()
251
- .noFeedbackClickable(onClick = onClick),
286
+ .tabClickable(index, pressedIndex, onClick),
252
287
  horizontalAlignment = Alignment.CenterHorizontally,
253
288
  verticalArrangement = Arrangement.spacedBy(Spacing.XXS, Alignment.CenterVertically),
254
289
  ) {
255
290
  DuoTabIcon(item, selected)
256
- Text(
257
- text = item.label,
258
- style = Typography.labelXsMedium,
259
- color = if (selected) colors.primary else colors.text.default,
260
- maxLines = 1,
261
- overflow = TextOverflow.Ellipsis,
262
- )
291
+ // like the system tab bar the label keeps its design size; Text grows it up to 5pt on the wide Duo displays
292
+ val context = LocalContext.current
293
+ val fixedScale = remember(context) {
294
+ (context ?: MiniAppContext()).copy(userScaleRate = 1f, useOSScaleRate = false)
295
+ }
296
+ CompositionLocalProvider(LocalContext provides fixedScale) {
297
+ Text(
298
+ text = item.label,
299
+ style = Typography.labelXsMedium,
300
+ color = if (selected) colors.primary else colors.text.hint,
301
+ maxLines = 1,
302
+ overflow = TextOverflow.Ellipsis,
303
+ )
304
+ }
263
305
  }
264
306
  }
265
307
 
308
+ // Tracks the finger so the selection lens jumps to the pressed tab before it is selected.
309
+ @Composable
310
+ private fun Modifier.tabClickable(index: Int, pressedIndex: MutableState<Int?>, onClick: () -> Unit): Modifier {
311
+ val interactionSource = remember { MutableInteractionSource() }
312
+ val pressed by interactionSource.collectIsPressedAsState()
313
+ LaunchedEffect(pressed) {
314
+ if (pressed) pressedIndex.value = index
315
+ else if (pressedIndex.value == index) pressedIndex.value = null
316
+ }
317
+ return clickable(interactionSource = interactionSource, indication = null, role = Role.Tab, onClick = onClick)
318
+ }
319
+
320
+ // Selection bubble; while pressed it grows into a lens (80x54 to about 96x70).
321
+ @Composable
322
+ private fun Modifier.selectionLens(pressed: Boolean): Modifier {
323
+ val lens by animateFloatAsState(if (pressed) 1f else 0f, LENS_SPRING, label = "DuoSelectionLens")
324
+ val tint = AppTheme.current.colors.primary.copy(alpha = SELECTION_TINT_ALPHA)
325
+ return graphicsLayer {
326
+ scaleX = 1 + lens * (LENS_SCALE_X - 1)
327
+ scaleY = 1 + lens * (LENS_SCALE_Y - 1)
328
+ }.background(tint, CircleShape)
329
+ }
330
+
266
331
  @Composable
267
332
  private fun DuoTabIcon(item: BottomTabItem, selected: Boolean) {
268
333
  val colors = AppTheme.current.colors
@@ -271,7 +336,7 @@ private fun DuoTabIcon(item: BottomTabItem, selected: Boolean) {
271
336
  Icon(
272
337
  source = item.icon,
273
338
  size = ICON_SIZE,
274
- color = if (selected) colors.primary else colors.text.default,
339
+ color = if (selected) colors.primary else colors.text.hint,
275
340
  )
276
341
  when {
277
342
  badgeLabel == null && !item.showDot -> Unit
@@ -288,9 +353,9 @@ private fun DuoTabIcon(item: BottomTabItem, selected: Boolean) {
288
353
  }
289
354
 
290
355
  @Composable
291
- private fun QrButton(data: BottomTabFloatingButton, size: Dp) {
356
+ private fun QrButton(data: BottomTabFloatingButton, size: Dp, modifier: Modifier = Modifier) {
292
357
  Box(
293
- modifier = Modifier
358
+ modifier = modifier
294
359
  .size(size)
295
360
  .clip(CircleShape)
296
361
  .background(AppTheme.current.colors.primary)
@@ -302,7 +367,7 @@ private fun QrButton(data: BottomTabFloatingButton, size: Dp) {
302
367
  }
303
368
  }
304
369
 
305
- // RN falls back to this when native glass is missing; Compose has no backdrop blur, so it always does.
370
+ // RN BarMaterial: surface with a 1dp border.
306
371
  @Composable
307
372
  private fun Modifier.tabBarMaterial(shape: Shape): Modifier {
308
373
  val colors = AppTheme.current.colors
@@ -97,7 +97,7 @@ internal fun headerTitleMaxWidth(): Dp {
97
97
  // Scroll distance over which the Duo glass bar fades in.
98
98
  private val DUO_GLASS_FADE_DISTANCE = 16.dp
99
99
 
100
- // RN DuoGlassBackground fallback: translucent surface with a hairline once content scrolls under it.
100
+ // RN DuoGlassBackground: translucent surface with a hairline once content scrolls under it.
101
101
  @Suppress("FrequentlyChangingValue")
102
102
  @Composable
103
103
  private fun Modifier.duoGlassBar(): Modifier {
@@ -17,17 +17,6 @@ import androidx.compose.foundation.shape.CircleShape
17
17
  import androidx.compose.foundation.shape.RoundedCornerShape
18
18
  import androidx.compose.runtime.Composable
19
19
  import androidx.compose.runtime.LaunchedEffect
20
- // AI-GENERATED START: dispose toolkit requests with their UI owner
21
- import androidx.compose.runtime.DisposableEffect
22
- import kotlinx.serialization.json.booleanOrNull
23
- import kotlinx.serialization.json.jsonPrimitive
24
- import vn.momo.maxapi.contract.navigation.model.DismissTarget
25
- import vn.momo.maxapi.contract.toolkit.model.ToolAction
26
- import vn.momo.maxapi.contract.toolkit.model.ToolkitOptions
27
- import vn.momo.maxapi.contract.toolkit.model.ToolkitToolGroup
28
- import vn.momo.maxapi.contract.toolkit.model.ToolkitTool
29
- import vn.momo.maxapi.runtime.ApiResult
30
- // AI-GENERATED END: dispose toolkit requests with their UI owner
31
20
  import androidx.compose.runtime.derivedStateOf
32
21
  import androidx.compose.runtime.getValue
33
22
  import androidx.compose.runtime.mutableStateOf
@@ -106,56 +95,53 @@ internal fun Toolkit(
106
95
  var isFavorite by remember { mutableStateOf(false) }
107
96
  val isLoading by remember { mutableStateOf(false) }
108
97
 
109
- // AI-GENERATED START: retain custom tool data and typed callback outcomes
110
- val requests = remember(api, context?.appCode) { ToolkitRequests() }
111
- DisposableEffect(requests) { onDispose { requests.close() } }
112
-
113
- LaunchedEffect(requests) {
114
- val client = api ?: return@LaunchedEffect
115
- requests.launch<Boolean>({ callback -> client.checkFavorite(context?.appCode, callback) }) { result ->
116
- if (result is ApiResult.Success) isFavorite = result.value
98
+ LaunchedEffect(context?.appCode) {
99
+ api?.isFavoriteApp(mapOf("code" to context?.appCode)) { callback ->
100
+ val response = callback?.get("response") as? Boolean
101
+ isFavorite = response == true
117
102
  }
118
103
  }
119
104
 
120
105
  fun onPressShortcut() {
121
- val client = api ?: return
122
- requests.launch({ callback ->
123
- client.performToolAction(ToolAction.FAVORITE, context?.appCode, callback)
124
- }) { result ->
125
- if (result is ApiResult.Success) {
126
- result.value.jsonPrimitive.booleanOrNull?.let { isFavorite = it }
106
+ api?.onToolAction(mapOf("item" to mapOf("key" to "onFavorite"), "context" to MiniAppContext.toMap(context))) { callback ->
107
+ val response = callback?.get("response") as? Map<*, *>?
108
+ val success = response?.get("success") as? Boolean
109
+ if (success == true){
110
+ isFavorite = !isFavorite
127
111
  }
128
112
  }
129
113
  }
130
114
 
131
115
  fun onPressMore() {
132
- val client = api ?: return
133
- val options = ToolkitOptions(
134
- customToolGroups = headerRight.tools.map { group ->
135
- ToolkitToolGroup(group.title, group.items.map { tool ->
136
- ToolkitTool(tool.key, tool.icon, tool.showBadge, tool.name, tool.showRightIcon)
137
- })
138
- },
139
- useSystemTools = headerRight.useSystemTools,
140
- )
141
- requests.launch({ callback -> client.showToolkit(options, callback) }) { result ->
142
- if (result is ApiResult.Success) headerRight.toolCallback?.invoke(result.value.actionKey)
116
+ api?.showTools(
117
+ mapOf(
118
+ "useSystemTools" to headerRight.useSystemTools,
119
+ "tools" to headerRight.tools.map { it.toMap() },
120
+ "context" to MiniAppContext.toMap(context)
121
+ )
122
+ ) { callback ->
123
+ val response = callback?.get("response") as? String
124
+ if (response != null) {
125
+ headerRight.toolCallback?.invoke(response)
126
+ }
143
127
  }
144
128
  }
145
- // AI-GENERATED END: retain custom tool data and typed callback outcomes
146
129
 
147
130
  fun onPressHelpCenter() {
148
131
  if (context == null) return
149
- // AI-GENERATED START: preserve Help Center context and own its event subscription
150
- val client = api ?: return
151
- requests.observe({ callback -> client.showBottomSheet(context.helpCenterRequest(), callback) }) {}
152
- // AI-GENERATED END: preserve Help Center context and own its event subscription
132
+ api?.showHelpCenter(
133
+ mapOf(
134
+ "appId" to context.appId,
135
+ "code" to context.appCode,
136
+ "name" to context.appName,
137
+ "icon" to context.appIcon,
138
+ "description" to context.description
139
+ )
140
+ ) {}
153
141
  }
154
142
 
155
143
  fun onPressClose() {
156
- // AI-GENERATED START: preserve current-container and whole-stack dismissal
157
- api?.dismiss(if (headerRight.useCloseIcon) DismissTarget.Current() else DismissTarget.All) {}
158
- // AI-GENERATED END: preserve current-container and whole-stack dismissal
144
+ if (headerRight.useCloseIcon) api?.dismiss { } else api?.dismissAll { }
159
145
  }
160
146
 
161
147
  val useHelpCenter = usesHelpCenter(context)
@@ -2,12 +2,6 @@ package vn.momo.kits.navigation.tracking
2
2
 
3
3
  import vn.momo.kits.application.MiniAppContext
4
4
  import vn.momo.maxapi.IMaxApi
5
- // AI-GENERATED START: serialize tracking attributes with their existing value types
6
- import kotlinx.serialization.json.JsonNull
7
- import kotlinx.serialization.json.JsonElement
8
- import kotlinx.serialization.json.JsonObject
9
- import kotlinx.serialization.json.JsonPrimitive
10
- // AI-GENERATED END: serialize tracking attributes with their existing value types
11
5
  import kotlin.time.Clock
12
6
  import kotlin.time.ExperimentalTime
13
7
 
@@ -21,7 +15,6 @@ class ScreenTrackingState {
21
15
  var action: String = "push"
22
16
  }
23
17
 
24
- // AI-GENERATED START: retain screen event names and typed tracking attributes
25
18
  object ScreenTracker {
26
19
 
27
20
  private var lastScreenName: String? = null
@@ -38,13 +31,13 @@ object ScreenTracker {
38
31
  val previousScreen = lastScreenName
39
32
  lastScreenName = screenName
40
33
 
41
- maxApi?.trackEvent(
34
+ maxApi?.track(
42
35
  eventName = "auto_screen_navigated",
43
- attributes = JsonObject(buildBaseParams(context, screenName).apply {
44
- put("pre_screen_name", JsonPrimitive(previousScreen))
45
- put("state", JsonPrimitive("navigated"))
46
- put("action", JsonPrimitive(action))
47
- })
36
+ params = buildBaseParams(context, screenName).apply {
37
+ put("pre_screen_name", previousScreen)
38
+ put("state", "navigated")
39
+ put("action", action)
40
+ }
48
41
  )
49
42
 
50
43
  state.releaseNavigated = true
@@ -60,12 +53,12 @@ object ScreenTracker {
60
53
  ) {
61
54
  if (state.releaseLoad) return
62
55
 
63
- maxApi?.trackEvent(
56
+ maxApi?.track(
64
57
  eventName = "auto_screen_displayed",
65
- attributes = JsonObject(buildBaseParams(context, screenName).apply {
66
- put("state", JsonPrimitive("load"))
67
- put("duration", JsonPrimitive(duration))
68
- })
58
+ params = buildBaseParams(context, screenName).apply {
59
+ put("state", "load")
60
+ put("duration", duration)
61
+ }
69
62
  )
70
63
 
71
64
  state.releaseLoad = true
@@ -81,13 +74,13 @@ object ScreenTracker {
81
74
  ) {
82
75
  if (state.releaseInteraction) return
83
76
 
84
- maxApi?.trackEvent(
77
+ maxApi?.track(
85
78
  eventName = "auto_screen_interacted",
86
- attributes = JsonObject(buildBaseParams(context, screenName).apply {
87
- put("state", JsonPrimitive("interaction"))
88
- put("duration", JsonPrimitive(duration))
89
- put("total_duration", JsonPrimitive(totalDuration))
90
- })
79
+ params = buildBaseParams(context, screenName).apply {
80
+ put("state", "interaction")
81
+ put("duration", duration)
82
+ put("total_duration", totalDuration)
83
+ }
91
84
  )
92
85
 
93
86
  state.releaseInteraction = true
@@ -97,13 +90,13 @@ object ScreenTracker {
97
90
  context: MiniAppContext?,
98
91
  screenName: String,
99
92
  componentName: String = "Screen"
100
- ): MutableMap<String, JsonElement> {
93
+ ): MutableMap<String, Any?> {
101
94
  return mutableMapOf(
102
- "app_id" to JsonPrimitive(context?.appId),
103
- "feature_code" to JsonPrimitive(context?.appCode),
104
- "screen_name" to JsonPrimitive(screenName),
105
- "component_name" to JsonPrimitive(componentName),
106
- "tracking_source" to JsonPrimitive(1)
95
+ "app_id" to context?.appId,
96
+ "feature_code" to context?.appCode,
97
+ "screen_name" to screenName,
98
+ "component_name" to componentName,
99
+ "tracking_source" to 1
107
100
  )
108
101
  }
109
102
 
@@ -112,16 +105,16 @@ object ScreenTracker {
112
105
  context: MiniAppContext?,
113
106
  parentScreenName: String?
114
107
  ) {
115
- maxApi?.trackEvent(
108
+ maxApi?.track(
116
109
  eventName = "auto_popup_displayed",
117
- attributes = JsonObject(mapOf(
118
- "screen_name" to JsonPrimitive(parentScreenName),
119
- "tracking_source" to JsonPrimitive(1),
120
- "app_id" to JsonPrimitive(context?.appId),
121
- "feature_code" to JsonPrimitive(context?.appCode),
122
- "kits_version" to JsonNull,
123
- "component_name" to JsonPrimitive("Modal")
124
- ))
110
+ params = mutableMapOf<String, Any?>(
111
+ "screen_name" to parentScreenName,
112
+ "tracking_source" to 1,
113
+ "app_id" to context?.appId,
114
+ "feature_code" to context?.appCode,
115
+ "kits_version" to null,
116
+ "component_name" to "Modal"
117
+ )
125
118
  )
126
119
  }
127
120
 
@@ -132,4 +125,3 @@ object ScreenTracker {
132
125
  lastScreenName = null
133
126
  }
134
127
  }
135
- // AI-GENERATED END: retain screen event names and typed tracking attributes
@@ -12,9 +12,7 @@ private const val STATUS_BAR_TAPPED_NOTIFICATION = "statusBarSelected"
12
12
  @Composable
13
13
  internal actual fun RegisterScrollToTop(callback: (() -> Unit)?) {
14
14
  val maxApi = LocalMaxApi.current
15
- // AI-GENERATED START: capture the current typed logger client
16
- DisposableEffect(callback, maxApi) {
17
- // AI-GENERATED END: capture the current typed logger client
15
+ DisposableEffect(callback) {
18
16
  val token: NSObjectProtocol? = if (callback != null) {
19
17
  NSNotificationCenter.defaultCenter.addObserverForName(
20
18
  name = STATUS_BAR_TAPPED_NOTIFICATION,
@@ -22,9 +20,7 @@ internal actual fun RegisterScrollToTop(callback: (() -> Unit)?) {
22
20
  queue = NSOperationQueue.mainQueue,
23
21
  ) { _: NSNotification? ->
24
22
  runCatching { callback() }.onFailure {
25
- // AI-GENERATED START: use typed host logging
26
- maxApi?.writeLog("[ScrollToTop] callback threw: ${it.message}")
27
- // AI-GENERATED END: use typed host logging
23
+ maxApi?.logFile("[ScrollToTop] callback threw: ${it.message}") {}
28
24
 
29
25
  }
30
26
  }
@@ -1,7 +1,5 @@
1
1
  [versions]
2
- # AI-GENERATED START: align Kits and its samples with the approved SDK 36 toolchain
3
- android-compileSdk = "36"
4
- # AI-GENERATED END: align Kits and its samples with the approved SDK 36 toolchain
2
+ android-compileSdk = "37"
5
3
  android-minSdk = "24"
6
4
  android-targetSdk = "36"
7
5
  ktor-version = "3.0.0-rc-1"
@@ -11,9 +9,7 @@ kSerialize = "1.7.1"
11
9
  compose = "1.10.3"
12
10
  navigation-multiplatform = "2.9.2"
13
11
  coil3-multiplatform = "3.0.0-alpha10"
14
- # AI-GENERATED START: align the Android toolchain with the integrating app
15
- androidGradlePlugin = "9.2.1"
16
- # AI-GENERATED END: align the Android toolchain with the integrating app
12
+ androidGradlePlugin = "8.13.2"
17
13
  r8 = "8.9.35"
18
14
  kotlinx-datetime = "0.7.1"
19
15
  airbnb-lottie = "5.2.0"
@@ -21,9 +17,10 @@ compottie = "2.1.0"
21
17
  androidx-activity = "1.9.1"
22
18
  androidx-appcompat = "1.7.0"
23
19
  material = "1.10.0"
20
+ maxapi = "0.1.1"
24
21
  vanniktechMavenPublish = "0.34.0"
25
22
  kits = "0.167.1-duo.1"
26
- nativemaxapi = "0.3.9"
23
+ nativemaxapi = "0.0.6"
27
24
  gif-image = "0.1.2"
28
25
 
29
26
  [libraries]
@@ -70,7 +67,3 @@ compose = { id = "org.jetbrains.compose", version.ref = "compose" }
70
67
  android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
71
68
  android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
72
69
  vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }
73
-
74
- # AI-GENERATED START: use the AGP Android target for Kotlin Multiplatform libraries
75
- android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "androidGradlePlugin" }
76
- # AI-GENERATED END: use the AGP Android target for Kotlin Multiplatform libraries
@@ -1,9 +1,7 @@
1
1
  #Thu Jul 31 17:05:40 ICT 2025
2
2
  distributionBase=GRADLE_USER_HOME
3
3
  distributionPath=wrapper/dists
4
- # AI-GENERATED START: align the Gradle wrapper with the integrating app
5
- distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
6
- # AI-GENERATED END: align the Gradle wrapper with the integrating app
4
+ distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
7
5
  networkTimeout=10000
8
6
  validateDistributionUrl=true
9
7
  zipStoreBase=GRADLE_USER_HOME