@momo-kits/native-kits 0.164.1-beta.17 → 0.164.1-beta.18-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 (140) hide show
  1. package/build.gradle.kts +11 -0
  2. package/compose/build.gradle.kts +190 -0
  3. package/compose/build.gradle.kts.backup +190 -0
  4. package/compose/compose.podspec +47 -0
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +19 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +16 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +150 -0
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +106 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +826 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +134 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +475 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +214 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +216 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +259 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +182 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +328 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +97 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +217 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +470 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +131 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +203 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +175 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +16 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +263 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +52 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +38 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +39 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +299 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +103 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +191 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +386 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +595 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +157 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +39 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +175 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +225 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +320 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +81 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +309 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +34 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +360 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +24 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +69 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +105 -0
  123. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
  124. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +131 -0
  125. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  126. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +42 -0
  127. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  128. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +220 -0
  129. package/gradle/libs.versions.toml +69 -0
  130. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  131. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  132. package/gradle.properties +26 -0
  133. package/gradlew +252 -0
  134. package/gradlew.bat +94 -0
  135. package/ios/native-kits.podspec +1 -1
  136. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  137. package/ios-demo/README.md +120 -0
  138. package/local.properties +8 -0
  139. package/package.json +2 -2
  140. package/settings.gradle.kts +64 -0
