@momo-kits/native-kits 0.300.3-debug → 0.300.4-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 +51 -7
- package/README.md +2 -2
- package/compose/build.gradle.kts +10 -2
- package/compose/build.gradle.kts.backup +8 -0
- package/compose/compose.podspec +1 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +20 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +6 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +132 -83
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +113 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
- 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 +15 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
- package/gradle/libs.versions.toml +3 -14
- package/gradle/wrapper/gradle-wrapper.properties +0 -2
- package/gradle.properties +3 -5
- package/ios/Application/ApplicationEnvironment.swift +24 -6
- package/ios/Application/DesignSystemKits.swift +100 -0
- package/ios/Application/DesignSystemKitsPayload.swift +68 -0
- package/ios/Application/MaxApi.swift +30 -3
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +8 -2
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
- package/ios/Carousel/Carousel.swift +29 -12
- package/ios/Checkbox/Checkbox.swift +7 -3
- package/ios/Chip/Chip.swift +5 -4
- package/ios/Collapse/Collapse.swift +70 -19
- package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/DateTimePicker/DateTimePicker.swift +54 -20
- package/ios/DateTimePicker/WheelPicker.swift +103 -105
- package/ios/Divider/Divider.swift +37 -4
- package/ios/Extensions/Color++.swift +38 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Extensions/SkeletonUICompat.swift +11 -0
- package/ios/Icon/Icon.swift +21 -23
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -23
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +171 -148
- package/ios/Input/InputPhoneNumber.swift +10 -9
- package/ios/Input/InputSearch.swift +107 -78
- package/ios/Input/InputTextArea.swift +133 -148
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +32 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +187 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +415 -0
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +14 -2
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
- package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
- package/ios/Navigation/Component/Header.swift +545 -0
- package/ios/Navigation/Component/HeaderBackground.swift +154 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +425 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
- package/ios/Navigation/Component/SideBar.swift +183 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +195 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
- package/ios/Navigation/StackScreen.swift +930 -0
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
- package/ios/Pagination/PaginationScroll.swift +2 -2
- package/ios/Popup/PopupDisplay.swift +72 -135
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +138 -0
- package/ios/Popup/PopupNotify.swift +59 -69
- package/ios/Popup/PopupPromotion.swift +31 -47
- package/ios/PopupView/FullscreenPopup.swift +13 -19
- package/ios/PopupView/PopupView.swift +1 -1
- package/ios/PopupView/Utils++.swift +0 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +2 -1
- package/ios/Rating/Rating.swift +9 -1
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
- package/ios/Skeleton/Skeleton.swift +22 -27
- package/ios/Slider/Slider.swift +16 -18
- package/ios/Stepper/Stepper.swift +7 -3
- package/ios/Steps/Steps.swift +8 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -0
- package/ios/Swipeable/SwipeCell.swift +10 -6
- package/ios/Switch/Switch.swift +22 -20
- package/ios/TabView/TabView.swift +102 -21
- package/ios/Tag/Tag.swift +15 -4
- package/ios/Template/Logo/Logo.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/FontScale.swift +17 -14
- package/ios/Typography/Text.swift +156 -20
- package/ios/Typography/Typography.swift +2 -2
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -3
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
- package/package.json +1 -1
- package/publish.sh +19 -2
- package/scripts/gen-ios-icons.mjs +36 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
- package/ios/Application/Components.swift +0 -270
- package/ios/Application/ComposeApi.swift +0 -22
- package/ios/Application/HeaderRight.swift +0 -306
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationContainer.swift +0 -213
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -22
- package/ios/Application/Navigation/component/Header.swift +0 -178
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
- package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
- package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
- package/ios/Application/Screen.swift +0 -250
- package/ios/Application/StackScreen.swift +0 -492
- package/ios/Typography/FontScaleStore.swift +0 -58
- package/ios/Typography/KitDesignSystem.swift +0 -17
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package vn.momo.kits.platform
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import androidx.compose.runtime.Immutable
|
|
5
|
+
import androidx.compose.ui.unit.Dp
|
|
6
|
+
|
|
7
|
+
@Immutable
|
|
8
|
+
internal data class ReservedRegion(val x: Dp, val y: Dp, val width: Dp, val height: Dp)
|
|
9
|
+
|
|
10
|
+
// occlusion: regions the system occludes (iOS 27.1+), in the coordinates of the Compose root view:
|
|
11
|
+
// the iPhone Duo camera, and the status bar cluster as it grows for a call or a Live Activity.
|
|
12
|
+
// isFoldable: the device reports a `.division` (hinge) region, Apple's foldable check.
|
|
13
|
+
@Immutable
|
|
14
|
+
internal data class ReservedRegions(
|
|
15
|
+
val occlusion: List<ReservedRegion> = emptyList(),
|
|
16
|
+
val isFoldable: Boolean = false,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@Composable
|
|
20
|
+
internal expect fun rememberReservedRegions(): ReservedRegions
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
package vn.momo.kits.utils
|
|
2
2
|
|
|
3
3
|
import androidx.compose.foundation.layout.WindowInsets
|
|
4
|
+
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
5
|
+
import androidx.compose.foundation.layout.displayCutout
|
|
6
|
+
import androidx.compose.foundation.layout.only
|
|
7
|
+
import androidx.compose.foundation.layout.union
|
|
8
|
+
import androidx.compose.foundation.layout.windowInsetsPadding
|
|
4
9
|
import androidx.compose.foundation.layout.asPaddingValues
|
|
5
10
|
import androidx.compose.foundation.layout.systemBars
|
|
6
11
|
import androidx.compose.runtime.Composable
|
|
@@ -90,6 +95,14 @@ fun getAppStatusBarHeight(): Dp {
|
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
internal val horizontalSafeAreaInsets: WindowInsets
|
|
99
|
+
@Composable get() = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)
|
|
100
|
+
|
|
101
|
+
// Left/right safe area like SwiftUI (landscape camera/notch); consumed, so nested StackScreens don't pad twice.
|
|
102
|
+
internal fun Modifier.horizontalSafeAreaPadding(): Modifier = composed {
|
|
103
|
+
windowInsetsPadding(horizontalSafeAreaInsets)
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
@Composable
|
|
94
107
|
internal fun getNavigationBarHeight(): Dp {
|
|
95
108
|
return if (AppNavigationBar.current == 0.dp) {
|
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
CHANGED
|
@@ -38,6 +38,21 @@ class ToolkitRequestsTest {
|
|
|
38
38
|
assertEquals(1, results)
|
|
39
39
|
}
|
|
40
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
|
+
|
|
41
56
|
private class Handle : ApiRequestHandle {
|
|
42
57
|
override val requestId = "toolkit-test"
|
|
43
58
|
var cancellations = 0
|
|
@@ -5,6 +5,7 @@ package vn.momo.kits.const
|
|
|
5
5
|
import androidx.compose.runtime.Composable
|
|
6
6
|
import androidx.compose.runtime.remember
|
|
7
7
|
import androidx.compose.ui.text.font.FontFamily
|
|
8
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
8
9
|
import androidx.compose.ui.text.platform.Font
|
|
9
10
|
import kotlinx.cinterop.ExperimentalForeignApi
|
|
10
11
|
import kotlinx.cinterop.addressOf
|
|
@@ -15,7 +16,7 @@ import platform.Foundation.dataWithContentsOfFile
|
|
|
15
16
|
import platform.posix.memcpy
|
|
16
17
|
|
|
17
18
|
@Composable
|
|
18
|
-
internal actual fun hostFontFamily(fileName: String): FontFamily? = remember(fileName) {
|
|
19
|
+
internal actual fun hostFontFamily(fileName: String, weight: FontWeight): FontFamily? = remember(fileName, weight) {
|
|
19
20
|
val name = fileName.substringBeforeLast('.')
|
|
20
21
|
val ext = fileName.substringAfterLast('.', "")
|
|
21
22
|
// Font từ font-source nằm rời ở bundle root, không nằm trong compose-resources.
|
|
@@ -26,6 +27,7 @@ internal actual fun hostFontFamily(fileName: String): FontFamily? = remember(fil
|
|
|
26
27
|
getData = {
|
|
27
28
|
NSData.dataWithContentsOfFile(path)?.toByteArray() ?: ByteArray(0)
|
|
28
29
|
},
|
|
30
|
+
weight = weight,
|
|
29
31
|
),
|
|
30
32
|
)
|
|
31
33
|
}
|
|
@@ -39,11 +39,15 @@ import platform.Foundation.NSBundle
|
|
|
39
39
|
import platform.UIKit.UIColor
|
|
40
40
|
import platform.UIKit.UIDevice
|
|
41
41
|
import platform.UIKit.UIScreen
|
|
42
|
+
import platform.UIKit.UIUserInterfaceIdiomPhone
|
|
42
43
|
import vn.momo.kits.navigation.AttachHostBackGesture
|
|
43
44
|
import vn.momo.kits.navigation.Navigator
|
|
44
45
|
|
|
45
46
|
actual fun getPlatformName(): String = "iOS"
|
|
46
47
|
|
|
48
|
+
actual fun isPhoneIdiom(): Boolean =
|
|
49
|
+
UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone
|
|
50
|
+
|
|
47
51
|
@OptIn(ExperimentalForeignApi::class)
|
|
48
52
|
@Composable
|
|
49
53
|
actual fun getScreenDimensions(): ScreenDimension = memScoped {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
package vn.momo.kits.platform
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import androidx.compose.runtime.DisposableEffect
|
|
5
|
+
import androidx.compose.runtime.getValue
|
|
6
|
+
import androidx.compose.runtime.mutableStateOf
|
|
7
|
+
import androidx.compose.runtime.remember
|
|
8
|
+
import androidx.compose.runtime.setValue
|
|
9
|
+
import androidx.compose.ui.uikit.LocalUIViewController
|
|
10
|
+
import androidx.compose.ui.unit.dp
|
|
11
|
+
import kotlinx.cinterop.BetaInteropApi
|
|
12
|
+
import kotlinx.cinterop.CFunction
|
|
13
|
+
import kotlinx.cinterop.COpaquePointer
|
|
14
|
+
import kotlinx.cinterop.ExperimentalForeignApi
|
|
15
|
+
import kotlinx.cinterop.interpretCPointer
|
|
16
|
+
import kotlinx.cinterop.interpretObjCPointer
|
|
17
|
+
import kotlinx.cinterop.interpretObjCPointerOrNull
|
|
18
|
+
import kotlinx.cinterop.invoke
|
|
19
|
+
import kotlinx.cinterop.objcPtr
|
|
20
|
+
import kotlinx.cinterop.reinterpret
|
|
21
|
+
import kotlinx.cinterop.readValue
|
|
22
|
+
import kotlinx.cinterop.useContents
|
|
23
|
+
import platform.CoreGraphics.CGRectZero
|
|
24
|
+
import platform.Foundation.NSArray
|
|
25
|
+
import platform.Foundation.NSSelectorFromString
|
|
26
|
+
import platform.Foundation.NSValue
|
|
27
|
+
import platform.Foundation.valueForKey
|
|
28
|
+
import platform.UIKit.CGRectValue
|
|
29
|
+
import platform.UIKit.UIView
|
|
30
|
+
import platform.UIKit.UIViewAutoresizingFlexibleHeight
|
|
31
|
+
import platform.UIKit.UIViewAutoresizingFlexibleWidth
|
|
32
|
+
import platform.darwin.NSObject
|
|
33
|
+
import platform.Foundation.NSClassFromString
|
|
34
|
+
import platform.posix.RTLD_DEFAULT
|
|
35
|
+
import platform.posix.dlsym
|
|
36
|
+
|
|
37
|
+
// The API is newer than the SDK Kotlin/Native binds, so it is reached through the runtime.
|
|
38
|
+
@OptIn(ExperimentalForeignApi::class)
|
|
39
|
+
private val regionsOfKind = NSSelectorFromString("reservedRegionsOfKind:")
|
|
40
|
+
|
|
41
|
+
@OptIn(ExperimentalForeignApi::class)
|
|
42
|
+
private val regionsOfKindOptions = NSSelectorFromString("reservedRegionsOfKind:options:")
|
|
43
|
+
|
|
44
|
+
// a hinge never goes away, so a positive answer is kept for every later container
|
|
45
|
+
private var isKnownFoldable = false
|
|
46
|
+
|
|
47
|
+
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
|
48
|
+
private fun regionKind(name: String): Any? {
|
|
49
|
+
// a class object fails `as? NSObjectProtocol` (K/N checks it as an instance), so it is reinterpreted
|
|
50
|
+
val kindClass = NSClassFromString("UIViewReservedRegionKind")
|
|
51
|
+
?.let { interpretObjCPointer<NSObject>(it.objcPtr()) } ?: return null
|
|
52
|
+
return kindClass.performSelector(NSSelectorFromString(name))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// performSelector cannot pass the NSUInteger options, so objc_msgSend is called with the method's own signature.
|
|
56
|
+
@OptIn(ExperimentalForeignApi::class)
|
|
57
|
+
private val msgSendRegionsOfKindOptions = dlsym(RTLD_DEFAULT, "objc_msgSend")
|
|
58
|
+
?.reinterpret<CFunction<(COpaquePointer?, COpaquePointer?, COpaquePointer?, ULong) -> COpaquePointer?>>()
|
|
59
|
+
|
|
60
|
+
// Apple's foldable check: only a device with a hinge reports a `.division` region, inactive while
|
|
61
|
+
// flat (UIViewReservedRegionQueryOptionsIncludeInactive) but still there. Screen size cannot tell it
|
|
62
|
+
// apart from a large iPhone in landscape or a narrowed window.
|
|
63
|
+
@OptIn(ExperimentalForeignApi::class)
|
|
64
|
+
private fun UIView.hasDivisionRegion(): Boolean {
|
|
65
|
+
if (window == null || !respondsToSelector(regionsOfKindOptions)) return false
|
|
66
|
+
val msgSend = msgSendRegionsOfKindOptions ?: return false
|
|
67
|
+
val kind = regionKind("divisionRegionKind") as? NSObject ?: return false
|
|
68
|
+
val result = msgSend(
|
|
69
|
+
interpretCPointer(objcPtr()),
|
|
70
|
+
regionsOfKindOptions,
|
|
71
|
+
interpretCPointer(kind.objcPtr()),
|
|
72
|
+
1uL,
|
|
73
|
+
)
|
|
74
|
+
val regions = interpretObjCPointerOrNull<NSArray>(result?.rawValue ?: return false)
|
|
75
|
+
return (regions?.count ?: 0uL) > 0uL
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@OptIn(ExperimentalForeignApi::class)
|
|
79
|
+
private fun UIView.occlusionRegions(target: UIView): List<ReservedRegion> {
|
|
80
|
+
if (window == null || !respondsToSelector(regionsOfKind)) return emptyList()
|
|
81
|
+
val kind = regionKind("occlusionRegionKind") ?: return emptyList()
|
|
82
|
+
val regions = when (val result = performSelector(regionsOfKind, withObject = kind)) {
|
|
83
|
+
is List<*> -> result
|
|
84
|
+
is NSArray -> List(result.count.toInt()) { result.objectAtIndex(it.toULong()) }
|
|
85
|
+
else -> return emptyList()
|
|
86
|
+
}
|
|
87
|
+
return regions.mapNotNull { region ->
|
|
88
|
+
val frame = ((region as? NSObject)?.valueForKey("frame") as? NSValue)?.CGRectValue ?: return@mapNotNull null
|
|
89
|
+
convertRect(frame, toView = target).useContents {
|
|
90
|
+
ReservedRegion(origin.x.dp, origin.y.dp, size.width.dp, size.height.dp)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Invisible full-size probe: reading the regions in layoutSubviews makes UIKit run it again
|
|
96
|
+
// when they change, so nothing polls.
|
|
97
|
+
@OptIn(ExperimentalForeignApi::class)
|
|
98
|
+
private class ReservedRegionsProbe(
|
|
99
|
+
private val onChange: (ReservedRegions) -> Unit,
|
|
100
|
+
) : UIView(frame = CGRectZero.readValue()) {
|
|
101
|
+
private var last: ReservedRegions? = null
|
|
102
|
+
|
|
103
|
+
init {
|
|
104
|
+
userInteractionEnabled = false
|
|
105
|
+
autoresizingMask = UIViewAutoresizingFlexibleWidth or UIViewAutoresizingFlexibleHeight
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override fun layoutSubviews() {
|
|
109
|
+
super.layoutSubviews()
|
|
110
|
+
// re-read until found: the window may not report the hinge on the first pass
|
|
111
|
+
if (!isKnownFoldable) isKnownFoldable = hasDivisionRegion()
|
|
112
|
+
val regions = ReservedRegions(superview?.let { occlusionRegions(it) } ?: emptyList(), isKnownFoldable)
|
|
113
|
+
if (regions != last) {
|
|
114
|
+
last = regions
|
|
115
|
+
onChange(regions)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
override fun didMoveToWindow() {
|
|
120
|
+
super.didMoveToWindow()
|
|
121
|
+
setNeedsLayout()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@OptIn(ExperimentalForeignApi::class)
|
|
126
|
+
@Composable
|
|
127
|
+
internal actual fun rememberReservedRegions(): ReservedRegions {
|
|
128
|
+
if (!isPhoneIdiom()) return ReservedRegions()
|
|
129
|
+
val host = LocalUIViewController.current.view
|
|
130
|
+
var regions by remember { mutableStateOf(ReservedRegions(isFoldable = isKnownFoldable)) }
|
|
131
|
+
DisposableEffect(host) {
|
|
132
|
+
val probe = ReservedRegionsProbe { regions = it }
|
|
133
|
+
probe.setFrame(host.bounds)
|
|
134
|
+
host.insertSubview(probe, atIndex = 0)
|
|
135
|
+
onDispose { probe.removeFromSuperview() }
|
|
136
|
+
}
|
|
137
|
+
return regions
|
|
138
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
[versions]
|
|
2
|
-
|
|
3
|
-
android-compileSdk = "36"
|
|
4
|
-
# AI-GENERATED END: align Kits and its samples with the approved SDK 36 toolchain
|
|
2
|
+
android-compileSdk = "37"
|
|
5
3
|
android-minSdk = "24"
|
|
6
4
|
android-targetSdk = "36"
|
|
7
5
|
ktor-version = "3.0.0-rc-1"
|
|
@@ -11,9 +9,7 @@ kSerialize = "1.7.1"
|
|
|
11
9
|
compose = "1.10.3"
|
|
12
10
|
navigation-multiplatform = "2.9.2"
|
|
13
11
|
coil3-multiplatform = "3.0.0-alpha10"
|
|
14
|
-
# AI-GENERATED START: align the Android toolchain with the integrating app
|
|
15
12
|
androidGradlePlugin = "9.2.1"
|
|
16
|
-
# AI-GENERATED END: align the Android toolchain with the integrating app
|
|
17
13
|
r8 = "8.9.35"
|
|
18
14
|
kotlinx-datetime = "0.7.1"
|
|
19
15
|
airbnb-lottie = "5.2.0"
|
|
@@ -21,12 +17,9 @@ compottie = "2.1.0"
|
|
|
21
17
|
androidx-activity = "1.9.1"
|
|
22
18
|
androidx-appcompat = "1.7.0"
|
|
23
19
|
material = "1.10.0"
|
|
24
|
-
maxapi = "0.1.1"
|
|
25
20
|
vanniktechMavenPublish = "0.34.0"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# AI-GENERATED END: identify the SDK 36 compatible Kits release
|
|
29
|
-
nativemaxapi = "0.3.1"
|
|
21
|
+
kits = "0.167.1-duo.1"
|
|
22
|
+
nativemaxapi = "0.3.9"
|
|
30
23
|
gif-image = "0.1.2"
|
|
31
24
|
|
|
32
25
|
[libraries]
|
|
@@ -46,14 +39,12 @@ coil-multiplatform-compose = { module = "io.coil-kt.coil3:coil-compose", version
|
|
|
46
39
|
coil-multiplatform-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil3-multiplatform" }
|
|
47
40
|
r8 = { module = "com.android.tools:r8", version.ref = "r8" }
|
|
48
41
|
airbnb-lottie = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "airbnb-lottie" }
|
|
49
|
-
# AI-GENERATED START
|
|
50
42
|
# Depend on compottie-core (not the `compottie` umbrella). The umbrella drags in the
|
|
51
43
|
# separate `keight` umbrella artifact, which collides with `keight-core` on the
|
|
52
44
|
# `findRoot__at__ScriptRuntime` symbol (duplicate symbol at the iOS `ld` link of
|
|
53
45
|
# shared.framework). compottie-core pulls only `keight-core` (which declares
|
|
54
46
|
# keight.js.JsAny, so the earlier unlinked-symbol error stays fixed too).
|
|
55
47
|
compottie = { module = "io.github.alexzhirkevich:compottie-core", version.ref = "compottie" }
|
|
56
|
-
# AI-GENERATED END
|
|
57
48
|
native-max-api = { group = "vn.momo.maxapi", name = "NativeMaxApi", version.ref = "nativemaxapi" }
|
|
58
49
|
kits = { module = "vn.momo.kits:kits", version.ref = "kits" }
|
|
59
50
|
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
|
@@ -74,6 +65,4 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug
|
|
|
74
65
|
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
|
75
66
|
vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }
|
|
76
67
|
|
|
77
|
-
# AI-GENERATED START: use the AGP Android target for Kotlin Multiplatform libraries
|
|
78
68
|
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "androidGradlePlugin" }
|
|
79
|
-
# AI-GENERATED END: use the AGP Android target for Kotlin Multiplatform libraries
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
#Thu Jul 31 17:05:40 ICT 2025
|
|
2
2
|
distributionBase=GRADLE_USER_HOME
|
|
3
3
|
distributionPath=wrapper/dists
|
|
4
|
-
# AI-GENERATED START: align the Gradle wrapper with the integrating app
|
|
5
4
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
|
|
6
|
-
# AI-GENERATED END: align the Gradle wrapper with the integrating app
|
|
7
5
|
networkTimeout=10000
|
|
8
6
|
validateDistributionUrl=true
|
|
9
7
|
zipStoreBase=GRADLE_USER_HOME
|
package/gradle.properties
CHANGED
|
@@ -18,11 +18,9 @@ kotlin.apple.xcodeCompatibility.nowarn=true
|
|
|
18
18
|
name="ComposeKits"
|
|
19
19
|
group=vn.momo.kits
|
|
20
20
|
artifact.id=kits
|
|
21
|
-
|
|
22
|
-
version=0.300.3
|
|
23
|
-
# AI-GENERATED END: publish the SDK 36 compatible Kits under a new version
|
|
21
|
+
version=0.300.4
|
|
24
22
|
|
|
25
23
|
repo=GitLab
|
|
26
24
|
url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
|
|
27
|
-
gitlab.user=
|
|
28
|
-
gitlab.password=
|
|
25
|
+
gitlab.user=
|
|
26
|
+
gitlab.password=
|
|
@@ -17,8 +17,8 @@ public class MiniAppContext {
|
|
|
17
17
|
public var toolkitConfig: [String: Any] = [:]
|
|
18
18
|
public var providerId: String = "momo"
|
|
19
19
|
public var permissions: [[String: Any]] = []
|
|
20
|
-
// Per-mini-app font-scale override; nil falls back to
|
|
21
|
-
// MiniAppContext.userScaleRate / useOSScaleRate, which win per field over
|
|
20
|
+
// Per-mini-app font-scale override; nil falls back to DesignSystemKits. Mirrors Compose's
|
|
21
|
+
// MiniAppContext.userScaleRate / useOSScaleRate, which win per field over DesignSystemKits.
|
|
22
22
|
public var userScaleRate: CGFloat? = nil
|
|
23
23
|
public var useOSScaleRate: Bool? = nil
|
|
24
24
|
|
|
@@ -37,10 +37,17 @@ public class MiniAppContext {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
public var
|
|
43
|
-
public var
|
|
40
|
+
// Value type so DesignSystemKitsConfigs can dedupe host pushes; mirrors Compose's DesignSystemConfig.
|
|
41
|
+
public struct KitConfig: Equatable {
|
|
42
|
+
public var trustBanner: TrustBannerData?
|
|
43
|
+
public var headerBar: String?
|
|
44
|
+
public var headerGradient: String?
|
|
45
|
+
|
|
46
|
+
public init(trustBanner: TrustBannerData? = nil, headerBar: String? = nil, headerGradient: String? = nil) {
|
|
47
|
+
self.trustBanner = trustBanner
|
|
48
|
+
self.headerBar = headerBar
|
|
49
|
+
self.headerGradient = headerGradient
|
|
50
|
+
}
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
public class ApplicationEnvironment: ObservableObject {
|
|
@@ -59,6 +66,17 @@ public class ApplicationEnvironment: ObservableObject {
|
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
extension ApplicationEnvironment {
|
|
70
|
+
/// Closes the mini app: through `composeApi` as before, else `maxApi` (Compose `maxApi.dismiss`).
|
|
71
|
+
func requestDismiss() {
|
|
72
|
+
if let composeApi {
|
|
73
|
+
composeApi.requestCallback(funcName: "dismiss", params: nil, onResponse: nil)
|
|
74
|
+
} else {
|
|
75
|
+
maxApi?.dismiss(completion: nil)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
62
80
|
private struct ApplicationEnvironmentKey: EnvironmentKey {
|
|
63
81
|
static let defaultValue = ApplicationEnvironment()
|
|
64
82
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// File: ios/Application/DesignSystemKits.swift
|
|
2
|
+
// Created At: 2026-10-02 14:17:29 +07:00
|
|
3
|
+
// Created By: AI
|
|
4
|
+
// AI Agent: Claude Code
|
|
5
|
+
// Model: claude-opus-5-5
|
|
6
|
+
|
|
7
|
+
// AI-GENERATED START: app-wide design-system holder mirroring Compose's DesignSystemKits
|
|
8
|
+
import SwiftUI
|
|
9
|
+
|
|
10
|
+
/// Host-owned design-system inputs; mirrors Compose's `DesignSystemKitsConfigs` field for field.
|
|
11
|
+
public struct DesignSystemKitsConfigs: Equatable {
|
|
12
|
+
public var userScaleRate: CGFloat
|
|
13
|
+
public var useOSScaleRate: Bool
|
|
14
|
+
public var isWhiteList: Bool
|
|
15
|
+
public var designSystemConfig: KitConfig?
|
|
16
|
+
|
|
17
|
+
public init(
|
|
18
|
+
userScaleRate: CGFloat = 1,
|
|
19
|
+
useOSScaleRate: Bool = true,
|
|
20
|
+
isWhiteList: Bool = false,
|
|
21
|
+
designSystemConfig: KitConfig? = nil
|
|
22
|
+
) {
|
|
23
|
+
self.userScaleRate = userScaleRate
|
|
24
|
+
self.useOSScaleRate = useOSScaleRate
|
|
25
|
+
self.isWhiteList = isWhiteList
|
|
26
|
+
self.designSystemConfig = designSystemConfig
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// App-wide holder for the SwiftUI kit, mirroring Compose's `DesignSystemKits`. The kit is sandboxed from the
|
|
31
|
+
/// host, so the host pushes `setConfigs(_:)` at startup and on every change; the OS Dynamic Type factor is
|
|
32
|
+
/// tracked here. Views re-render on change as long as the subtree got `.momoFontScale()` (`NavigationContainer`
|
|
33
|
+
/// applies it). Swift reserves `init`, so there is no `init(source:)` twin: the host observes and pushes.
|
|
34
|
+
public final class DesignSystemKits: ObservableObject {
|
|
35
|
+
public static let shared = DesignSystemKits()
|
|
36
|
+
|
|
37
|
+
@Published private var configs = DesignSystemKitsConfigs()
|
|
38
|
+
@Published public private(set) var osFontScale: CGFloat = DesignSystemKits.currentOsFontScale()
|
|
39
|
+
|
|
40
|
+
private init() {
|
|
41
|
+
NotificationCenter.default.addObserver(
|
|
42
|
+
self,
|
|
43
|
+
selector: #selector(contentSizeCategoryDidChange),
|
|
44
|
+
name: UIContentSizeCategory.didChangeNotification,
|
|
45
|
+
object: nil
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public func setConfigs(_ configs: DesignSystemKitsConfigs) {
|
|
50
|
+
onMain {
|
|
51
|
+
// Re-assigning an unchanged value would still publish and invalidate every kit subtree.
|
|
52
|
+
guard self.configs != configs else { return }
|
|
53
|
+
self.configs = configs
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public func getConfigs() -> DesignSystemKitsConfigs {
|
|
58
|
+
configs
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public var fontScale: FontScale {
|
|
62
|
+
FontScale(
|
|
63
|
+
userScaleRate: configs.userScaleRate,
|
|
64
|
+
useOSScaleRate: configs.useOSScaleRate,
|
|
65
|
+
osFontScale: osFontScale
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@objc private func contentSizeCategoryDidChange() {
|
|
70
|
+
let next = Self.currentOsFontScale()
|
|
71
|
+
guard next != osFontScale else { return }
|
|
72
|
+
osFontScale = next
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Same factor Compose's iOS `ProvideOsFontScale` derives from
|
|
76
|
+
/// `UIFont.preferredFont(forTextStyle: .body).pointSize / 17`, so both iOS surfaces scale alike.
|
|
77
|
+
static func currentOsFontScale() -> CGFloat {
|
|
78
|
+
let base = UIFont.systemFont(ofSize: UIFont.labelFontSize)
|
|
79
|
+
return UIFontMetrics.default.scaledFont(for: base).pointSize / base.pointSize
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Synchronous when already on main — an async hop would defer the change by a frame.
|
|
83
|
+
private func onMain(_ work: @escaping () -> Void) {
|
|
84
|
+
if Thread.isMainThread { work() } else { DispatchQueue.main.async(execute: work) }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private struct DesignSystemKitsConfigsKey: EnvironmentKey {
|
|
89
|
+
/// Computed, never stored: an un-injected subtree still reads the live holder, just not reactively.
|
|
90
|
+
static var defaultValue: DesignSystemKitsConfigs { DesignSystemKits.shared.getConfigs() }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public extension EnvironmentValues {
|
|
94
|
+
/// Injected by `.momoFontScale()`; never set it directly (same trap as `\.momoFontScale`).
|
|
95
|
+
var designSystemKitsConfigs: DesignSystemKitsConfigs {
|
|
96
|
+
get { self[DesignSystemKitsConfigsKey.self] }
|
|
97
|
+
set { self[DesignSystemKitsConfigsKey.self] = newValue }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// AI-GENERATED END: app-wide design-system holder mirroring Compose's DesignSystemKits
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// File: ios/Application/DesignSystemKitsPayload.swift
|
|
2
|
+
// Created At: 2026-10-02 15:40:00 +07:00
|
|
3
|
+
// Created By: AI
|
|
4
|
+
// AI Agent: Claude Code
|
|
5
|
+
// Model: claude-opus-5-5
|
|
6
|
+
|
|
7
|
+
// AI-GENERATED START: JSON entry point for hosts that can't share Swift types with the kit
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
public extension DesignSystemKits {
|
|
11
|
+
/// Applies configs pushed as JSON by a host that can't share Swift types with the kit (e.g. the KMP bridge).
|
|
12
|
+
/// Invalid JSON is ignored and the current configs are kept.
|
|
13
|
+
func setConfigs(json: String) {
|
|
14
|
+
guard let data = json.data(using: .utf8),
|
|
15
|
+
let payload = try? JSONDecoder().decode(DesignSystemKitsPayload.self, from: data) else { return }
|
|
16
|
+
setConfigs(payload.configs)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Wire format; field names match the host's serializer (momo-app HostDesignSystemKits / DesignSystemData.kt).
|
|
21
|
+
private struct DesignSystemKitsPayload: Decodable {
|
|
22
|
+
let userScaleRate: CGFloat
|
|
23
|
+
let useOSScaleRate: Bool
|
|
24
|
+
let isWhiteList: Bool
|
|
25
|
+
let designSystemConfig: KitConfigPayload?
|
|
26
|
+
|
|
27
|
+
var configs: DesignSystemKitsConfigs {
|
|
28
|
+
DesignSystemKitsConfigs(
|
|
29
|
+
userScaleRate: userScaleRate,
|
|
30
|
+
useOSScaleRate: useOSScaleRate,
|
|
31
|
+
isWhiteList: isWhiteList,
|
|
32
|
+
designSystemConfig: designSystemConfig?.kitConfig
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private struct KitConfigPayload: Decodable {
|
|
38
|
+
let trustBanner: TrustBannerPayload?
|
|
39
|
+
let headerBar: String?
|
|
40
|
+
let headerGradient: String?
|
|
41
|
+
|
|
42
|
+
var kitConfig: KitConfig {
|
|
43
|
+
KitConfig(trustBanner: trustBanner?.trustBannerData, headerBar: headerBar, headerGradient: headerGradient)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private struct TrustBannerPayload: Decodable {
|
|
48
|
+
let content: [String: String]
|
|
49
|
+
let subContent: [String: String]
|
|
50
|
+
let pciImage: String?
|
|
51
|
+
let sslImage: String?
|
|
52
|
+
let urlConfig: String?
|
|
53
|
+
let icons: [String]?
|
|
54
|
+
let momoImage: String?
|
|
55
|
+
|
|
56
|
+
var trustBannerData: TrustBannerData {
|
|
57
|
+
TrustBannerData(
|
|
58
|
+
content: content,
|
|
59
|
+
subContent: subContent,
|
|
60
|
+
pciImage: pciImage,
|
|
61
|
+
momoImage: momoImage,
|
|
62
|
+
sslImage: sslImage,
|
|
63
|
+
icons: icons,
|
|
64
|
+
urlConfig: urlConfig ?? ""
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// AI-GENERATED END: JSON entry point for hosts that can't share Swift types with the kit
|
|
@@ -5,14 +5,41 @@
|
|
|
5
5
|
// Swift mirror of the host-app bridge exposed on Kotlin as `vn.momo.maxapi.IMaxApi`
|
|
6
6
|
// (`LocalMaxApi`). `IMaxApi` itself is an external compiled dependency with no
|
|
7
7
|
// source in this repo, so this protocol intentionally exposes only the methods
|
|
8
|
-
// this kit's Kotlin source actually calls
|
|
9
|
-
//
|
|
8
|
+
// this kit's Kotlin source actually calls — not a full guess at IMaxApi's real surface.
|
|
9
|
+
// Callbacks carry IMaxApi's `["response": …]` payload.
|
|
10
10
|
//
|
|
11
11
|
|
|
12
|
-
import
|
|
12
|
+
import UIKit
|
|
13
13
|
|
|
14
14
|
public protocol MaxApi {
|
|
15
15
|
func track(eventName: String, params: [String: Any?])
|
|
16
16
|
func dismiss(completion: (() -> Void)?)
|
|
17
17
|
func getLanguage(callback: @escaping ([String: Any?]?) -> Void)
|
|
18
|
+
/// `type` is the IMaxApi vibration name (`"light"`, …).
|
|
19
|
+
func triggerEventVibration(_ type: String)
|
|
20
|
+
// Toolkit (Compose `HeaderRight.Toolkit`).
|
|
21
|
+
func isFavoriteApp(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
|
|
22
|
+
func onToolAction(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
|
|
23
|
+
func showTools(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
|
|
24
|
+
func showHelpCenter(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
|
|
25
|
+
/// Back to the host home, closing every mini app.
|
|
26
|
+
func dismissAll(completion: (() -> Void)?)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public extension MaxApi {
|
|
30
|
+
/// Hosts that don't bridge haptics still get the system light tick.
|
|
31
|
+
func triggerEventVibration(_ type: String) {
|
|
32
|
+
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Hosts that don't bridge the toolkit keep it inert, as before these joined the protocol.
|
|
36
|
+
func isFavoriteApp(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
|
|
37
|
+
func onToolAction(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
|
|
38
|
+
func showTools(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
|
|
39
|
+
func showHelpCenter(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
|
|
40
|
+
|
|
41
|
+
/// Without a home to return to, closing this mini app is the closest thing.
|
|
42
|
+
func dismissAll(completion: (() -> Void)?) {
|
|
43
|
+
dismiss(completion: completion)
|
|
44
|
+
}
|
|
18
45
|
}
|