@momo-kits/native-kits 0.167.1-sizescale.2-debug → 0.300.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 (56) hide show
  1. package/CLAUDE.md +4 -4
  2. package/README.md +10 -2
  3. package/build.gradle.kts +3 -1
  4. package/compose/build.gradle.kts +19 -20
  5. package/compose/build.gradle.kts.backup +18 -19
  6. package/compose/compose.podspec +1 -1
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +1 -4
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +2 -2
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +3 -3
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +25 -16
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +23 -13
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +3 -3
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +11 -15
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +6 -23
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +22 -9
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +28 -21
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -1
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +6 -1
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +26 -17
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +4 -5
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +47 -30
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +33 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +40 -32
  31. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +47 -0
  32. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +6 -2
  33. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +1 -3
  34. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +6 -11
  35. package/gradle/libs.versions.toml +9 -3
  36. package/gradle/wrapper/gradle-wrapper.properties +3 -1
  37. package/gradle.properties +1 -1
  38. package/ios/native-kits.podspec +1 -1
  39. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  40. package/package.json +1 -1
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitSizeScale.kt +0 -56
  42. package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Example.xcscheme +0 -32
  43. package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  44. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  45. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/WorkspaceSettings.xcsettings +0 -16
  46. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
  47. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -5
  48. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
  49. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
  50. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImage.xcscheme +0 -58
  51. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +0 -58
  52. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SkeletonUI.xcscheme +0 -58
  53. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios-LottiePrivacyInfo.xcscheme +0 -58
  54. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios.xcscheme +0 -58
  55. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -60
  56. package/local.properties +0 -8
@@ -6,33 +6,46 @@ import androidx.compose.foundation.layout.width
6
6
  import androidx.compose.runtime.Composable
7
7
  import androidx.compose.runtime.compositionLocalOf
8
8
  import androidx.compose.ui.Modifier
9
+ import androidx.compose.ui.platform.LocalDensity
9
10
  import androidx.compose.ui.unit.Dp
10
11
  import androidx.compose.ui.unit.dp
12
+ import vn.momo.kits.platform.getScreenDimensions
13
+ import kotlin.math.floor
11
14
 
12
15
  internal data class GridContext(
13
16
  val numberOfColumns: Int = 12,
14
- val gutter: Dp = 12.dp,
15
- val sizePerSpan: Dp = 0.dp,
17
+ val gutterPx: Int = 0,
18
+ val columnPx: Float = 0f,
19
+ val density: Float = 1f,
16
20
  ) {
17
- fun sizeForSpan(span: Int): Dp = sizePerSpan * span + gutter * (span - 1)
21
+ fun sizeForSpan(span: Int): Dp = (floor(columnPx * span + gutterPx * (span - 1)) / density).dp
18
22
  }
19
23
 
20
24
  internal val LocalGridContext = compositionLocalOf<GridContext?> { null }
21
25
 
22
26
  @Composable
