@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.
- package/CLAUDE.md +4 -4
- package/README.md +10 -2
- package/build.gradle.kts +3 -1
- package/compose/build.gradle.kts +19 -20
- package/compose/build.gradle.kts.backup +18 -19
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +23 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +24 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +6 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +6 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +44 -30
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +40 -32
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +62 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +6 -2
- package/gradle/libs.versions.toml +11 -4
- package/gradle/wrapper/gradle-wrapper.properties +3 -1
- package/gradle.properties +1 -1
- package/package.json +1 -1
- package/publish.sh +7 -2
- 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: `
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
package/compose/build.gradle.kts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
val
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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?.
|
|
41
|
+
maxApi?.trackEvent(
|
|
35
42
|
eventName = "auto_screen_navigated",
|
|
36
|
-
|
|
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?.
|
|
63
|
+
maxApi?.trackEvent(
|
|
57
64
|
eventName = "auto_screen_displayed",
|
|
58
|
-
|
|
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?.
|
|
84
|
+
maxApi?.trackEvent(
|
|
78
85
|
eventName = "auto_screen_interacted",
|
|
79
|
-
|
|
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,
|
|
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?.
|
|
115
|
+
maxApi?.trackEvent(
|
|
109
116
|
eventName = "auto_popup_displayed",
|
|
110
|
-
|
|
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
|
|
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
|
package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt
ADDED
|
@@ -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
|
package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
package/package.json
CHANGED
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
|
|
378
|
-
echo "
|
|
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
|
|