@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.
- package/CLAUDE.md +4 -4
- package/README.md +2 -10
- package/build.gradle.kts +1 -3
- package/compose/build.gradle.kts +20 -19
- package/compose/build.gradle.kts.backup +19 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +14 -23
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +5 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +14 -24
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +1 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +1 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +1 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +2 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +124 -59
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +30 -44
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +32 -40
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +2 -6
- package/gradle/libs.versions.toml +4 -11
- package/gradle/wrapper/gradle-wrapper.properties +1 -3
- package/gradle.properties +3 -3
- package/ios/Layout/ScreenLayout.swift +5 -0
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +2 -3
- package/ios/Navigation/BottomTab/DuoTabBar.swift +94 -69
- package/ios/Navigation/Component/HeaderBackground.swift +22 -1
- package/ios/Navigation/StackScreen.swift +3 -7
- package/package.json +1 -1
- package/publish.sh +2 -33
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +0 -37
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +0 -41
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +0 -44
- 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 =
|
|
75
|
+
private val FLOATING_HEIGHT = DuoMetrics.floatingTabBarHeight
|
|
63
76
|
private val FLOATING_PADDING = Spacing.XS
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
private const val
|
|
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 +
|
|
104
|
+
if (layout.isIPhoneDuo && layout.sideBar == null) FLOATING_HEIGHT + floatingTabBarBottom(layout.insetBottom) else 0.dp
|
|
84
105
|
|
|
85
|
-
// iPhone Duo tab bar:
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
|
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
|
-
|
|
224
|
+
Row(
|
|
207
225
|
modifier = Modifier
|
|
208
226
|
.align(Alignment.BottomCenter)
|
|
209
|
-
.
|
|
210
|
-
.
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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(
|
|
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
|
-
.
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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?.
|
|
34
|
+
maxApi?.track(
|
|
42
35
|
eventName = "auto_screen_navigated",
|
|
43
|
-
|
|
44
|
-
put("pre_screen_name",
|
|
45
|
-
put("state",
|
|
46
|
-
put("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?.
|
|
56
|
+
maxApi?.track(
|
|
64
57
|
eventName = "auto_screen_displayed",
|
|
65
|
-
|
|
66
|
-
put("state",
|
|
67
|
-
put("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?.
|
|
77
|
+
maxApi?.track(
|
|
85
78
|
eventName = "auto_screen_interacted",
|
|
86
|
-
|
|
87
|
-
put("state",
|
|
88
|
-
put("duration",
|
|
89
|
-
put("total_duration",
|
|
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,
|
|
93
|
+
): MutableMap<String, Any?> {
|
|
101
94
|
return mutableMapOf(
|
|
102
|
-
"app_id" to
|
|
103
|
-
"feature_code" to
|
|
104
|
-
"screen_name" to
|
|
105
|
-
"component_name" to
|
|
106
|
-
"tracking_source" to
|
|
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?.
|
|
108
|
+
maxApi?.track(
|
|
116
109
|
eventName = "auto_popup_displayed",
|
|
117
|
-
|
|
118
|
-
"screen_name" to
|
|
119
|
-
"tracking_source" to
|
|
120
|
-
"app_id" to
|
|
121
|
-
"feature_code" to
|
|
122
|
-
"kits_version" to
|
|
123
|
-
"component_name" to
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|