@momo-kits/native-kits 0.168.1-beta.4-debug → 0.168.1-beta.5-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CLAUDE.md +4 -4
  2. package/README.md +2 -10
  3. package/build.gradle.kts +1 -3
  4. package/compose/build.gradle.kts +20 -19
  5. package/compose/build.gradle.kts.backup +19 -18
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +14 -23
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +5 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +14 -24
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +1 -6
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +1 -6
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +1 -6
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +2 -3
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +124 -59
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +30 -44
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +32 -40
  17. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +2 -6
  18. package/gradle/libs.versions.toml +4 -11
  19. package/gradle/wrapper/gradle-wrapper.properties +1 -3
  20. package/gradle.properties +3 -3
  21. package/ios/Layout/ScreenLayout.swift +5 -0
  22. package/ios/Navigation/BottomTab/DuoBarCompression.swift +2 -3
  23. package/ios/Navigation/BottomTab/DuoTabBar.swift +94 -69
  24. package/ios/Navigation/Component/HeaderBackground.swift +22 -1
  25. package/ios/Navigation/StackScreen.swift +3 -7
  26. package/package.json +1 -1
  27. package/publish.sh +2 -33
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +0 -37
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +0 -41
  30. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +0 -44
  31. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +0 -62
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: 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`).
10
+ - Targets: `androidTarget` (JVM 17, compile/min/target SDK 36/24/36), `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
- 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.
29
+ No unit-test suite is configured. Validate changes by compiling the relevant target(s); 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:compileAndroidMain # Android target
33
+ ./gradlew :compose:compileDebugKotlinAndroid # 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:** 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.
53
+ - **Native bridge:** `IMaxApi` (`vn.momo.maxapi`, exposed as `LocalMaxApi`) is the host-app bridge (dismiss, tracking, device info, language).
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+ và Android SDK (compileSdk xem `libs.versions.toml`); wrapper Gradle `9.5.0`, AGP `9.2.1`.
56
+ - JDK 17+, Gradle 8, Android Studio Hedgehog+ với Android SDK (compileSdk xem `libs.versions.toml`).
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,12 +174,4 @@ 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`.
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.
177
+ - Nếu animation dùng asset/font nội bộ, kết hợp `compottie-resources` (`rememberResourcesAssetsManager`, `rememberResourcesFontManager`) để trỏ về `composeResources`.
package/build.gradle.kts CHANGED
@@ -2,9 +2,7 @@ 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
- // 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
5
+ alias(libs.plugins.android.library) apply false
8
6
  alias(libs.plugins.compose) apply false
9
7
  alias(libs.plugins.kotlin.compose.compiler) apply false
10
8
  alias(libs.plugins.jetbrains.kotlin.multiplatform) apply false
@@ -4,9 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4
4
 
5
5
  plugins {
6
6
  alias(libs.plugins.jetbrains.kotlin.multiplatform)
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
7
+ alias(libs.plugins.android.library)
10
8
  alias(libs.plugins.compose)
11
9
  alias(libs.plugins.kotlin.compose.compiler)
12
10
  // alias(libs.plugins.vanniktech.mavenPublish)
@@ -24,18 +22,12 @@ val gitlabUser: String = findProperty("gitlab.user") as String? ?: ""
24
22
  val gitlabKey: String = findProperty("gitlab.password") as String? ?: ""
25
23
 
26
24
  kotlin {
27
- // AI-GENERATED START: configure the AGP 9 Android target and host tests
28
- android {
29
- namespace = "vn.momo.kits.kits"
30
- compileSdk = 36
31
- minSdk = 24
32
- androidResources.enable = true
33
- withHostTest {}
25
+ androidTarget {
34
26
  compilerOptions {
35
27
  jvmTarget.set(JvmTarget.JVM_17)
36
28
  }
29
+ publishLibraryVariants("release")
37
30
  }
38
- // AI-GENERATED END: configure the AGP 9 Android target and host tests
39
31
 
40
32
  val fwName = gitlabArtifactId
41
33
  val iosTargets = listOf(iosArm64(), iosSimulatorArm64())
@@ -48,7 +40,7 @@ kotlin {
48
40
  }
49
41
 
50
42
  cocoapods {
51
- version = "0.168.1-beta.4-debug"
43
+ version = "0.168.1-beta.5-debug"
52
44
  summary = "IOS Shared module"
53
45
  homepage = "https://momo.vn"
54
46
  ios.deploymentTarget = "15.0"
@@ -94,14 +86,24 @@ kotlin {
94
86
  iosMain.dependencies {
95
87
  implementation(libs.ktor.client.darwin)
96
88
  }
97
- // AI-GENERATED START: verify toolkit request ownership
98
- commonTest.dependencies {
99
- implementation(kotlin("test"))
100
- }
101
- // AI-GENERATED END: verify toolkit request ownership
102
89
  }
103
90
  }
104
91
 
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
+
105
107
  repositories {
106
108
  google {
107
109
  mavenContent {
@@ -151,9 +153,8 @@ compose {
151
153
  // configure(
152
154
  // KotlinMultiplatform(
153
155
  // javadocJar = JavadocJar.Empty(),
154
- // // AI-GENERATED START: the AGP KMP target publishes a single Android variant
155
156
  // sourcesJar = true,
156
- // // AI-GENERATED END: the AGP KMP target publishes a single Android variant
157
+ // androidVariantsToPublish = listOf("release")
157
158
  // )
158
159
  // )
159
160
  //
@@ -4,9 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4
4
 
5
5
  plugins {
6
6
  alias(libs.plugins.jetbrains.kotlin.multiplatform)
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
7
+ alias(libs.plugins.android.library)
10
8
  alias(libs.plugins.compose)
11
9
  alias(libs.plugins.kotlin.compose.compiler)
12
10
  alias(libs.plugins.vanniktech.mavenPublish)
@@ -24,18 +22,12 @@ val gitlabUser: String = findProperty("gitlab.user") as String? ?: ""
24
22
  val gitlabKey: String = findProperty("gitlab.password") as String? ?: ""
25
23
 
26
24
  kotlin {
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 {}
25
+ androidTarget {
34
26
  compilerOptions {
35
27
  jvmTarget.set(JvmTarget.JVM_17)
36
28
  }
29
+ publishLibraryVariants("release")
37
30
  }
38
- // AI-GENERATED END: configure the AGP 9 Android target and host tests
39
31
 
40
32
  val fwName = gitlabArtifactId
41
33
  val iosTargets = listOf(iosArm64(), iosSimulatorArm64())
@@ -94,14 +86,24 @@ kotlin {
94
86
  iosMain.dependencies {
95
87
  implementation(libs.ktor.client.darwin)
96
88
  }
97
- // AI-GENERATED START: verify toolkit request ownership
98
- commonTest.dependencies {
99
- implementation(kotlin("test"))
100
- }
101
- // AI-GENERATED END: verify toolkit request ownership
102
89
  }
103
90
  }
104
91
 
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
+
105
107
  repositories {
106
108
  google {
107
109
  mavenContent {
@@ -151,9 +153,8 @@ mavenPublishing {
151
153
  configure(
152
154
  KotlinMultiplatform(
153
155
  javadocJar = JavadocJar.Empty(),
154
- // AI-GENERATED START: the AGP KMP target publishes a single Android variant
155
156
  sourcesJar = true,
156
- // AI-GENERATED END: the AGP KMP target publishes a single Android variant
157
+ androidVariantsToPublish = listOf("release")
157
158
  )
158
159
  )
159
160
 
@@ -2,18 +2,13 @@ package vn.momo.kits.application
2
2
 
3
3
  import androidx.compose.runtime.Composable
4
4
  import androidx.compose.runtime.CompositionLocalProvider
5
- // AI-GENERATED START: own the typed language request
6
- import androidx.compose.runtime.DisposableEffect
7
- // AI-GENERATED END: own the typed language request
5
+ import androidx.compose.runtime.LaunchedEffect
8
6
  import androidx.compose.runtime.remember
9
7
  import vn.momo.kits.const.AppTheme
10
8
  import vn.momo.kits.const.Theme
11
9
  import vn.momo.kits.navigation.LocalMaxApi
12
10
  import vn.momo.kits.platform.ProvideOsFontScale
13
- // AI-GENERATED START: consume typed NativeMaxApi results
14
11
  import vn.momo.maxapi.IMaxApi
15
- import vn.momo.maxapi.runtime.ApiResult
16
- // AI-GENERATED END: consume typed NativeMaxApi results
17
12
 
18
13
 
19
14
  @Composable
@@ -32,25 +27,15 @@ fun WidgetContainer(
32
27
  val resolvedLocalize = localize ?: LocalLocalize.current
33
28
  val resolvedMaxApi = maxApi ?: LocalMaxApi.current
34
29
 
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()
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
+ })
51
37
  }
52
38
  }
53
- // AI-GENERATED END: cancel language delivery when its composition owner changes
54
39
 
55
40
  ProvideOsFontScale {
56
41
  CompositionLocalProvider(
@@ -63,3 +48,9 @@ fun WidgetContainer(
63
48
  }
64
49
  }
65
50
  }
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
+ }
@@ -86,6 +86,11 @@ internal object DuoMetrics {
86
86
  // (x 550–643 of 669, clock digits centered on y 47); its reserved region is taller than the glyphs.
87
87
  val innerStatusBarCenterY = 47.dp
88
88
  val innerStatusBarWidth = 120.dp
89
+ // Host app's iOS 26 tab bar on the inner display in portrait: a 62pt glass pill sized to its tabs,
90
+ // QR as a 62pt platter beside it, ≥ 20pt from the sides, 13pt into the home indicator inset.
91
+ val floatingTabBarHeight = 62.dp
92
+ val floatingTabBarMargin = 20.dp
93
+ val floatingTabBarInsetOverlap = 13.dp
89
94
  }
90
95
 
91
96
  // Widest short side of any single-display iPhone.
@@ -37,10 +37,7 @@ 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
41
40
  import vn.momo.maxapi.IMaxApi
42
- import vn.momo.maxapi.runtime.ApiResult
43
- // AI-GENERATED END: consume typed NativeMaxApi results
44
41
  import kotlin.time.Duration.Companion.milliseconds
45
42
 
46
43
  @Composable
@@ -58,11 +55,7 @@ fun NavigationContainer(
58
55
  ) {
59
56
  val navController = rememberNavController()
60
57
  val registry = remember { DynamicScreenRegistry() }
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
58
+ val navigator = remember { Navigator(navController = navController, maxApi = maxApi, registry = registry) }
66
59
  val statusBarHeight = statusBarHeight ?: getAppStatusBarHeight()
67
60
  val navigationBarHeight = getNavigationBarHeight()
68
61
  val screenLayout = calculateScreenLayout(statusBarHeight, parent = LocalScreenLayout.current)
@@ -87,25 +80,16 @@ fun NavigationContainer(
87
80
 
88
81
  val resolvedLocalize = localize ?: fallbackLocalize
89
82
 
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)
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)
100
89
  }
101
- }
102
- }.getOrNull()
103
- onDispose {
104
- active = false
105
- handle?.cancel()
90
+ })
106
91
  }
107
92
  }
108
- // AI-GENERATED END: cancel language delivery when its composition owner changes
109
93
 
110
94
  val screenId = rememberSaveable { registry.nextId() }
111
95
  registry.bind(screenId, initialScreenName, initialScreen, options)
@@ -213,3 +197,9 @@ fun NavigationContainer(
213
197
 
214
198
  val LocalMaxApi = staticCompositionLocalOf<IMaxApi?> { null }
215
199
 
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,9 +22,6 @@ 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
28
25
  import kotlin.time.Duration.Companion.milliseconds
29
26
 
30
27
  class Navigator(
@@ -128,9 +125,7 @@ class Navigator(
128
125
  delay(STACK_TRANSITION_DURATION.milliseconds)
129
126
  dismissing?.let { registry.unregisterScreen(it.id) }
130
127
  } else {
131
- // AI-GENERATED START: preserve root-screen dismissal
132
- maxApi?.dismiss(DismissTarget.Current()) {}
133
- // AI-GENERATED END: preserve root-screen dismissal
128
+ maxApi?.dismiss { }
134
129
  }
135
130
  }
136
131
 
@@ -6,9 +6,6 @@ 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
12
9
 
13
10
  // Unscaled, so the tick lands at the same physical distance on every device.
14
11
  private const val SWIPE_BACK_HAPTIC_THRESHOLD_DP = 50f
@@ -28,9 +25,7 @@ internal fun SwipeBackHaptic(maxApi: IMaxApi?) {
28
25
  snapshotFlow { progress.value }.collect { fraction ->
29
26
  val travelled = fraction ?: 0f
30
27
  if (travelled >= threshold && !isSendHaptic) {
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
28
+ runCatching { maxApi.triggerEventVibration("light", callback = null) }
34
29
  isSendHaptic = true
35
30
  } else if (travelled < threshold && isSendHaptic) {
36
31
  isSendHaptic = false
@@ -39,9 +39,6 @@ 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
45
42
 
46
43
  private val inOutQuad = Easing { t -> if (t < 0.5f) 2 * t * t else 1 - 2 * (1 - t) * (1 - t) }
47
44
  internal val tabTimingSpec = tween<Float>(durationMillis = 200, easing = inOutQuad)
@@ -135,9 +132,7 @@ fun BottomTab(
135
132
  it.bottom + duoRailHeight(items.size, hasQr, compressed = compression.tabBarCompressed) + DUO_GROUP_GAP
136
133
  } ?: 0.dp
137
134
  val onTabSelected: (Int) -> Unit = { index ->
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
135
+ runCatching { maxApi?.triggerEventVibration("light", callback = null) }
141
136
  val switchTab = {
142
137
  if (index != selectedIndex) selectTab(index)
143
138
  }
@@ -39,8 +39,6 @@ internal val DUO_GROUP_GAP = Spacing.M
39
39
  private val DEFAULT_TOOLBAR_HEIGHT = DuoMetrics.controlSize + Spacing.M + DuoMetrics.itemPitch * 2
40
40
  // Back, then the single button the toolbar folds into
41
41
  private val COMPRESSED_TOOLBAR_HEIGHT = DuoMetrics.controlSize * 2 + Spacing.M
42
- // Back alone, while a rail opened from the compressed state takes the toolbar slot
43
- private val BACK_ONLY_HEIGHT = DuoMetrics.controlSize
44
42
 
45
43
  // Port of RN DuoBarCompression (HIG, Designing for iPhone Duo: Vertical controls):
46
44
  // - both fit: both stay whole
@@ -78,7 +76,8 @@ internal fun rememberDuoBarCompression(
78
76
  expandTabBar = { expanded = DuoBar.TAB_BAR },
79
77
  setToolbarHeight = setToolbarHeight,
80
78
  )
81
- opened == DuoBar.TAB_BAR && fits(BACK_ONLY_HEIGHT, tabBar) -> DuoBarCompression(
79
+ // an opened rail always shows, even when only the group gap is short: it closes once a tab is picked
80
+ opened == DuoBar.TAB_BAR -> DuoBarCompression(
82
81
  toolbarCompressed = true,
83
82
  toolbarHidden = true,
84
83
  expandToolbar = { opened = DuoBar.TOOLBAR },