23
- internal fun Item(
24
- widthSpan: Int = 12,
27
+ internal fun gridColumns(): Int = if (getScreenDimensions().width > 600) 24 else 12
28
+
29
+ @Composable
30
+ internal fun gridContext(maxWidthPx: Int, columns: Int, gap: Dp): GridContext {
31
+ val density = LocalDensity.current
32
+ val gutterPx = with(density) { gap.roundToPx() }
33
+ return GridContext(columns, gutterPx, (maxWidthPx - gutterPx * (columns - 1)).toFloat() / columns, density.density)
34
+ }
35
+
36
+ @Composable
37
+ fun Item(
38
+ widthSpan: Int? = null,
25
39
  heightSpan: Int = 0,
26
40
  modifier: Modifier = Modifier,
41
+ contentModifier: Modifier = Modifier,
27
42
  content: @Composable () -> Unit,
28
43
  ) {
29
44
  val grid = LocalGridContext.current
30
45
  var m = modifier
31
46
  if (grid != null) {
32
- m = m.width(grid.sizeForSpan(widthSpan))
47
+ m = m.width(grid.sizeForSpan(widthSpan ?: grid.numberOfColumns))
33
48
  if (heightSpan > 0) m = m.height(grid.sizeForSpan(heightSpan))
34
49
  }
35
- // A Column (not a Box) so multiple children flow VERTICALLY instead of stacking
36
- // on top of each other — matches the React Item's flex-column content.
37
- Column(modifier = m) { content() }
50
+ Column(modifier = m.then(contentModifier)) { content() }
38
51
  }
@@ -3,6 +3,7 @@ package vn.momo.kits.layout
3
3
  import androidx.compose.foundation.layout.Arrangement
4
4
  import androidx.compose.foundation.layout.Box
5
5
  import androidx.compose.foundation.layout.BoxWithConstraints
6
+ import androidx.compose.foundation.layout.ExperimentalLayoutApi
6
7
  import androidx.compose.foundation.layout.FlowRow
7
8
  import androidx.compose.foundation.layout.fillMaxWidth
8
9
  import androidx.compose.foundation.layout.padding
@@ -11,27 +12,28 @@ import androidx.compose.runtime.CompositionLocalProvider
11
12
  import androidx.compose.ui.Modifier
12
13
  import androidx.compose.ui.unit.dp
13
14
  import vn.momo.kits.components.Image
15
+ import vn.momo.kits.const.Spacing
14
16
 
15
17
  @Composable
16
- internal fun Section(
18
+ @OptIn(ExperimentalLayoutApi::class)
19
+ fun Section(
17
20
  useMargin: Boolean = true,
18
21
  backgroundImage: String? = null,
19
22
  modifier: Modifier = Modifier,
20
23
  content: @Composable () -> Unit,
21
24
  ) {
22
- val columns = 12
23
- val gutter = 12.dp
24
- val margin = if (useMargin) 12.dp else 0.dp
25
- Box(modifier = modifier.fillMaxWidth().padding(horizontal = margin)) {
25
+ val columns = gridColumns()
26
+ val gap = Spacing.M
27
+ val margin = if (useMargin) Spacing.M else 0.dp
28
+ Box(modifier = Modifier.fillMaxWidth().padding(horizontal = margin).then(modifier)) {
26
29
  if (backgroundImage != null) {
27
30
  Image(source = backgroundImage, modifier = Modifier.matchParentSize())
28
31
  }
29
32
  BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
30
- val sizePerSpan = (maxWidth - gutter * (columns - 1)) / columns - 1.dp / columns
31
- CompositionLocalProvider(LocalGridContext provides GridContext(columns, gutter, sizePerSpan)) {
33
+ CompositionLocalProvider(LocalGridContext provides gridContext(constraints.maxWidth, columns, gap)) {
32
34
  FlowRow(
33
- horizontalArrangement = Arrangement.spacedBy(gutter),
34
- verticalArrangement = Arrangement.spacedBy(gutter),
35
+ horizontalArrangement = Arrangement.spacedBy(gap),
36
+ verticalArrangement = Arrangement.spacedBy(gap),
35
37
  ) { content() }
36
38
  }
37
39
  }
@@ -30,13 +30,15 @@ import vn.momo.kits.const.AppNavigationBar
30
30
  import vn.momo.kits.const.AppStatusBar
31
31
  import vn.momo.kits.const.AppTheme
32
32
  import vn.momo.kits.const.Theme
33
- import vn.momo.kits.const.ProvideKitScale
34
33
  import vn.momo.kits.const.defaultTheme
35
- import vn.momo.kits.const.pendingSizeScale
36
34
  import vn.momo.kits.platform.ProvideNavigationEventDispatcherOwner
35
+ import vn.momo.kits.platform.ProvideOsFontScale
37
36
  import vn.momo.kits.utils.getAppStatusBarHeight
38
37
  import vn.momo.kits.utils.getNavigationBarHeight
38
+ // AI-GENERATED START: consume typed NativeMaxApi results
39
39
  import vn.momo.maxapi.IMaxApi
40
+ import vn.momo.maxapi.runtime.ApiResult
41
+ // AI-GENERATED END: consume typed NativeMaxApi results
40
42
  import kotlin.time.Duration.Companion.milliseconds
41
43
 
42
44
  @Composable
@@ -54,11 +56,13 @@ fun NavigationContainer(
54
56
  ) {
55
57
  val navController = rememberNavController()
56
58
  val registry = remember { DynamicScreenRegistry() }
57
- val navigator = remember { Navigator(navController = navController, maxApi = maxApi, registry = registry) }
58
- // Insets are measured in real units; convert to the dp of the size-scaled container.
59
- val sizeScale = pendingSizeScale()
60
- val statusBarHeight = (statusBarHeight ?: getAppStatusBarHeight()) / sizeScale
61
- val navigationBarHeight = getNavigationBarHeight() / sizeScale
59
+ // AI-GENERATED START: retain the current host client after recomposition
60
+ val navigator = remember(navController, maxApi, registry) {
61
+ Navigator(navController = navController, maxApi = maxApi, registry = registry)
62
+ }
63
+ // AI-GENERATED END: retain the current host client after recomposition
64
+ val statusBarHeight = statusBarHeight ?: getAppStatusBarHeight()
65
+ val navigationBarHeight = getNavigationBarHeight()
62
66
 
63
67
  val parentContext = LocalContext.current
64
68
  val mergedContext = MiniAppContext.merge(parentContext, miniAppContext)
@@ -80,22 +84,31 @@ fun NavigationContainer(
80
84
 
81
85
  val resolvedLocalize = localize ?: fallbackLocalize
82
86
 
83
- LaunchedEffect(maxApi, resolvedLocalize) {
84
- val api = maxApi ?: return@LaunchedEffect
85
- runCatching {
86
- api.getLanguage(callback = { data ->
87
- if (parseLanguage(data) == Localize.EN) {
88
- resolvedLocalize.changeLanguage(Localize.EN)
87
+ // AI-GENERATED START: cancel language delivery when its composition owner changes
88
+ DisposableEffect(maxApi, resolvedLocalize) {
89
+ val api = maxApi
90
+ val target = resolvedLocalize
91
+ var active = true
92
+ val handle = runCatching {
93
+ api.getLanguage { result ->
94
+ if (active && result is ApiResult.Success &&
95
+ result.value.trim().lowercase().startsWith(Localize.EN)) {
96
+ target.changeLanguage(Localize.EN)
89
97
  }
90
- })
98
+ }
99
+ }.getOrNull()
100
+ onDispose {
101
+ active = false
102
+ handle?.cancel()
91
103
  }
92
104
  }
105
+ // AI-GENERATED END: cancel language delivery when its composition owner changes
93
106
 
94
107
  val screenId = rememberSaveable { registry.nextId() }
95
108
  registry.bind(screenId, initialScreenName, initialScreen, options)
96
109
  val startDestination = remember(screenId) { DynamicScreenRoute(screenId) }
97
110
 
98
- ProvideKitScale {
111
+ ProvideOsFontScale {
99
112
  ProvideNavigationEventDispatcherOwner(navigator) {
100
113
  CompositionLocalProvider(
101
114
  LocalNavigator provides navigator,
@@ -196,9 +209,3 @@ fun NavigationContainer(
196
209
 
197
210
  val LocalMaxApi = staticCompositionLocalOf<IMaxApi?> { null }
198
211
 
199
- private fun parseLanguage(raw: Map<String, Any?>?): String? {
200
- val code = (raw?.get("response") as? String)?.trim()?.lowercase()
201
- if (code.isNullOrEmpty()) return null
202
- return if (code.startsWith(Localize.EN)) Localize.EN else Localize.VI
203
- }
204
-
@@ -22,6 +22,9 @@ import vn.momo.kits.components.PopupNotify
22
22
  import vn.momo.kits.navigation.BottomHeader.*
23
23
  import vn.momo.kits.navigation.component.SnackBar
24
24
  import vn.momo.maxapi.IMaxApi
25
+ // AI-GENERATED START: dismiss through the typed navigator target
26
+ import vn.momo.maxapi.contract.navigation.model.DismissTarget
27
+ // AI-GENERATED END: dismiss through the typed navigator target
25
28
  import kotlin.time.Duration.Companion.milliseconds
26
29
 
27
30
  class Navigator(
@@ -119,7 +122,9 @@ class Navigator(
119
122
  delay(STACK_TRANSITION_DURATION.milliseconds)
120
123
  dismissing?.let { registry.unregisterScreen(it.id) }
121
124
  } else {
122
- maxApi?.dismiss { }
125
+ // AI-GENERATED START: preserve root-screen dismissal
126
+ maxApi?.dismiss(DismissTarget.Current()) {}
127
+ // AI-GENERATED END: preserve root-screen dismissal
123
128
  }
124
129
  }
125
130
 
@@ -6,6 +6,9 @@ import androidx.compose.runtime.snapshotFlow
6
6
  import vn.momo.kits.platform.getScreenDimensions
7
7
  import vn.momo.kits.platform.rememberBackGestureProgress
8
8
  import vn.momo.maxapi.IMaxApi
9
+ // AI-GENERATED START: use the typed light vibration pattern
10
+ import vn.momo.maxapi.contract.device.vibration.model.VibrateStyle
11
+ // AI-GENERATED END: use the typed light vibration pattern
9
12
 
10
13
  // Unscaled, so the tick lands at the same physical distance on every device.
11
14
  private const val SWIPE_BACK_HAPTIC_THRESHOLD_DP = 50f
@@ -25,7 +28,9 @@ internal fun SwipeBackHaptic(maxApi: IMaxApi?) {
25
28
  snapshotFlow { progress.value }.collect { fraction ->
26
29
  val travelled = fraction ?: 0f
27
30
  if (travelled >= threshold && !isSendHaptic) {
28
- runCatching { maxApi.triggerEventVibration("light", callback = null) }
31
+ // AI-GENERATED START: keep one light tick per threshold crossing
32
+ runCatching { maxApi.vibrate(VibrateStyle.LIGHT) }
33
+ // AI-GENERATED END: keep one light tick per threshold crossing
29
34
  isSendHaptic = true
30
35
  } else if (travelled < threshold && isSendHaptic) {
31
36
  isSendHaptic = false
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.*
10
10
  import androidx.compose.runtime.Composable
11
11
  import androidx.compose.runtime.DisposableEffect
12
12
  import androidx.compose.runtime.LaunchedEffect
13
+ import androidx.compose.runtime.remember
13
14
  import androidx.compose.ui.Alignment
14
15
  import androidx.compose.ui.Modifier
15
16
  import androidx.compose.ui.unit.dp
@@ -26,6 +27,7 @@ import vn.momo.kits.navigation.LocalNavigator
26
27
  import vn.momo.kits.navigation.NavigationOptions
27
28
  import vn.momo.kits.navigation.StackScreen
28
29
  import vn.momo.kits.navigation.component.HeaderType
30
+ import vn.momo.kits.platform.BackHandler
29
31
  import vn.momo.kits.platform.reportFloatingArrowBack
30
32
 
31
33
  private var bottomTabOptionItems : MutableList<NavigationOptions?> = mutableListOf()
@@ -43,11 +45,17 @@ internal fun getBottomTabOption(index: Int): NavigationOptions? {
43
45
  @Composable
44
46
  fun BottomTab(
45
47
  items: List<BottomTabItem>,
46
- floatingButton: BottomTabFloatingButton? = null
48
+ floatingButton: BottomTabFloatingButton? = null,
49
+ initialRouteName: String? = null,
50
+ onTabPress: ((index: Int, switchTab: () -> Unit) -> Unit)? = null,
51
+ backToInitialTab: Boolean = true,
47
52
  ) {
48
53
  val navigation = LocalNavigation.current
49
54
  val navigator = LocalNavigator.current
50
55
  val navController = rememberNavController()
56
+ val startRoute = remember {
57
+ "option${items.indexOfFirst { it.name == initialRouteName }.coerceAtLeast(0)}"
58
+ }
51
59
 
52
60
  bottomTabOptionItems = items.mapIndexed { index, item ->
53
61
  item.options ?: NavigationOptions()
@@ -62,15 +70,14 @@ fun BottomTab(
62
70
  )
63
71
  }
64
72
 
65
- // Tab 0 keeps the seeked transition; other tabs go to the host's floating arrow. Matches RN.
66
73
  val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
67
- val isFirstTab = currentRoute == null || currentRoute == "option0"
74
+ val onStartTab = currentRoute == null || currentRoute == startRoute
75
+ val isFirstTab = onStartTab || !backToInitialTab
76
+ val backToStartTab = { navController.popBackStack(startRoute, inclusive = false) }
68
77
 
69
78
  DisposableEffect(isFirstTab) {
70
79
  reportFloatingArrowBack(!isFirstTab)
71
- navigator.tabBackHandler = if (isFirstTab) null else ({
72
- navController.popBackStack(navController.graph.startDestinationRoute!!, false)
73
- })
80
+ navigator.tabBackHandler = if (isFirstTab) null else ({ backToStartTab() })
74
81
  // both live outside this composition
75
82
  onDispose {
76
83
  reportFloatingArrowBack(false)
@@ -84,7 +91,7 @@ fun BottomTab(
84
91
  ) {
85
92
  NavHost(
86
93
  navController = navController,
87
- startDestination = "option0"
94
+ startDestination = startRoute
88
95
  ) {
89
96
  items.forEachIndexed { index, item ->
90
97
  composable(
@@ -125,11 +132,8 @@ fun BottomTab(
125
132
  navigationOptions = option,
126
133
  bottomTabIndex = index,
127
134
  onBackHandler = {
128
- if (index != 0) {
129
- navController.popBackStack(
130
- navController.graph.startDestinationRoute!!,
131
- inclusive = false
132
- )
135
+ if (backToInitialTab && "option$index" != startRoute) {
136
+ backToStartTab()
133
137
  } else {
134
138
  navigator.pop()
135
139
  }
@@ -139,17 +143,22 @@ fun BottomTab(
139
143
  }
140
144
  }
141
145
  }
146
+ BackHandler(enabled = !onStartTab) {
147
+ if (backToInitialTab) backToStartTab() else navigator.pop()
148
+ }
142
149
  Column {
143
150
  BottomTabBar(
144
151
  items = items,
145
152
  floatingButton = floatingButton,
146
153
  navController = navController,
147
- onTabSelected = {
148
- val currentRoute = navController.currentBackStackEntry?.destination?.route
149
- val targetRoute = "option$it"
150
- if (currentRoute != targetRoute) {
151
- navController.navigate(targetRoute)
154
+ onTabSelected = { index ->
155
+ val switchTab = {
156
+ val targetRoute = "option$index"
157
+ if (navController.currentBackStackEntry?.destination?.route != targetRoute) {
158
+ navController.navigate(targetRoute)
159
+ }
152
160
  }
161
+ onTabPress?.invoke(index, switchTab) ?: switchTab()
153
162
  }
154
163
  )
155
164
  Spacer(modifier = Modifier.fillMaxWidth().height(AppNavigationBar.current + Spacing.S).background(AppTheme.current.colors.background.surface))
@@ -67,11 +67,10 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
67
67
  else -> HEADER_HEIGHT
68
68
  }
69
69
  val opacity by animateFloatAsState(
70
- targetValue = if (options.headerType is HeaderType.Transparent) {
71
- 0f
72
- } else {
73
- (scrollState.value / opacityHeight.toFloat())
74
- .coerceIn(0f, 1f)
70
+ targetValue = when {
71
+ options.headerType is HeaderType.Transparent -> 0f
72
+ opacityHeight <= 0 -> if (scrollState.value > 0) 1f else 0f
73
+ else -> (scrollState.value / opacityHeight.toFloat()).coerceIn(0f, 1f)
75
74
  }
76
75
  )
77
76
 
@@ -16,6 +16,17 @@ import androidx.compose.foundation.shape.CircleShape
16
16
  import androidx.compose.foundation.shape.RoundedCornerShape
17
17
  import androidx.compose.runtime.Composable
18
18
  import androidx.compose.runtime.LaunchedEffect
19
+ // AI-GENERATED START: dispose toolkit requests with their UI owner
20
+ import androidx.compose.runtime.DisposableEffect
21
+ import kotlinx.serialization.json.booleanOrNull
22
+ import kotlinx.serialization.json.jsonPrimitive
23
+ import vn.momo.maxapi.contract.navigation.model.DismissTarget
24
+ import vn.momo.maxapi.contract.toolkit.model.ToolAction
25
+ import vn.momo.maxapi.contract.toolkit.model.ToolkitOptions
26
+ import vn.momo.maxapi.contract.toolkit.model.ToolkitToolGroup
27
+ import vn.momo.maxapi.contract.toolkit.model.ToolkitTool
28
+ import vn.momo.maxapi.runtime.ApiResult
29
+ // AI-GENERATED END: dispose toolkit requests with their UI owner
19
30
  import androidx.compose.runtime.derivedStateOf
20
31
  import androidx.compose.runtime.getValue
21
32
  import androidx.compose.runtime.mutableStateOf
@@ -71,37 +82,43 @@ internal fun Toolkit(
71
82
  var isFavorite by remember { mutableStateOf(false) }
72
83
  val isLoading by remember { mutableStateOf(false) }
73
84
 
74
- LaunchedEffect(context?.appCode) {
75
- api?.isFavoriteApp(mapOf("code" to context?.appCode)) { callback ->
76
- val response = callback?.get("response") as? Boolean
77
- isFavorite = response == true
85
+ // AI-GENERATED START: retain custom tool data and typed callback outcomes
86
+ val requests = remember(api, context?.appCode) { ToolkitRequests() }
87
+ DisposableEffect(requests) { onDispose { requests.close() } }
88
+
89
+ LaunchedEffect(requests) {
90
+ val client = api ?: return@LaunchedEffect
91
+ requests.launch<Boolean>({ callback -> client.checkFavorite(context?.appCode, callback) }) { result ->
92
+ if (result is ApiResult.Success) isFavorite = result.value
78
93
  }
79
94
  }
80
95
 
81
96
  fun onPressShortcut() {
82
- api?.onToolAction(mapOf("item" to mapOf("key" to "onFavorite"), "context" to MiniAppContext.toMap(context))) { callback ->
83
- val response = callback?.get("response") as? Map<*, *>?
84
- val success = response?.get("success") as? Boolean
85
- if (success == true){
86
- isFavorite = !isFavorite
97
+ val client = api ?: return
98
+ requests.launch({ callback ->
99
+ client.performToolAction(ToolAction.FAVORITE, context?.appCode, callback)
100
+ }) { result ->
101
+ if (result is ApiResult.Success) {
102
+ result.value.jsonPrimitive.booleanOrNull?.let { isFavorite = it }
87
103
  }
88
104
  }
89
105
  }
90
106
 
91
107
  fun onPressMore() {
92
- api?.showTools(
93
- mapOf(
94
- "useSystemTools" to headerRight.useSystemTools,
95
- "tools" to headerRight.tools.map { it.toMap() },
96
- "context" to MiniAppContext.toMap(context)
97
- )
98
- ) { callback ->
99
- val response = callback?.get("response") as? String
100
- if (response != null) {
101
- headerRight.toolCallback?.invoke(response)
102
- }
108
+ val client = api ?: return
109
+ val options = ToolkitOptions(
110
+ customToolGroups = headerRight.tools.map { group ->
111
+ ToolkitToolGroup(group.title, group.items.map { tool ->
112
+ ToolkitTool(tool.key, tool.icon, tool.showBadge, tool.name, tool.showRightIcon)
113
+ })
114
+ },
115
+ useSystemTools = headerRight.useSystemTools,
116
+ )
117
+ requests.launch({ callback -> client.showToolkit(options, callback) }) { result ->
118
+ if (result is ApiResult.Success) headerRight.toolCallback?.invoke(result.value.actionKey)
103
119
  }
104
120
  }
121
+ // AI-GENERATED END: retain custom tool data and typed callback outcomes
105
122
 
106
123
  val navButtonConfig = getNavigationButtonConfig(
107
124
  headerRight,
@@ -156,15 +173,13 @@ internal fun Toolkit(
156
173
  size = 20.dp,
157
174
  color = headerColor.tintIconColor,
158
175
  modifier = Modifier.padding(4.dp).activeOpacityClickable {
159
- api?.showHelpCenter(
160
- mapOf(
161
- "appId" to context.appId,
162
- "code" to context.appCode,
163
- "name" to context.appName,
164
- "icon" to context.appIcon,
165
- "description" to context.description
166
- )
167
- ) {}
176
+ // AI-GENERATED START: open support through the typed action
177
+ api?.let { client ->
178
+ requests.launch({ callback ->
179
+ client.performToolAction(ToolAction.SUPPORT, context.appCode, callback)
180
+ }) {}
181
+ }
182
+ // AI-GENERATED END: open support through the typed action
168
183
  }
169
184
  )
170
185
  Box(
@@ -179,7 +194,9 @@ internal fun Toolkit(
179
194
  size = 20.dp,
180
195
  color = headerColor.tintIconColor,
181
196
  modifier = Modifier.padding(4.dp).activeOpacityClickable {
182
- api?.dismissAll { }
197
+ // AI-GENERATED START: preserve dismiss-all without selecting Home
198
+ api?.dismiss(DismissTarget.All) {}
199
+ // AI-GENERATED END: preserve dismiss-all without selecting Home
183
200
  }
184
201
  )
185
202
  }
@@ -9,10 +9,12 @@ import androidx.compose.ui.text.style.TextOverflow
9
9
  import androidx.compose.ui.unit.sp
10
10
  import androidx.compose.ui.zIndex
11
11
  import vn.momo.kits.components.Text
12
+ import vn.momo.kits.const.KitDesignSystem
12
13
  import vn.momo.kits.const.Typography
13
14
  import vn.momo.kits.modifier.setAutomationId
14
15
 
15
- internal const val HEADER_FONT_FAMILY = "MoMoTrustDisplay"
16
+ internal val headerFontFamily: String?
17
+ get() = if (KitDesignSystem.isWhiteList) "MoMoTrustDisplay" else null
16
18
 
17
19
  @Composable
18
20
  internal fun HeaderTitle(
@@ -29,7 +31,7 @@ internal fun HeaderTitle(
29
31
  fontSize = 15.sp,
30
32
  lineHeight = 22.sp,
31
33
  ),
32
- fontFamily = HEADER_FONT_FAMILY,
34
+ fontFamily = headerFontFamily,
33
35
  color = color,
34
36
  maxLines = 1,
35
37
  overflow = TextOverflow.Ellipsis
@@ -79,7 +79,7 @@ internal fun HeaderUser(
79
79
  text = data.title,
80
80
  color = data.tintColor,
81
81
  style = Typography.actionXsBold,
82
- fontFamily = HEADER_FONT_FAMILY,
82
+ fontFamily = headerFontFamily,
83
83
  maxLines = 1,
84
84
  overflow = TextOverflow.Ellipsis
85
85
  )
@@ -0,0 +1,33 @@
1
+ // File: ToolkitRequests.kt
2
+ // Created At: 2026-10-12 +07:00 | Created By: AI | AI Agent: Codex | Model: GPT-6
3
+ // AI-GENERATED START: cancel only toolkit requests owned by this composition
4
+ package vn.momo.kits.navigation.component
5
+
6
+ import vn.momo.maxapi.runtime.ApiCallback
7
+ import vn.momo.maxapi.runtime.ApiRequestHandle
8
+
9
+ internal class ToolkitRequests {
10
+ private var active = true
11
+ private val handles = mutableSetOf<ApiRequestHandle>()
12
+
13
+ fun <T> launch(request: (ApiCallback<T>) -> ApiRequestHandle, onResult: ApiCallback<T>) {
14
+ if (!active) return
15
+ var completed = false
16
+ var handle: ApiRequestHandle? = null
17
+ val created = request { result ->
18
+ completed = true
19
+ handle?.let(handles::remove)
20
+ if (active) onResult(result)
21
+ }
22
+ handle = created
23
+ if (!active) created.cancel() else if (!completed) handles.add(created)
24
+ }
25
+
26
+ fun close() {
27
+ active = false
28
+ val pending = handles.toList()
29
+ handles.clear()
30
+ pending.forEach { it.cancel() }
31
+ }
32
+ }
33
+ // AI-GENERATED END: cancel only toolkit requests owned by this composition