@@ -0,0 +1,55 @@
1
+ package vn.momo.kits.utils
2
+
3
+ import androidx.compose.runtime.*
4
+ import org.jetbrains.compose.resources.DrawableResource
5
+ import org.jetbrains.compose.resources.InternalResourceApi
6
+ import org.jetbrains.compose.resources.ResourceItem
7
+ import org.jetbrains.compose.resources.readResourceBytes
8
+ @OptIn(InternalResourceApi::class)
9
+ @Composable
10
+ internal fun readJson(name: String): String {
11
+ val path = name.plus(".json")
12
+ val candidatePaths = listOf(
13
+ path,
14
+ "composeResources/vn.momo.compose.resources/$path",
15
+ )
16
+
17
+ val jsonContent by rememberState(path, { "" }) {
18
+ val cached = resourceCache.getOrPut(path) {
19
+ var content = ""
20
+ for (candidate in candidatePaths) {
21
+ try {
22
+ content = readResourceBytes(candidate).decodeToString()
23
+ break
24
+ } catch (_: Exception) {
25
+ // try next candidate
26
+ }
27
+ }
28
+ ResourceCache.JSON(content)
29
+ } as ResourceCache.JSON
30
+
31
+ cached.json
32
+ }
33
+
34
+ return jsonContent
35
+ }
36
+
37
+
38
+ private val resourceCache = mutableMapOf<String, ResourceCache>()
39
+
40
+ private sealed interface ResourceCache {
41
+ class JSON(val json: String) : ResourceCache
42
+ }
43
+
44
+ @Composable
45
+ fun <T> rememberState(
46
+ key: Any,
47
+ getDefault: () -> T,
48
+ block: suspend () -> T
49
+ ): MutableState<T> {
50
+ val state = remember(key) { mutableStateOf(getDefault()) }
51
+ LaunchedEffect(key) {
52
+ state.value = block()
53
+ }
54
+ return state
55
+ }
@@ -0,0 +1,15 @@
1
+ package vn.momo.kits.utils
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import vn.momo.kits.application.LocalComponentInformation
5
+
6
+ @Composable
7
+ internal fun getComponentId(
8
+ componentName: String? = null,
9
+ accessibilityLabel: String? = null,
10
+ ): String {
11
+ val componentInformation = LocalComponentInformation.current
12
+ if (accessibilityLabel != null) return accessibilityLabel
13
+ if (componentInformation?.componentId != null) return componentInformation.componentId
14
+ return ""
15
+ }
@@ -0,0 +1,105 @@
1
+ package vn.momo.kits.utils
2
+
3
+ import androidx.compose.foundation.layout.WindowInsets
4
+ import androidx.compose.foundation.layout.asPaddingValues
5
+ import androidx.compose.foundation.layout.systemBars
6
+ import androidx.compose.runtime.Composable
7
+ import androidx.compose.ui.Modifier
8
+ import androidx.compose.ui.composed
9
+ import androidx.compose.ui.draw.drawBehind
10
+ import androidx.compose.ui.geometry.Offset
11
+ import androidx.compose.ui.graphics.Color
12
+ import androidx.compose.ui.platform.LocalDensity
13
+ import androidx.compose.ui.unit.Dp
14
+ import androidx.compose.ui.unit.dp
15
+ import androidx.compose.ui.unit.min
16
+ import vn.momo.kits.const.AppNavigationBar
17
+ import vn.momo.kits.const.AppStatusBar
18
+ import vn.momo.kits.platform.getPlatformName
19
+ import kotlin.math.abs
20
+
21
+
22
+ internal fun Modifier.bottomBorder(strokeWidth: Dp, color: Color) = composed(
23
+ factory = {
24
+ val density = LocalDensity.current
25
+ val strokeWidthPx = density.run { strokeWidth.toPx() }
26
+ if (strokeWidth > 0.dp) {
27
+ Modifier.drawBehind {
28
+ val width = size.width
29
+ val height = size.height - strokeWidthPx / 2
30
+
31
+ drawLine(
32
+ color = color,
33
+ start = Offset(x = 0f, y = height),
34
+ end = Offset(x = width, y = height),
35
+ strokeWidth = strokeWidthPx
36
+ )
37
+ }
38
+ } else {
39
+ Modifier
40
+ }
41
+ }
42
+ )
43
+
44
+ fun <T> Boolean.ifTrue(trueCase: T, falseCase: T): T = if (this) trueCase else falseCase
45
+ fun <T> String.ifNotNull(trueCase: T, falseCase: T): T =
46
+ if (this.isBlank()) falseCase else trueCase
47
+
48
+ fun formatNumberToMoney(number: Long, currency: String = "đ"): String {
49
+ if (number == 0L) {
50
+ return "0$currency"
51
+ }
52
+ val array = mutableListOf<Char>()
53
+ var result = ""
54
+ var isNegative = false
55
+
56
+ if (number < 0) {
57
+ isNegative = true
58
+ }
59
+ val numberString = abs(number).toString() // Convert to Int to avoid decimal places
60
+ if (numberString.length < 3) {
61
+ return numberString + currency
62
+ }
63
+ var count = 0
64
+ for (i in numberString.length - 1 downTo 0) {
65
+ count += 1
66
+ array.add(numberString[i])
67
+ if (count == 3 && i >= 1) {
68
+ array.add('.')
69
+ count = 0
70
+ }
71
+ }
72
+
73
+ for (i in array.size - 1 downTo 0) {
74
+ result += array[i]
75
+ }
76
+
77
+ if (isNegative) {
78
+ result = "-$result"
79
+ }
80
+
81
+ return result + currency
82
+ }
83
+
84
+ @Composable
85
+ fun getAppStatusBarHeight(): Dp {
86
+ return if (AppStatusBar.current == 0.dp) {
87
+ WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
88
+ } else {
89
+ AppStatusBar.current
90
+ }
91
+ }
92
+
93
+ @Composable
94
+ internal fun getNavigationBarHeight(): Dp {
95
+ return if (AppNavigationBar.current == 0.dp) {
96
+ val systemBottomPadding = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()
97
+ if (getPlatformName() == "iOS") {
98
+ min(systemBottomPadding, 21.dp)
99
+ } else {
100
+ systemBottomPadding
101
+ }
102
+ } else {
103
+ AppNavigationBar.current
104
+ }
105
+ }
@@ -0,0 +1,40 @@
1
+ @file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
2
+
3
+ package vn.momo.kits.const
4
+
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.runtime.remember
7
+ import androidx.compose.ui.text.font.FontFamily
8
+ import androidx.compose.ui.text.platform.Font
9
+ import kotlinx.cinterop.ExperimentalForeignApi
10
+ import kotlinx.cinterop.addressOf
11
+ import kotlinx.cinterop.usePinned
12
+ import platform.Foundation.NSBundle
13
+ import platform.Foundation.NSData
14
+ import platform.Foundation.dataWithContentsOfFile
15
+ import platform.posix.memcpy
16
+
17
+ @Composable
18
+ internal actual fun hostFontFamily(fileName: String): FontFamily? = remember(fileName) {
19
+ val name = fileName.substringBeforeLast('.')
20
+ val ext = fileName.substringAfterLast('.', "")
21
+ // Font từ font-source nằm rời ở bundle root, không nằm trong compose-resources.
22
+ val path = NSBundle.mainBundle.pathForResource(name, ext) ?: return@remember null
23
+ FontFamily(
24
+ Font(
25
+ identity = fileName,
26
+ getData = {
27
+ NSData.dataWithContentsOfFile(path)?.toByteArray() ?: ByteArray(0)
28
+ },
29
+ ),
30
+ )
31
+ }
32
+
33
+ @OptIn(ExperimentalForeignApi::class)
34
+ private fun NSData.toByteArray(): ByteArray {
35
+ val size = length.toInt()
36
+ if (size == 0) return ByteArray(0)
37
+ return ByteArray(size).apply {
38
+ usePinned { pinned -> memcpy(pinned.addressOf(0), bytes, length) }
39
+ }
40
+ }
@@ -0,0 +1,131 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.DisposableEffect
5
+ import androidx.compose.runtime.remember
6
+ import androidx.navigationevent.NavigationEvent
7
+ import androidx.navigationevent.NavigationEventDispatcher
8
+ import androidx.navigationevent.NavigationEventInput
9
+
10
+ /**
11
+ * Kit end of the host's edge-pan recogniser.
12
+ *
13
+ * iOS keeps exactly one back-gesture recogniser and it belongs to the host app — the kit never
14
+ * installs its own, so the `.failed -> completed` conflict between two competing recognisers is
15
+ * unreachable by construction. The host replays its drag here; the events are fed into the
16
+ * `NavigationEventDispatcher` that [NavigationContainer] provides, which drives NavHost's
17
+ * `PredictiveBackHandler` and therefore `SeekableTransitionState.seekTo`. Parallax, scrim and edge
18
+ * shadow are children of that same transition, so they follow the finger for free.
19
+ *
20
+ * Only primitives cross the Swift boundary: `androidx.navigationevent` is a private transitive
21
+ * dependency and is absent from the generated framework header.
22
+ *
23
+ * **Every method must be called from the main thread.** Nothing here is synchronised:
24
+ * [attachedInputs] and [gestureInput] are also written from the composition's `DisposableEffect`,
25
+ * and the events land on Compose state. The host drives this from its `UIPanGestureRecognizer`
26
+ * action, which UIKit already delivers on main.
27
+ */
28
+ object HostBackGestureBridge {
29
+ private val attachedInputs = mutableListOf<HostNavigationEventInput>()
30
+ private var gestureInput: HostNavigationEventInput? = null
31
+
32
+ /**
33
+ * Whether the top-most mounted container has an enabled back handler, i.e. the authoritative
34
+ * answer to "will Compose consume this back?". `false` means no container is mounted, or the
35
+ * mounted one deliberately opts out (kill switch, overlay showing, custom back handler), and
36
+ * the host should fall back to its own navigation.
37
+ */
38
+ val hasEnabledHandlers: Boolean
39
+ get() = attachedInputs.lastOrNull()?.hasEnabledHandlers == true
40
+
41
+ /**
42
+ * Whether the host should run its floating-arrow interaction instead of replaying the drag
43
+ * here. Set while a bottom tab other than the first one is showing: every tab lives inside a
44
+ * single stack card, so seeking that card would take all of them with it rather than
45
+ * returning to the first tab. Read it when the recogniser begins — the kit keeps it current.
46
+ *
47
+ * Mirrors `MiniAppModule.setShouldUseFloatingArrowBack` on the React Native side, so both
48
+ * stacks hand the same case to the same host interaction.
49
+ */
50
+ var shouldUseFloatingArrowBack: Boolean = false
51
+ internal set
52
+
53
+ /** Begins a drag. Pairs with exactly one [completed] or [cancelled]. */
54
+ fun started() {
55
+ val input = attachedInputs.lastOrNull() ?: return
56
+ gestureInput = input
57
+ input.backStarted()
58
+ }
59
+
60
+ /** Drag progress in `0f..1f` — the fraction of the screen width travelled. */
61
+ fun progressed(progress: Float) {
62
+ gestureInput?.backProgressed(progress.coerceIn(0f, 1f))
63
+ }
64
+
65
+ /**
66
+ * Released past the commit threshold: the screen pops.
67
+ *
68
+ * Falls back to the attached input when no drag was started, so a host that commits without
69
+ * ever seeking still gets a back. Dispatching a bare completion is the documented
70
+ * non-predictive path — the same shape as a back button press — and dropping it here is what
71
+ * silently stranded the mini-app root, where the host never seeks.
72
+ */
73
+ fun completed() {
74
+ (gestureInput ?: attachedInputs.lastOrNull())?.backCompleted()
75
+ gestureInput = null
76
+ }
77
+
78
+ /** Released below the commit threshold: the screen springs back. */
79
+ fun cancelled() {
80
+ gestureInput?.backCancelled()
81
+ gestureInput = null
82
+ }
83
+
84
+ internal fun attach(input: HostNavigationEventInput) {
85
+ attachedInputs += input
86
+ }
87
+
88
+ internal fun detach(input: HostNavigationEventInput) {
89
+ attachedInputs -= input
90
+ // A container unmounted mid-drag must not hand its pending terminal event to whichever
91
+ // container is on top now: NavigationEventInput fabricates a non-predictive start for a
92
+ // completed/cancelled it never saw begin, which would pop a screen out of nowhere.
93
+ if (gestureInput === input) gestureInput = null
94
+ }
95
+ }
96
+
97
+ /** Wires one [NavigationContainer]'s dispatcher to [HostBackGestureBridge] while it is composed. */
98
+ @Composable
99
+ internal fun AttachHostBackGesture(dispatcher: NavigationEventDispatcher) {
100
+ val input = remember(dispatcher) { HostNavigationEventInput() }
101
+
102
+ DisposableEffect(dispatcher, input) {
103
+ dispatcher.addInput(input)
104
+ HostBackGestureBridge.attach(input)
105
+ onDispose {
106
+ HostBackGestureBridge.detach(input)
107
+ dispatcher.removeInput(input)
108
+ }
109
+ }
110
+ }
111
+
112
+ internal class HostNavigationEventInput : NavigationEventInput() {
113
+ var hasEnabledHandlers: Boolean = false
114
+ private set
115
+
116
+ override fun onHasEnabledHandlersChanged(hasEnabledHandlers: Boolean) {
117
+ this.hasEnabledHandlers = hasEnabledHandlers
118
+ }
119
+
120
+ // Touch coordinates are not forwarded — the stack transition is seeked by progress alone, and
121
+ // leaving them out keeps the bridge's Swift-facing signatures primitive.
122
+ fun backStarted() = dispatchOnBackStarted(NavigationEvent(swipeEdge = NavigationEvent.EDGE_LEFT))
123
+
124
+ fun backProgressed(progress: Float) = dispatchOnBackProgressed(
125
+ NavigationEvent(swipeEdge = NavigationEvent.EDGE_LEFT, progress = progress)
126
+ )
127
+
128
+ fun backCancelled() = dispatchOnBackCancelled()
129
+
130
+ fun backCompleted() = dispatchOnBackCompleted()
131
+ }
@@ -0,0 +1,32 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.DisposableEffect
5
+ import platform.Foundation.NSNotification
6
+ import platform.Foundation.NSNotificationCenter
7
+ import platform.Foundation.NSOperationQueue
8
+ import platform.darwin.NSObjectProtocol
9
+
10
+ private const val STATUS_BAR_TAPPED_NOTIFICATION = "statusBarSelected"
11
+
12
+ @Composable
13
+ internal actual fun RegisterScrollToTop(callback: (() -> Unit)?) {
14
+ val maxApi = LocalMaxApi.current
15
+ DisposableEffect(callback) {
16
+ val token: NSObjectProtocol? = if (callback != null) {
17
+ NSNotificationCenter.defaultCenter.addObserverForName(
18
+ name = STATUS_BAR_TAPPED_NOTIFICATION,
19
+ `object` = null,
20
+ queue = NSOperationQueue.mainQueue,
21
+ ) { _: NSNotification? ->
22
+ runCatching { callback() }.onFailure {
23
+ maxApi?.logFile("[ScrollToTop] callback threw: ${it.message}") {}
24
+
25
+ }
26
+ }
27
+ } else null
28
+ onDispose {
29
+ token?.let { NSNotificationCenter.defaultCenter.removeObserver(it) }
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,42 @@
1
+ package vn.momo.kits.platform
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.LaunchedEffect
5
+ import androidx.compose.runtime.State
6
+ import androidx.compose.runtime.mutableStateOf
7
+ import androidx.compose.runtime.remember
8
+ import androidx.compose.ui.InternalComposeUiApi
9
+ import androidx.compose.ui.backhandler.LocalCompatNavigationEventDispatcherOwner
10
+ import androidx.navigationevent.NavigationEventTransitionState
11
+ import vn.momo.kits.navigation.HostBackGestureBridge
12
+
13
+ // NavigationEventDispatcher.transitionState is a passive observer: UIKitNavigationEventInput
14
+ // publishes every edge-pan event to it without any handler consuming the gesture. Its `progress`
15
+ // is touch.x / view width, i.e. a plain fraction of the screen width.
16
+ @OptIn(InternalComposeUiApi::class)
17
+ @Composable
18
+ actual fun rememberBackGestureProgress(): State<Float?> {
19
+ val dispatcher = LocalCompatNavigationEventDispatcherOwner.current?.navigationEventDispatcher
20
+ val progress = remember { mutableStateOf<Float?>(null) }
21
+
22
+ LaunchedEffect(dispatcher) {
23
+ if (dispatcher == null) {
24
+ progress.value = null
25
+ return@LaunchedEffect
26
+ }
27
+ dispatcher.transitionState.collect { state ->
28
+ progress.value = (state as? NavigationEventTransitionState.InProgress)
29
+ ?.takeIf { it.direction == NavigationEventTransitionState.TRANSITIONING_BACK }
30
+ ?.latestEvent
31
+ ?.progress
32
+ }
33
+ }
34
+
35
+ return progress
36
+ }
37
+
38
+ // The host pulls this when its recogniser begins, so a plain assignment is enough -- no dispatch,
39
+ // no observer. Kept next to the progress observer since both are the same host contract.
40
+ actual fun reportFloatingArrowBack(shouldUse: Boolean) {
41
+ HostBackGestureBridge.shouldUseFloatingArrowBack = shouldUse
42
+ }
@@ -0,0 +1,66 @@
1
+ package vn.momo.kits.platform
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.CompositionLocalProvider
5
+ import androidx.compose.runtime.DisposableEffect
6
+ import androidx.compose.runtime.mutableStateOf
7
+ import androidx.compose.runtime.remember
8
+ import androidx.compose.ui.platform.LocalDensity
9
+ import androidx.compose.ui.unit.Density
10
+ import kotlinx.cinterop.ExperimentalForeignApi
11
+ import kotlinx.cinterop.ObjCAction
12
+ import platform.Foundation.NSNotification
13
+ import platform.Foundation.NSNotificationCenter
14
+ import platform.Foundation.NSSelectorFromString
15
+ import platform.UIKit.UIContentSizeCategoryDidChangeNotification
16
+ import platform.UIKit.UIFont
17
+ import platform.UIKit.UIFontTextStyleBody
18
+ import platform.darwin.NSObject
19
+
20
+ // Compose MP iOS reads LocalDensity.fontScale once at ComposeUIViewController creation and does not
21
+ // recompose it when UIContentSizeCategory changes (Android tracks Configuration.fontScale live). We
22
+ // observe the notification and override LocalDensity.fontScale so scaleSize() recomposes live. The
23
+ // formula matches the SwiftUI kit (UIFont body point size / 17) so both iOS surfaces scale alike.
24
+ private const val DEFAULT_BODY_POINT_SIZE = 17.0
25
+
26
+ private fun currentOsFontScale(): Float =
27
+ (UIFont.preferredFontForTextStyle(UIFontTextStyleBody).pointSize / DEFAULT_BODY_POINT_SIZE).toFloat()
28
+
29
+ @OptIn(ExperimentalForeignApi::class)
30
+ @Composable
31
+ actual fun ProvideOsFontScale(content: @Composable () -> Unit) {
32
+ val osFontScale = remember { mutableStateOf(currentOsFontScale()) }
33
+
34
+ val listener = remember {
35
+ object : NSObject() {
36
+ @Suppress("unused")
37
+ @ObjCAction
38
+ fun contentSizeChanged(arg: NSNotification) {
39
+ osFontScale.value = currentOsFontScale()
40
+ }
41
+ }
42
+ }
43
+
44
+ DisposableEffect(Unit) {
45
+ NSNotificationCenter.defaultCenter.addObserver(
46
+ observer = listener,
47
+ selector = NSSelectorFromString(listener::contentSizeChanged.name + ":"),
48
+ name = UIContentSizeCategoryDidChangeNotification,
49
+ `object` = null,
50
+ )
51
+ onDispose {
52
+ NSNotificationCenter.defaultCenter.removeObserver(
53
+ observer = listener,
54
+ name = UIContentSizeCategoryDidChangeNotification,
55
+ `object` = null,
56
+ )
57
+ }
58
+ }
59
+
60
+ val density = LocalDensity.current
61
+ CompositionLocalProvider(
62
+ LocalDensity provides Density(density.density, osFontScale.value),
63
+ ) {
64
+ content()
65
+ }
66
+ }