@momo-kits/native-kits 0.168.1-beta.2-debug → 0.168.1-beta.3-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 (23) 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/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +23 -14
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +24 -14
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +6 -1
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +6 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +44 -30
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +41 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +40 -32
  15. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
  16. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +62 -0
  17. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +6 -2
  18. package/gradle/libs.versions.toml +11 -4
  19. package/gradle/wrapper/gradle-wrapper.properties +3 -1
  20. package/gradle.properties +1 -1
  21. package/package.json +1 -1
  22. package/publish.sh +7 -2
  23. package/.claude/settings.local.json +0 -9
package/CLAUDE.md CHANGED
@@ -7,7 +7,7 @@ Guidance for working in this repo. Keep it accurate — update it when structure
7
7
  `momo-native-kits` — MoMo's design-system / UI-kit as a **Kotlin Multiplatform + Compose Multiplatform** library, published for Android (AAR/Maven) and iOS (static framework + CocoaPods). It is the Compose port of the React Native `momo-kits`. Consumed by mini-apps embedded in the host MoMo app.
8
8
 
9
9
  - Artifact: `vn.momo.kits:kits` (version in `gradle.properties`). NPM mirror: `@momo-kits/native-kits`.
10
- - Targets: `androidTarget` (JVM 17, compile/min/target SDK 36/24/36), `iosArm64`, `iosSimulatorArm64` (framework `baseName = "kits"`, `isStatic = true`).
10
+ - Targets: AGP KMP `android` target (JVM 17, compile/min SDK 36/24; AGP 9.2.1, Gradle 9.5.0), `iosArm64`, `iosSimulatorArm64` (framework `baseName = "kits"`, `isStatic = true`).
11
11
  - Kotlin 2.3.10, Compose MP 1.10.3.
12
12
 
13
13
  ## Modules
@@ -26,11 +26,11 @@ Guidance for working in this repo. Keep it accurate — update it when structure
26
26
 
27
27
  ## Build & verify
28
28
 
29
- No unit-test suite is configured. Validate changes by compiling the relevant target(s); use the sample app for visual checks.
29
+ Run `./gradlew :compose:testAndroidHostTest` for the request ownership tests in `commonTest`. Compile the relevant Android/iOS targets and use the sample app for visual checks.
30
30
 
31
31
  ```bash
32
32
  ./gradlew :compose:compileCommonMainKotlinMetadata # validate commonMain (fast, do this first)
33
- ./gradlew :compose:compileDebugKotlinAndroid # Android target
33
+ ./gradlew :compose:compileAndroidMain # Android target
34
34
  ./gradlew :compose:linkDebugFrameworkIosSimulatorArm64 # iOS framework (slow)
35
35
  ./gradlew :sample:androidApp:installDebug # run demo on device/emulator
36
36
  ```
@@ -50,7 +50,7 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
50
50
  - **Configuration flows via CompositionLocals**, not params. Public ones: `AppTheme`, `AppStatusBar`, `AppNavigationBar` (`const/Theme.kt`); `ApplicationContext`, `AppConfig`, `AppLanguage`, `LocalLocalize` (`application/`); `LocalNavigator`, `LocalMaxApi` (`navigation/`). Read with `X.current`.
51
51
  - **Design-system config** (`trustBanner`, `headerBar`, `headerGradient`): the host seeds `KitDesignSystemConfig.update(...)` once (same pattern as `KitFontScale`); `NavigationContainer` provides it as `LocalDesignSystemConfig` (param `designSystemConfig` → parent → global) and applies `headerBar` as the header image when the theme has none.
52
52
  - **Platform code** goes through `expect`/`actual` in `platform/` — never reference Android/iOS APIs from `commonMain` directly.
53
- - **Native bridge:** `IMaxApi` (`vn.momo.maxapi`, exposed as `LocalMaxApi`) is the host-app bridge (dismiss, tracking, device info, language).
53
+ - **Native bridge:** typed `IMaxApi` from NativeMaxApi `0.3.1` (`vn.momo.maxapi`, exposed as `LocalMaxApi`) is the host-app bridge. Toolkit uses `ToolkitOptions` to preserve custom groups and system-tool policy; pending callbacks are cancelled when their composition is disposed. Containers borrow the host client and must not close it.
54
54
  - Navigation uses a runtime `DynamicScreenRegistry`; overlays (modal/bottom-sheet/snackbar) go through `Navigator` + `OverplayComponentRegistry`.
55
55
  - **iOS swipe-back:** the host owns the only edge-pan recogniser and replays it through `HostBackGestureBridge` (`navigation/HostBackGestureBridge.kt`; the kit framework is never exported, so the host reaches it through its own Koin-provided facade): `checkCanPop` / `canPopInner` / `pop` route the back into the kit stack, `started` / `progressed` / `completed` / `cancelled` seek the NavHost transition, `shouldUseFloatingArrowBack` is set by bottom tabs. `NavigationContainer` attaches itself automatically (`AttachHostBackGesture`, iOS only, detached by identity on dispose) — hosts need no wrapper and no `setNavigator`. `maxApi` is a required parameter of `NavigationContainer` — every container, nested or root, is handed its host bridge explicitly.
56
56
 
package/README.md CHANGED
@@ -53,7 +53,7 @@ ComposeKit là bộ UI Kit đa nền tảng phục vụ Mini App/SDK của MoMo.
53
53
  ---
54
54
 
55
55
  ### Yêu cầu môi trường
56
- - JDK 17+, Gradle 8, Android Studio Hedgehog+ với Android SDK (compileSdk xem `libs.versions.toml`).
56
+ - JDK 17+ và Android SDK (compileSdk xem `libs.versions.toml`); wrapper Gradle `9.5.0`, AGP `9.2.1`.
57
57
  - Kotlin Multiplatform plugin + Compose Multiplatform (JetBrains 1.6.1+).
58
58
  - Xcode 15, CocoaPods 1.12+ để build `iosApp` hoặc publish Pod.
59
59
  - Truy cập mạng nội bộ GitLab Packages nếu cần publish/consume Maven repo `https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven`.
@@ -174,4 +174,12 @@ fun SuccessState() {
174
174
  ```
175
175
 
176
176
  - `shared/build.gradle.kts` đã bật `coreLibraryDesugaringEnabled` để tương thích `compottie-dot`/`compottie-network` với `minSdk = 24`.
177
- - Nếu animation dùng asset/font nội bộ, kết hợp `compottie-resources` (`rememberResourcesAssetsManager`, `rememberResourcesFontManager`) để trỏ về `composeResources`.
177
+ - Nếu animation dùng asset/font nội bộ, kết hợp `compottie-resources` (`rememberResourcesAssetsManager`, `rememberResourcesFontManager`) để trỏ về `composeResources`.
178
+ ### NativeMaxApi 0.3.1
179
+
180
+ Compose dùng typed `vn.momo.maxapi.IMaxApi` do host truyền vào `NavigationContainer`.
181
+ Toolkit dùng overload `showToolkit(ToolkitOptions, callback)` để giữ nhóm tool tùy chỉnh,
182
+ nhãn đa ngôn ngữ, icon, badge và `useSystemTools`. Callback trả `actionKey`; request đang chờ
183
+ được hủy khi composition bị gỡ. Kits không đóng client do host sở hữu.
184
+
185
+ Chạy `./gradlew :compose:testAndroidHostTest` để kiểm tra hủy request và callback đến muộn.
package/build.gradle.kts CHANGED
@@ -2,7 +2,9 @@ plugins {
2
2
  // this is necessary to avoid the plugins to be loaded multiple times
3
3
  // in each subproject's classloader
4
4
  alias(libs.plugins.android.application) apply false
5
- alias(libs.plugins.android.library) apply false
5
+ // AI-GENERATED START: use the AGP 9 Kotlin Multiplatform Android library target
6
+ alias(libs.plugins.android.kotlin.multiplatform.library) apply false
7
+ // AI-GENERATED END: use the AGP 9 Kotlin Multiplatform Android library target
6
8
  alias(libs.plugins.compose) apply false
7
9
  alias(libs.plugins.kotlin.compose.compiler) apply false
8
10
  alias(libs.plugins.jetbrains.kotlin.multiplatform) apply false
@@ -4,7 +4,9 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4
4
 
5
5
  plugins {
6
6
  alias(libs.plugins.jetbrains.kotlin.multiplatform)
7
- alias(libs.plugins.android.library)
7
+ // AI-GENERATED START: use the AGP 9 Kotlin Multiplatform Android library target
8
+ alias(libs.plugins.android.kotlin.multiplatform.library)
9
+ // AI-GENERATED END: use the AGP 9 Kotlin Multiplatform Android library target
8
10
  alias(libs.plugins.compose)
9
11
  alias(libs.plugins.kotlin.compose.compiler)
10
12
  // alias(libs.plugins.vanniktech.mavenPublish)
@@ -22,12 +24,18 @@ val gitlabUser: String = findProperty("gitlab.user") as String? ?: ""
22
24
  val gitlabKey: String = findProperty("gitlab.password") as String? ?: ""
23
25
 
24
26
  kotlin {
25
- androidTarget {
27
+ // AI-GENERATED START: configure the AGP 9 Android target and host tests
28
+ android {
29
+ namespace = "vn.momo.kits.kits"
30
+ compileSdk = 35
31
+ minSdk = 24
32
+ androidResources.enable = true
33
+ withHostTest {}
26
34
  compilerOptions {
27
35
  jvmTarget.set(JvmTarget.JVM_17)
28
36
  }
29
- publishLibraryVariants("release")
30
37
  }
38
+ // AI-GENERATED END: configure the AGP 9 Android target and host tests
31
39
 
32
40
  val fwName = gitlabArtifactId
33
41
  val iosTargets = listOf(iosArm64(), iosSimulatorArm64())
@@ -40,7 +48,7 @@ kotlin {
40
48
  }
41
49
 
42
50
  cocoapods {
43
- version = "0.168.1-beta.2-debug"
51
+ version = "0.168.1-beta.3-debug"
44
52
  summary = "IOS Shared module"
45
53
  homepage = "https://momo.vn"
46
54
  ios.deploymentTarget = "15.0"
@@ -86,24 +94,14 @@ kotlin {
86
94
  iosMain.dependencies {
87
95
  implementation(libs.ktor.client.darwin)
88
96
  }
97
+ // AI-GENERATED START: verify toolkit request ownership
98
+ commonTest.dependencies {
99
+ implementation(kotlin("test"))
100
+ }
101
+ // AI-GENERATED END: verify toolkit request ownership
89
102
  }
90
103
  }
91
104
 
92
- android {
93
- namespace = "vn.momo.kits.kits"
94
- compileSdk = 35
95
- compileOptions {
96
- sourceCompatibility = JavaVersion.VERSION_17
97
- targetCompatibility = JavaVersion.VERSION_17
98
- }
99
- defaultConfig {
100
- minSdk = 24
101
- }
102
- }
103
-
104
- dependencies {
105
- }
106
-
107
105
  repositories {
108
106
  google {
109
107
  mavenContent {
@@ -153,8 +151,9 @@ compose {
153
151
  // configure(
154
152
  // KotlinMultiplatform(
155
153
  // javadocJar = JavadocJar.Empty(),
154
+ // // AI-GENERATED START: the AGP KMP target publishes a single Android variant
156
155
  // sourcesJar = true,
157
- // androidVariantsToPublish = listOf("release")
156
+ // // AI-GENERATED END: the AGP KMP target publishes a single Android variant
158
157
  // )
159
158
  // )
160
159
  //
@@ -4,7 +4,9 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4
4
 
5
5
  plugins {
6
6
  alias(libs.plugins.jetbrains.kotlin.multiplatform)
7
- alias(libs.plugins.android.library)
7
+ // AI-GENERATED START: use the AGP 9 Kotlin Multiplatform Android library target
8
+ alias(libs.plugins.android.kotlin.multiplatform.library)
9
+ // AI-GENERATED END: use the AGP 9 Kotlin Multiplatform Android library target
8
10
  alias(libs.plugins.compose)
9
11
  alias(libs.plugins.kotlin.compose.compiler)
10
12
  alias(libs.plugins.vanniktech.mavenPublish)
@@ -22,12 +24,18 @@ val gitlabUser: String = findProperty("gitlab.user") as String? ?: ""
22
24
  val gitlabKey: String = findProperty("gitlab.password") as String? ?: ""
23
25
 
24
26
  kotlin {
25
- androidTarget {
27
+ // AI-GENERATED START: configure the AGP 9 Android target and host tests
28
+ android {
29
+ namespace = "$gitlabGroup.$gitlabArtifactId"
30
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
31
+ minSdk = libs.versions.android.minSdk.get().toInt()
32
+ androidResources.enable = true
33
+ withHostTest {}
26
34
  compilerOptions {
27
35
  jvmTarget.set(JvmTarget.JVM_17)
28
36
  }
29
- publishLibraryVariants("release")
30
37
  }
38
+ // AI-GENERATED END: configure the AGP 9 Android target and host tests
31
39
 
32
40
  val fwName = gitlabArtifactId
33
41
  val iosTargets = listOf(iosArm64(), iosSimulatorArm64())
@@ -86,24 +94,14 @@ kotlin {
86
94
  iosMain.dependencies {
87
95
  implementation(libs.ktor.client.darwin)
88
96
  }
97
+ // AI-GENERATED START: verify toolkit request ownership
98
+ commonTest.dependencies {
99
+ implementation(kotlin("test"))
100
+ }
101
+ // AI-GENERATED END: verify toolkit request ownership
89
102
  }
90
103
  }
91
104
 
92
- android {
93
- namespace = "$gitlabGroup.$gitlabArtifactId"
94
- compileSdk = libs.versions.android.compileSdk.get().toInt()
95
- compileOptions {
96
- sourceCompatibility = JavaVersion.VERSION_17
97
- targetCompatibility = JavaVersion.VERSION_17
98
- }
99
- defaultConfig {
100
- minSdk = libs.versions.android.minSdk.get().toInt()
101
- }
102
- }
103
-
104
- dependencies {
105
- }
106
-
107
105
  repositories {
108
106
  google {
109
107
  mavenContent {
@@ -153,8 +151,9 @@ mavenPublishing {
153
151
  configure(
154
152
  KotlinMultiplatform(
155
153
  javadocJar = JavadocJar.Empty(),
154
+ // AI-GENERATED START: the AGP KMP target publishes a single Android variant
156
155
  sourcesJar = true,
157
- androidVariantsToPublish = listOf("release")
156
+ // AI-GENERATED END: the AGP KMP target publishes a single Android variant
158
157
  )
159
158
  )
160
159
 
@@ -2,13 +2,18 @@ package vn.momo.kits.application
2
2
 
3
3
  import androidx.compose.runtime.Composable
4
4
  import androidx.compose.runtime.CompositionLocalProvider
5
- import androidx.compose.runtime.LaunchedEffect
5
+ // AI-GENERATED START: own the typed language request
6
+ import androidx.compose.runtime.DisposableEffect
7
+ // AI-GENERATED END: own the typed language request
6
8
  import androidx.compose.runtime.remember
7
9
  import vn.momo.kits.const.AppTheme
8
10
  import vn.momo.kits.const.Theme
9
11
  import vn.momo.kits.navigation.LocalMaxApi
10
12
  import vn.momo.kits.platform.ProvideOsFontScale
13
+ // AI-GENERATED START: consume typed NativeMaxApi results
11
14
  import vn.momo.maxapi.IMaxApi
15
+ import vn.momo.maxapi.runtime.ApiResult
16
+ // AI-GENERATED END: consume typed NativeMaxApi results
12
17
 
13
18
 
14
19
  @Composable
@@ -27,15 +32,25 @@ fun WidgetContainer(
27
32
  val resolvedLocalize = localize ?: LocalLocalize.current
28
33
  val resolvedMaxApi = maxApi ?: LocalMaxApi.current
29
34
 
30
- LaunchedEffect(resolvedMaxApi, localize) {
31
- val api = resolvedMaxApi ?: return@LaunchedEffect
32
- val target = localize ?: return@LaunchedEffect
33
- runCatching {
34
- api.getLanguage(callback = { data ->
35
- if (parseLanguage(data) == Localize.EN) target.changeLanguage(Localize.EN)
36
- })
35
+ // AI-GENERATED START: cancel language delivery when its composition owner changes
36
+ DisposableEffect(resolvedMaxApi, localize) {
37
+ val api = resolvedMaxApi
38
+ val target = localize
39
+ var active = true
40
+ val handle = runCatching {
41
+ api?.getLanguage { result ->
42
+ if (active && target != null && result is ApiResult.Success &&
43
+ result.value.trim().lowercase().startsWith(Localize.EN)) {
44
+ target.changeLanguage(Localize.EN)
45
+ }
46
+ }
47
+ }.getOrNull()
48
+ onDispose {
49
+ active = false
50
+ handle?.cancel()
37
51
  }
38
52
  }
53
+ // AI-GENERATED END: cancel language delivery when its composition owner changes
39
54
 
40
55
  ProvideOsFontScale {
41
56
  CompositionLocalProvider(
@@ -48,9 +63,3 @@ fun WidgetContainer(
48
63
  }
49
64
  }
50
65
  }
51
-
52
- private fun parseLanguage(raw: Map<String, Any?>?): String? {
53
- val code = (raw?.get("response") as? String)?.trim()?.lowercase()
54
- if (code.isNullOrEmpty()) return null
55
- return if (code.startsWith(Localize.EN)) Localize.EN else Localize.VI
56
- }
@@ -37,7 +37,10 @@ import vn.momo.kits.platform.ProvideNavigationEventDispatcherOwner
37
37
  import vn.momo.kits.platform.ProvideOsFontScale
38
38
  import vn.momo.kits.utils.getAppStatusBarHeight
39
39
  import vn.momo.kits.utils.getNavigationBarHeight
40
+ // AI-GENERATED START: consume typed NativeMaxApi results
40
41
  import vn.momo.maxapi.IMaxApi
42
+ import vn.momo.maxapi.runtime.ApiResult
43
+ // AI-GENERATED END: consume typed NativeMaxApi results
41
44
  import kotlin.time.Duration.Companion.milliseconds
42
45
 
43
46
  @Composable
@@ -55,7 +58,11 @@ fun NavigationContainer(
55
58
  ) {
56
59
  val navController = rememberNavController()
57
60
  val registry = remember { DynamicScreenRegistry() }
58
- val navigator = remember { Navigator(navController = navController, maxApi = maxApi, registry = registry) }
61
+ // AI-GENERATED START: retain the current host client after recomposition
62
+ val navigator = remember(navController, maxApi, registry) {
63
+ Navigator(navController = navController, maxApi = maxApi, registry = registry)
64
+ }
65
+ // AI-GENERATED END: retain the current host client after recomposition
59
66
  val statusBarHeight = statusBarHeight ?: getAppStatusBarHeight()
60
67
  val navigationBarHeight = getNavigationBarHeight()
61
68
  val screenLayout = calculateScreenLayout(statusBarHeight, parent = LocalScreenLayout.current)
@@ -80,16 +87,25 @@ fun NavigationContainer(
80
87
 
81
88
  val resolvedLocalize = localize ?: fallbackLocalize
82
89
 
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)
90
+ // AI-GENERATED START: cancel language delivery when its composition owner changes
91
+ DisposableEffect(maxApi, resolvedLocalize) {
92
+ val api = maxApi
93
+ val target = resolvedLocalize
94
+ var active = true
95
+ val handle = runCatching {
96
+ api.getLanguage { result ->
97
+ if (active && result is ApiResult.Success &&
98
+ result.value.trim().lowercase().startsWith(Localize.EN)) {
99
+ target.changeLanguage(Localize.EN)
89
100
  }
90
- })
101
+ }
102
+ }.getOrNull()
103
+ onDispose {
104
+ active = false
105
+ handle?.cancel()
91
106
  }
92
107
  }
108
+ // AI-GENERATED END: cancel language delivery when its composition owner changes
93
109
 
94
110
  val screenId = rememberSaveable { registry.nextId() }
95
111
  registry.bind(screenId, initialScreenName, initialScreen, options)
@@ -197,9 +213,3 @@ fun NavigationContainer(
197
213
 
198
214
  val LocalMaxApi = staticCompositionLocalOf<IMaxApi?> { null }
199
215
 
200
- private fun parseLanguage(raw: Map<String, Any?>?): String? {
201
- val code = (raw?.get("response") as? String)?.trim()?.lowercase()
202
- if (code.isNullOrEmpty()) return null
203
- return if (code.startsWith(Localize.EN)) Localize.EN else Localize.VI
204
- }
205
-
@@ -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(
@@ -125,7 +128,9 @@ class Navigator(
125
128
  delay(STACK_TRANSITION_DURATION.milliseconds)
126
129
  dismissing?.let { registry.unregisterScreen(it.id) }
127
130
  } else {
128
- maxApi?.dismiss { }
131
+ // AI-GENERATED START: preserve root-screen dismissal
132
+ maxApi?.dismiss(DismissTarget.Current()) {}
133
+ // AI-GENERATED END: preserve root-screen dismissal
129
134
  }
130
135
  }
131
136
 
@@ -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
@@ -39,6 +39,9 @@ import vn.momo.kits.platform.BackHandler
39
39
  import vn.momo.kits.platform.reportFloatingArrowBack
40
40
  import vn.momo.kits.layout.getScreenLayout
41
41
  import kotlinx.coroutines.launch
42
+ // AI-GENERATED START: use the typed tab haptic pattern
43
+ import vn.momo.maxapi.contract.device.vibration.model.VibrateStyle
44
+ // AI-GENERATED END: use the typed tab haptic pattern
42
45
 
43
46
  private val inOutQuad = Easing { t -> if (t < 0.5f) 2 * t * t else 1 - 2 * (1 - t) * (1 - t) }
44
47
  internal val tabTimingSpec = tween<Float>(durationMillis = 200, easing = inOutQuad)
@@ -132,7 +135,9 @@ fun BottomTab(
132
135
  it.bottom + duoRailHeight(items.size, hasQr, compressed = compression.tabBarCompressed) + DUO_GROUP_GAP
133
136
  } ?: 0.dp
134
137
  val onTabSelected: (Int) -> Unit = { index ->
135
- runCatching { maxApi?.triggerEventVibration("light", callback = null) }
138
+ // AI-GENERATED START: preserve the light haptic on tab selection
139
+ runCatching { maxApi?.vibrate(VibrateStyle.LIGHT) }
140
+ // AI-GENERATED END: preserve the light haptic on tab selection
136
141
  val switchTab = {
137
142
  if (index != selectedIndex) selectTab(index)
138
143
  }
@@ -17,6 +17,17 @@ 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
20
31
  import androidx.compose.runtime.derivedStateOf
21
32
  import androidx.compose.runtime.getValue
22
33
  import androidx.compose.runtime.mutableStateOf
@@ -95,53 +106,56 @@ internal fun Toolkit(
95
106
  var isFavorite by remember { mutableStateOf(false) }
96
107
  val isLoading by remember { mutableStateOf(false) }
97
108
 
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
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
102
117
  }
103
118
  }
104
119
 
105
120
  fun onPressShortcut() {
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
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 }
111
127
  }
112
128
  }
113
129
  }
114
130
 
115
131
  fun onPressMore() {
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
- }
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)
127
143
  }
128
144
  }
145
+ // AI-GENERATED END: retain custom tool data and typed callback outcomes
129
146
 
130
147
  fun onPressHelpCenter() {
131
148
  if (context == null) return
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
- ) {}
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
141
153
  }
142
154
 
143
155
  fun onPressClose() {
144
- if (headerRight.useCloseIcon) api?.dismiss { } else api?.dismissAll { }
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
145
159
  }
146
160
 
147
161
  val useHelpCenter = usesHelpCenter(context)
@@ -0,0 +1,37 @@
1
+ // File: HelpCenterRequest.kt
2
+ // Created At: 2026-10-19 +07:00
3
+ // Created By: AI | AI Agent: Codex | Model: GPT-6
4
+ // AI-GENERATED START: preserve the toolkit Help Center payload in the typed request
5
+ package vn.momo.kits.navigation.component
6
+
7
+ import kotlinx.serialization.json.JsonArray
8
+ import kotlinx.serialization.json.JsonElement
9
+ import kotlinx.serialization.json.JsonNull
10
+ import kotlinx.serialization.json.JsonObject
11
+ import kotlinx.serialization.json.JsonPrimitive
12
+ import vn.momo.kits.application.MiniAppContext
13
+ import vn.momo.maxapi.contract.navigation.model.BottomSheetRequest
14
+ import vn.momo.maxapi.contract.navigation.model.HelpCenterContext
15
+
16
+ internal fun MiniAppContext.helpCenterRequest() = BottomSheetRequest.HelpCenter(
17
+ HelpCenterContext(
18
+ appId = appId,
19
+ code = appCode,
20
+ name = appName?.helpCenterJson(),
21
+ icon = appIcon,
22
+ description = description?.helpCenterJson(),
23
+ ),
24
+ )
25
+
26
+ private fun Any?.helpCenterJson(): JsonElement = when (this) {
27
+ null -> JsonNull
28
+ is JsonElement -> this
29
+ is String -> JsonPrimitive(this)
30
+ is Boolean -> JsonPrimitive(this)
31
+ is Number -> JsonPrimitive(this)
32
+ is Map<*, *> -> JsonObject(entries.associate { (key, value) -> key.toString() to value.helpCenterJson() })
33
+ is Iterable<*> -> JsonArray(map { it.helpCenterJson() })
34
+ is Array<*> -> JsonArray(map { it.helpCenterJson() })
35
+ else -> JsonPrimitive(toString())
36
+ }
37
+ // AI-GENERATED END: preserve the toolkit Help Center payload in the typed request
@@ -0,0 +1,41 @@
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
+ // AI-GENERATED START: retain subscriptions across nonterminal sheet events
27
+ fun <T> observe(request: (ApiCallback<T>) -> ApiRequestHandle, onResult: ApiCallback<T>) {
28
+ if (!active) return
29
+ val handle = request { result -> if (active) onResult(result) }
30
+ if (active) handles.add(handle) else handle.cancel()
31
+ }
32
+ // AI-GENERATED END: retain subscriptions across nonterminal sheet events
33
+
34
+ fun close() {
35
+ active = false
36
+ val pending = handles.toList()
37
+ handles.clear()
38
+ pending.forEach { it.cancel() }
39
+ }
40
+ }
41
+ // AI-GENERATED END: cancel only toolkit requests owned by this composition
@@ -2,6 +2,12 @@ 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
5
11
  import kotlin.time.Clock
6
12
  import kotlin.time.ExperimentalTime
7
13
 
@@ -15,6 +21,7 @@ class ScreenTrackingState {
15
21
  var action: String = "push"
16
22
  }
17
23
 
24
+ // AI-GENERATED START: retain screen event names and typed tracking attributes
18
25
  object ScreenTracker {
19
26
 
20
27
  private var lastScreenName: String? = null
@@ -31,13 +38,13 @@ object ScreenTracker {
31
38
  val previousScreen = lastScreenName
32
39
  lastScreenName = screenName
33
40
 
34
- maxApi?.track(
41
+ maxApi?.trackEvent(
35
42
  eventName = "auto_screen_navigated",
36
- params = buildBaseParams(context, screenName).apply {
37
- put("pre_screen_name", previousScreen)
38
- put("state", "navigated")
39
- put("action", action)
40
- }
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
+ })
41
48
  )
42
49
 
43
50
  state.releaseNavigated = true
@@ -53,12 +60,12 @@ object ScreenTracker {
53
60
  ) {
54
61
  if (state.releaseLoad) return
55
62
 
56
- maxApi?.track(
63
+ maxApi?.trackEvent(
57
64
  eventName = "auto_screen_displayed",
58
- params = buildBaseParams(context, screenName).apply {
59
- put("state", "load")
60
- put("duration", duration)
61
- }
65
+ attributes = JsonObject(buildBaseParams(context, screenName).apply {
66
+ put("state", JsonPrimitive("load"))
67
+ put("duration", JsonPrimitive(duration))
68
+ })
62
69
  )
63
70
 
64
71
  state.releaseLoad = true
@@ -74,13 +81,13 @@ object ScreenTracker {
74
81
  ) {
75
82
  if (state.releaseInteraction) return
76
83
 
77
- maxApi?.track(
84
+ maxApi?.trackEvent(
78
85
  eventName = "auto_screen_interacted",
79
- params = buildBaseParams(context, screenName).apply {
80
- put("state", "interaction")
81
- put("duration", duration)
82
- put("total_duration", totalDuration)
83
- }
86
+ attributes = JsonObject(buildBaseParams(context, screenName).apply {
87
+ put("state", JsonPrimitive("interaction"))
88
+ put("duration", JsonPrimitive(duration))
89
+ put("total_duration", JsonPrimitive(totalDuration))
90
+ })
84
91
  )
85
92
 
86
93
  state.releaseInteraction = true
@@ -90,13 +97,13 @@ object ScreenTracker {
90
97
  context: MiniAppContext?,
91
98
  screenName: String,
92
99
  componentName: String = "Screen"
93
- ): MutableMap<String, Any?> {
100
+ ): MutableMap<String, JsonElement> {
94
101
  return mutableMapOf(
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
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)
100
107
  )
101
108
  }
102
109
 
@@ -105,16 +112,16 @@ object ScreenTracker {
105
112
  context: MiniAppContext?,
106
113
  parentScreenName: String?
107
114
  ) {
108
- maxApi?.track(
115
+ maxApi?.trackEvent(
109
116
  eventName = "auto_popup_displayed",
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
- )
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
+ ))
118
125
  )
119
126
  }
120
127
 
@@ -125,3 +132,4 @@ object ScreenTracker {
125
132
  lastScreenName = null
126
133
  }
127
134
  }
135
+ // AI-GENERATED END: retain screen event names and typed tracking attributes
@@ -0,0 +1,44 @@
1
+ // File: HelpCenterRequestTest.kt
2
+ // Created At: 2026-10-19 +07:00
3
+ // Created By: AI | AI Agent: Codex | Model: GPT-6
4
+ // AI-GENERATED START: verify Help Center identity and localized payload preservation
5
+ package vn.momo.kits.navigation.component
6
+
7
+ import kotlinx.serialization.json.JsonObject
8
+ import kotlinx.serialization.json.JsonPrimitive
9
+ import vn.momo.kits.application.MiniAppContext
10
+ import kotlin.test.Test
11
+ import kotlin.test.assertEquals
12
+ import kotlin.test.assertNull
13
+
14
+ class HelpCenterRequestTest {
15
+ @Test
16
+ fun preservesApplicationIdentityAndLocalizedObjects() {
17
+ val context = MiniAppContext(
18
+ appId = "app-id",
19
+ appCode = "app-code",
20
+ appIcon = "app-icon",
21
+ appName = mapOf("vi" to "Tên ứng dụng", "en" to "App name"),
22
+ description = mapOf("vi" to "Mô tả", "en" to "Description"),
23
+ )
24
+ val request = context.helpCenterRequest().context
25
+ assertEquals(context.appId, request.appId)
26
+ assertEquals(context.appCode, request.code)
27
+ assertEquals(context.appIcon, request.icon)
28
+ assertEquals(JsonObject(mapOf("vi" to JsonPrimitive("Tên ứng dụng"),
29
+ "en" to JsonPrimitive("App name"))), request.name)
30
+ assertEquals(JsonObject(mapOf("vi" to JsonPrimitive("Mô tả"),
31
+ "en" to JsonPrimitive("Description"))), request.description)
32
+ assertNull(request.support)
33
+ assertNull(request.toolkitConfig)
34
+ assertNull(request.permissions)
35
+ }
36
+
37
+ @Test
38
+ fun preservesPlainStringsAndAbsentValues() {
39
+ val request = MiniAppContext(appName = "App name").helpCenterRequest().context
40
+ assertEquals(JsonPrimitive("App name"), request.name)
41
+ assertNull(request.description)
42
+ }
43
+ }
44
+ // AI-GENERATED END: verify Help Center identity and localized payload preservation
@@ -0,0 +1,62 @@
1
+ // File: ToolkitRequestsTest.kt
2
+ // Created At: 2026-10-12 +07:00 | Created By: AI | AI Agent: Codex | Model: GPT-6
3
+ // AI-GENERATED START: verify cancellation and late callback suppression for toolkit UI
4
+ package vn.momo.kits.navigation.component
5
+
6
+ import kotlin.test.Test
7
+ import kotlin.test.assertEquals
8
+ import vn.momo.maxapi.runtime.ApiCallback
9
+ import vn.momo.maxapi.runtime.ApiRequestHandle
10
+ import vn.momo.maxapi.runtime.ApiResult
11
+
12
+ class ToolkitRequestsTest {
13
+ @Test
14
+ fun closingCancelsPendingRequestsAndSuppressesLateCallbacks() {
15
+ val owner = ToolkitRequests()
16
+ val handle = Handle()
17
+ var callback: ApiCallback<Boolean> = {}
18
+ var results = 0
19
+ owner.launch<Boolean>({ callback = it; handle }) { results++ }
20
+ owner.close()
21
+ owner.close()
22
+ callback(ApiResult.Success(true))
23
+ assertEquals(1, handle.cancellations)
24
+ assertEquals(0, results)
25
+ }
26
+
27
+ @Test
28
+ fun completedRequestsAreReleasedAndClosedOwnersNeverStartNewRequests() {
29
+ val owner = ToolkitRequests()
30
+ val handle = Handle()
31
+ var starts = 0
32
+ var results = 0
33
+ owner.launch<Boolean>({ callback -> starts++; callback(ApiResult.Success(false)); handle }) { results++ }
34
+ owner.close()
35
+ owner.launch<Boolean>({ starts++; handle }) { results++ }
36
+ assertEquals(0, handle.cancellations)
37
+ assertEquals(1, starts)
38
+ assertEquals(1, results)
39
+ }
40
+
41
+ @Test
42
+ fun sheetEventsKeepTheirSubscriptionUntilTheOwnerCloses() {
43
+ val owner = ToolkitRequests()
44
+ val handle = Handle()
45
+ var callback: ApiCallback<Boolean> = {}
46
+ var results = 0
47
+ owner.observe<Boolean>({ callback = it; handle }) { results++ }
48
+ callback(ApiResult.Success(true))
49
+ callback(ApiResult.Success(false))
50
+ owner.close()
51
+ callback(ApiResult.Success(true))
52
+ assertEquals(2, results)
53
+ assertEquals(1, handle.cancellations)
54
+ }
55
+
56
+ private class Handle : ApiRequestHandle {
57
+ override val requestId = "toolkit-test"
58
+ var cancellations = 0
59
+ override fun cancel() { cancellations++ }
60
+ }
61
+ }
62
+ // AI-GENERATED END: verify cancellation and late callback suppression for toolkit UI
@@ -12,7 +12,9 @@ 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
- DisposableEffect(callback) {
15
+ // AI-GENERATED START: capture the current typed logger client
16
+ DisposableEffect(callback, maxApi) {
17
+ // AI-GENERATED END: capture the current typed logger client
16
18
  val token: NSObjectProtocol? = if (callback != null) {
17
19
  NSNotificationCenter.defaultCenter.addObserverForName(
18
20
  name = STATUS_BAR_TAPPED_NOTIFICATION,
@@ -20,7 +22,9 @@ internal actual fun RegisterScrollToTop(callback: (() -> Unit)?) {
20
22
  queue = NSOperationQueue.mainQueue,
21
23
  ) { _: NSNotification? ->
22
24
  runCatching { callback() }.onFailure {
23
- maxApi?.logFile("[ScrollToTop] callback threw: ${it.message}") {}
25
+ // AI-GENERATED START: use typed host logging
26
+ maxApi?.writeLog("[ScrollToTop] callback threw: ${it.message}")
27
+ // AI-GENERATED END: use typed host logging
24
28
 
25
29
  }
26
30
  }
@@ -1,5 +1,7 @@
1
1
  [versions]
2
- android-compileSdk = "37"
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
3
5
  android-minSdk = "24"
4
6
  android-targetSdk = "36"
5
7
  ktor-version = "3.0.0-rc-1"
@@ -9,7 +11,9 @@ kSerialize = "1.7.1"
9
11
  compose = "1.10.3"
10
12
  navigation-multiplatform = "2.9.2"
11
13
  coil3-multiplatform = "3.0.0-alpha10"
12
- androidGradlePlugin = "8.13.2"
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
13
17
  r8 = "8.9.35"
14
18
  kotlinx-datetime = "0.7.1"
15
19
  airbnb-lottie = "5.2.0"
@@ -17,10 +21,9 @@ compottie = "2.1.0"
17
21
  androidx-activity = "1.9.1"
18
22
  androidx-appcompat = "1.7.0"
19
23
  material = "1.10.0"
20
- maxapi = "0.1.1"
21
24
  vanniktechMavenPublish = "0.34.0"
22
25
  kits = "0.167.1-duo.1"
23
- nativemaxapi = "0.0.6"
26
+ nativemaxapi = "0.3.9"
24
27
  gif-image = "0.1.2"
25
28
 
26
29
  [libraries]
@@ -67,3 +70,7 @@ compose = { id = "org.jetbrains.compose", version.ref = "compose" }
67
70
  android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
68
71
  android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
69
72
  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,7 +1,9 @@
1
1
  #Thu Jul 31 17:05:40 ICT 2025
2
2
  distributionBase=GRADLE_USER_HOME
3
3
  distributionPath=wrapper/dists
4
- distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
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
5
7
  networkTimeout=10000
6
8
  validateDistributionUrl=true
7
9
  zipStoreBase=GRADLE_USER_HOME
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.167.1-duo.15
21
+ version=0.300.4
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.168.1-beta.2-debug",
3
+ "version": "0.168.1-beta.3-debug",
4
4
  "private": false,
5
5
  "dependeNavigationncies": {},
6
6
  "devDependencies": {},
package/publish.sh CHANGED
@@ -374,8 +374,13 @@ phase_publish_npm() {
374
374
  echo ""
375
375
  # A bad token surfaces as a 404 on the PUT, after the working tree was already rewritten
376
376
  # below — so verify auth first, before anything is touched.
377
- if ! npm whoami --registry https://registry.npmjs.org >/dev/null 2>&1; then
378
- echo "❌ Error: npm auth failed (npm whoami). Renew the token in .npmrc / ~/.npmrc and retry."
377
+ if ! NPM_WHOAMI_OUTPUT=$(npm whoami --registry https://registry.npmjs.org 2>&1); then
378
+ echo "$NPM_WHOAMI_OUTPUT"
379
+ if echo "$NPM_WHOAMI_OUTPUT" | grep -qE "E401|ENEEDAUTH"; then
380
+ echo "❌ Error: npm auth failed (npm whoami). Renew the token in .npmrc / ~/.npmrc and retry."
381
+ else
382
+ echo "❌ Error: npm whoami failed (see above) — likely network / VPN to registry.npmjs.org. Retry in a moment."
383
+ fi
379
384
  exit 1
380
385
  fi
381
386
 
@@ -1,9 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Read(//Users/trinh.ho2/**)",
5
- "Bash(npm config *)",
6
- "Bash(npm whoami *)"
7
- ]
8
- }
9
- }