@momo-kits/native-kits 0.162.1-beta.1-debug → 0.162.1-beta.12-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 (66) hide show
  1. package/CLAUDE.md +78 -0
  2. package/compose/build.gradle.kts +59 -58
  3. package/compose/build.gradle.kts.backup +61 -60
  4. package/compose/compose.podspec +1 -1
  5. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +13 -14
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +2 -2
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +5 -7
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +2 -2
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +21 -19
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +1 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +69 -26
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +3 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +2 -5
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +1 -1
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +6 -3
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +5 -7
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +21 -22
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +2 -2
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +2 -2
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +3 -2
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +66 -25
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +42 -28
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +4 -3
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +25 -16
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +3 -1
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +4 -3
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +4 -4
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -10
  33. package/example/ios/Example.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
  34. package/example/ios/Example.xcworkspace/xcuserdata/huynhdung.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  35. package/example/ios/Example.xcworkspace/xcuserdata/huynhdung.xcuserdatad/xcschemes/xcschememanagement.plist +5 -0
  36. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +58 -0
  37. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/Pods-Example.xcscheme +58 -0
  38. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/SDWebImage.xcscheme +58 -0
  39. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +58 -0
  40. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/SkeletonUI.xcscheme +58 -0
  41. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/xcschememanagement.plist +46 -0
  42. package/gradle/libs.versions.toml +2 -0
  43. package/gradle.properties +1 -1
  44. package/ios/Application/ApplicationEnvironment.swift +1 -0
  45. package/ios/Application/Components.swift +1 -0
  46. package/ios/Application/FloatingButton.swift +1 -0
  47. package/ios/Application/FontScaleStore.swift +59 -0
  48. package/ios/Badge/Badge.swift +1 -0
  49. package/ios/Badge/BadgeRibbon.swift +2 -0
  50. package/ios/Button/Button.swift +1 -1
  51. package/ios/Checkbox/Checkbox.swift +1 -0
  52. package/ios/Information/Information.swift +151 -0
  53. package/ios/Input/Input.swift +1 -0
  54. package/ios/Input/InputPhoneNumber.swift +1 -0
  55. package/ios/Input/InputSearch.swift +3 -0
  56. package/ios/Input/InputTextArea.swift +1 -0
  57. package/ios/OTPKeyboard/KeyboardButton.swift +1 -1
  58. package/ios/Popup/PopupDisplay.swift +6 -0
  59. package/ios/Popup/PopupInput.swift +2 -0
  60. package/ios/Template/TrustBanner/TrustBanner.swift +2 -0
  61. package/ios/Typography/Text.swift +13 -7
  62. package/ios/Typography/Typography.swift +22 -8
  63. package/local.properties +2 -2
  64. package/package.json +1 -1
  65. package/.claude/settings.local.json +0 -32
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Deprecated.kt +0 -107
@@ -18,8 +18,7 @@ import androidx.compose.ui.draw.clip
18
18
  import androidx.compose.ui.graphics.Color
19
19
  import androidx.compose.ui.layout.ContentScale
20
20
  import androidx.compose.ui.unit.dp
21
- import vn.momo.kits.application.AppConfig
22
- import vn.momo.kits.application.AppLanguage
21
+ import vn.momo.kits.application.LocalLocalize
23
22
  import vn.momo.kits.application.IsShowBaseLineDebug
24
23
  import vn.momo.kits.const.Colors
25
24
  import vn.momo.kits.const.Typography
@@ -68,9 +67,8 @@ fun TrustBanner(
68
67
  onPress: ((Map<String, String>) -> Unit)? = null,
69
68
  trackEvent: ((String, Map<String, String>) -> Unit)? = null
70
69
  ) {
71
- val appConfig = AppConfig.current
72
- val language = AppLanguage.current ?: "vi"
73
- val trustBanner = appConfig?.trustBanner ?: defaultBanner
70
+ val localize = LocalLocalize.current
71
+ val trustBanner = defaultBanner
74
72
  val trackParams = mapOf(
75
73
  "service_name" to serviceName,
76
74
  "screen_name" to screenName,
@@ -124,7 +122,7 @@ fun TrustBanner(
124
122
  .padding(start = 10.dp)
125
123
  ) {
126
124
  Text(
127
- text = trustBanner.content[language] ?: "",
125
+ text = localize.translateData(trustBanner.content),
128
126
  modifier = Modifier.padding(bottom = 8.dp),
129
127
  color = Color(contentColor),
130
128
  style = Typography.descriptionDefaultRegular,
@@ -138,7 +136,7 @@ fun TrustBanner(
138
136
  verticalAlignment = Alignment.CenterVertically,
139
137
  ) {
140
138
  Text(
141
- text = trustBanner.subContent[language] ?: "",
139
+ text = localize.translateData(trustBanner.subContent),
142
140
  color = Color(subContentColor),
143
141
  style = Typography.actionXsBold,
144
142
  maxLines = 2
@@ -11,7 +11,7 @@ import androidx.compose.ui.unit.*
11
11
  import org.jetbrains.compose.resources.Font
12
12
  import org.jetbrains.compose.resources.FontResource
13
13
  import org.jetbrains.compose.resources.InternalResourceApi
14
- import vn.momo.kits.application.ScaleSizeMaxRate
14
+ import vn.momo.kits.application.LocalFontScaleConfig
15
15
  import vn.momo.kits.platform.getScreenDimensions
16
16
  import vn.momo.uikits.resources.*
17
17
  import kotlin.math.max
@@ -22,59 +22,58 @@ const val MAX_FONT_SCALE = 1.5f
22
22
  const val MAX_DEVICE_SCALE = 5
23
23
 
24
24
  @Composable
25
- fun scaleSize(size: Float): Float {
26
- val scaleSizeMaxRate: Float = ScaleSizeMaxRate.current ?: MAX_FONT_SCALE
27
- val deviceWidth = getScreenDimensions().width
28
- val deviceScale = deviceWidth / DEFAULT_SCREEN_SIZE
25
+ fun scaleSize(size: Float, userScaleRate: Float? = null): Float {
26
+ val fontScaleConfig = LocalFontScaleConfig.current
27
+ val useOSFontScale = fontScaleConfig.useOSFontScale
28
+ val customRate = userScaleRate ?: fontScaleConfig.userScaleRate ?: 1f
29
29
 
30
- val density = LocalDensity.current
31
- val fontScale = density.fontScale
30
+ val deviceScale = getScreenDimensions().width / DEFAULT_SCREEN_SIZE
31
+ val fontScale = fontScaleConfig.osFontScale ?: LocalDensity.current.fontScale
32
+
33
+ if (!useOSFontScale) {
34
+ return if (customRate > 1f) customRate * size else size
35
+ }
32
36
 
33
37
  var fontSizeScaleDevice = size
34
38
  var fontSizeScaleOS = size
35
-
36
39
  if (deviceScale > 1) {
37
40
  fontSizeScaleDevice =
38
41
  min(deviceScale * fontSizeScaleDevice, fontSizeScaleDevice + MAX_DEVICE_SCALE)
39
42
  }
40
-
41
- if (fontScale > 1) {
42
- fontSizeScaleOS = min(fontScale * fontSizeScaleOS, fontSizeScaleOS * scaleSizeMaxRate)
43
+ if (fontScale > 1f) {
44
+ fontSizeScaleOS = min(fontScale * fontSizeScaleOS, fontSizeScaleOS * MAX_FONT_SCALE)
43
45
  }
44
46
 
45
- return max(
46
- fontSizeScaleDevice,
47
- fontSizeScaleOS
48
- )
47
+ return max(fontSizeScaleDevice, fontSizeScaleOS)
49
48
  }
50
49
 
51
50
  @Composable
52
- fun scaleSize(size: TextUnit): TextUnit {
51
+ fun scaleSize(size: TextUnit, userScaleRate: Float? = null): TextUnit {
53
52
  if (!size.isSp) return size
54
53
 
55
54
  val density = LocalDensity.current
56
55
 
57
- val scaled = scaleSize(size.value)
56
+ val scaled = scaleSize(size.value, userScaleRate)
58
57
  val spValue = scaled / density.fontScale
59
58
 
60
59
  return TextUnit(value = spValue, type = TextUnitType.Sp)
61
60
  }
62
61
 
63
62
  @Composable
64
- fun scaleSize(size: Dp): Dp {
65
- return scaleSize(size.value).dp
63
+ fun scaleSize(size: Dp, userScaleRate: Float? = null): Dp {
64
+ return scaleSize(size.value, userScaleRate).dp
66
65
  }
67
66
 
68
67
  @Composable
69
- fun scaleSize(textStyle: TextStyle): TextStyle {
68
+ fun scaleSize(textStyle: TextStyle, userScaleRate: Float? = null): TextStyle {
70
69
  return textStyle.copy(
71
70
  fontSize = if (textStyle.fontSize != TextUnit.Unspecified)
72
- scaleSize(textStyle.fontSize)
71
+ scaleSize(textStyle.fontSize, userScaleRate)
73
72
  else
74
73
  TextUnit.Unspecified,
75
74
 
76
75
  lineHeight = if (textStyle.lineHeight != TextUnit.Unspecified)
77
- scaleSize(textStyle.lineHeight)
76
+ scaleSize(textStyle.lineHeight, userScaleRate)
78
77
  else
79
78
  TextUnit.Unspecified,
80
79
  )
@@ -0,0 +1,104 @@
1
+ package vn.momo.kits.modifier
2
+
3
+ import androidx.compose.animation.core.LinearEasing
4
+ import androidx.compose.animation.core.RepeatMode
5
+ import androidx.compose.animation.core.animateFloat
6
+ import androidx.compose.animation.core.infiniteRepeatable
7
+ import androidx.compose.animation.core.rememberInfiniteTransition
8
+ import androidx.compose.animation.core.tween
9
+ import androidx.compose.runtime.Composable
10
+ import androidx.compose.runtime.getValue
11
+ import androidx.compose.ui.Modifier
12
+ import androidx.compose.ui.draw.drawWithContent
13
+ import androidx.compose.ui.geometry.Offset
14
+ import androidx.compose.ui.graphics.Color
15
+ import androidx.compose.ui.graphics.drawscope.clipRect
16
+ import androidx.compose.ui.text.TextStyle
17
+ import androidx.compose.ui.text.drawText
18
+ import androidx.compose.ui.text.font.FontWeight
19
+ import androidx.compose.ui.text.rememberTextMeasurer
20
+ import androidx.compose.ui.unit.sp
21
+ import vn.momo.kits.application.IsShowBaseLineDebug
22
+ import vn.momo.kits.const.Colors
23
+
24
+ @Suppress("ExperimentalAnnotationRetention")
25
+ @RequiresOptIn(
26
+ message = "This is internal API of native-kits, become internal in next sprint",
27
+ level = RequiresOptIn.Level.WARNING,
28
+ )
29
+ @Retention(AnnotationRetention.BINARY)
30
+ @Target(
31
+ AnnotationTarget.CLASS,
32
+ AnnotationTarget.FUNCTION,
33
+ AnnotationTarget.PROPERTY,
34
+ )
35
+ annotation class InternalApi
36
+
37
+ @Composable
38
+ fun DeprecatedModifier(text: String = "Deprecated component"): Modifier = debugMarker(
39
+ label = text,
40
+ background = Colors.red_04.copy(alpha = 0.7f),
41
+ textColor = Colors.red_01,
42
+ animationLabel = "deprecatedMarquee",
43
+ )
44
+
45
+ @Composable
46
+ private fun debugMarker(
47
+ label: String,
48
+ background: Color,
49
+ textColor: Color,
50
+ animationLabel: String,
51
+ ): Modifier {
52
+ if (!IsShowBaseLineDebug) return Modifier
53
+
54
+ val textMeasurer = rememberTextMeasurer()
55
+ val marquee = rememberInfiniteTransition(label = animationLabel)
56
+ val progress by marquee.animateFloat(
57
+ initialValue = 0f,
58
+ targetValue = 1f,
59
+ animationSpec = infiniteRepeatable(
60
+ animation = tween(durationMillis = 4000, easing = LinearEasing),
61
+ repeatMode = RepeatMode.Restart,
62
+ ),
63
+ label = "${animationLabel}Progress",
64
+ )
65
+
66
+ return Modifier.drawWithContent {
67
+ drawContent()
68
+ drawRect(color = background)
69
+
70
+ if (size.width <= 0f || size.height <= 0f) return@drawWithContent
71
+
72
+ val fontPx = (size.width * 0.1f).coerceIn(12.sp.toPx(), 20.sp.toPx())
73
+ val style = TextStyle(
74
+ color = textColor,
75
+ fontSize = fontPx.toSp(),
76
+ fontWeight = FontWeight.Bold,
77
+ )
78
+
79
+ val intrinsic = textMeasurer.measure(
80
+ text = label,
81
+ style = style,
82
+ maxLines = 1,
83
+ softWrap = false,
84
+ )
85
+ val textW = intrinsic.size.width
86
+ val textH = intrinsic.size.height
87
+ val y = (size.height - textH) / 2f
88
+
89
+ if (textW <= size.width) {
90
+ drawText(
91
+ textLayoutResult = intrinsic,
92
+ topLeft = Offset((size.width - textW) / 2f, y),
93
+ )
94
+ } else {
95
+ clipRect(right = size.width, bottom = size.height) {
96
+ val gap = textW * 0.5f
97
+ val travel = textW + gap
98
+ val x = -progress * travel
99
+ drawText(textLayoutResult = intrinsic, topLeft = Offset(x, y))
100
+ drawText(textLayoutResult = intrinsic, topLeft = Offset(x + travel, y))
101
+ }
102
+ }
103
+ }
104
+ }
@@ -41,7 +41,7 @@ import vn.momo.kits.const.AppTheme
41
41
  import vn.momo.kits.const.Colors
42
42
  import vn.momo.kits.const.Radius
43
43
  import vn.momo.kits.const.Spacing
44
- import vn.momo.kits.application.ApplicationContext
44
+ import vn.momo.kits.application.LocalContext
45
45
  import vn.momo.kits.application.IsShowBaseLineDebug
46
46
  import vn.momo.kits.const.Typography
47
47
  import vn.momo.kits.navigation.tracking.ScreenTracker
@@ -73,7 +73,7 @@ internal fun BottomSheet(
73
73
  val coroutineScope = rememberCoroutineScope()
74
74
 
75
75
  val maxApi = LocalMaxApi.current
76
- val context = ApplicationContext.current
76
+ val context = LocalContext.current
77
77
 
78
78
  DisposableEffect(Unit) {
79
79
  ScreenTracker.trackPopupDisplayed(
@@ -18,7 +18,7 @@ import androidx.compose.ui.draw.scale
18
18
  import androidx.compose.ui.graphics.Color
19
19
  import androidx.compose.ui.unit.dp
20
20
  import kotlinx.coroutines.launch
21
- import vn.momo.kits.application.ApplicationContext
21
+ import vn.momo.kits.application.LocalContext
22
22
  import vn.momo.kits.application.IsShowBaseLineDebug
23
23
  import vn.momo.kits.const.Colors
24
24
  import vn.momo.kits.modifier.conditional
@@ -40,7 +40,7 @@ internal fun ModalScreen(
40
40
  val coroutineScope = rememberCoroutineScope()
41
41
 
42
42
  val maxApi = LocalMaxApi.current
43
- val context = ApplicationContext.current
43
+ val context = LocalContext.current
44
44
 
45
45
  DisposableEffect(Unit) {
46
46
  ScreenTracker.trackPopupDisplayed(
@@ -9,7 +9,8 @@ import vn.momo.kits.navigation.component.*
9
9
  class Navigation(
10
10
  val id: Int = -1,
11
11
  val bottomTabIndex: Int = -1,
12
- val initOptions: NavigationOptions? = null
12
+ val initOptions: NavigationOptions? = null,
13
+ private val registry: DynamicScreenRegistry? = null
13
14
  ) {
14
15
  private val _options = mutableStateOf(initOptions ?: NavigationOptions())
15
16
  val currentOptions: State<NavigationOptions> get() = _options
@@ -56,7 +57,7 @@ class Navigation(
56
57
  private fun updateOptions(updated: NavigationOptions) {
57
58
  _options.value = updated
58
59
  if (bottomTabIndex != -1) setBottomTabOption(bottomTabIndex, updated)
59
- if (id != -1) DynamicScreenRegistry.setOptions(id, updated)
60
+ if (id != -1) registry?.setOptions(id, updated)
60
61
  }
61
62
  }
62
63
 
@@ -22,50 +22,57 @@ fun NavigationContainer(
22
22
  initialScreen: @Composable () -> Unit,
23
23
  options: NavigationOptions? = null,
24
24
  initialTheme: Theme = defaultTheme,
25
- applicationContext: MiniAppContext? = null,
26
- maxApi: IMaxApi? = null,
25
+ miniAppContext: MiniAppContext? = null,
26
+ maxApi: IMaxApi?,
27
27
  setNavigator: ((Navigator) -> Unit)? = null,
28
28
  statusBarHeight: Dp? = null,
29
- config: KitConfig? = null,
30
- language: String? = null
29
+ localize: Localize? = null,
31
30
  ){
32
31
  val navController = rememberNavController()
33
- val navigator = remember { Navigator(navController = navController, maxApi = maxApi) }
32
+ val registry = remember { DynamicScreenRegistry() }
33
+ val navigator = remember { Navigator(navController = navController, maxApi = maxApi, registry = registry) }
34
34
  val statusBarHeight = statusBarHeight ?: getAppStatusBarHeight()
35
35
  val navigationBarHeight = getNavigationBarHeight()
36
36
 
37
- val parentContext = ApplicationContext.current
38
- val mergedContext = MiniAppContext.merge(parentContext, applicationContext)
37
+ val parentContext = LocalContext.current
38
+ val mergedContext = MiniAppContext.merge(parentContext, miniAppContext)
39
39
 
40
40
  val theme = remember { mutableStateOf(initialTheme) }
41
+ val fallbackLocalize = remember { Localize() }
41
42
 
42
- LaunchedEffect(Unit) {
43
- val headerBar = config?.headerBar
44
- if (headerBar != null && theme.value.assets.headerBackground == null) {
45
- theme.value = theme.value.copy(
46
- assets = ThemeAssets(
47
- headerBackground = headerBar
48
- )
49
- )
43
+ val resolvedLocalize = localize ?: fallbackLocalize
44
+
45
+ var observerFontScaleConfig by remember { mutableStateOf(mergedContext?.fontScaleConfig) }
46
+ LaunchedEffect(maxApi, resolvedLocalize) {
47
+ val api = maxApi ?: return@LaunchedEffect
48
+ runCatching {
49
+ api.getLanguage(callback = { data ->
50
+ if (parseLanguage(data) == Localize.EN) {
51
+ resolvedLocalize.changeLanguage(Localize.EN)
52
+ }
53
+ })
54
+ api.observer(FONT_SCALE_OBSERVER_KEY).collect { data ->
55
+ observerFontScaleConfig = parseFontScaleConfig(data)
56
+ }
50
57
  }
51
58
  }
52
59
 
53
- val screenId = rememberSaveable { DynamicScreenRegistry.nextId() }
54
- DynamicScreenRegistry.bind(screenId, initialScreenName, initialScreen, options)
60
+ val screenId = rememberSaveable { registry.nextId() }
61
+ registry.bind(screenId, initialScreenName, initialScreen, options)
55
62
  val startDestination = remember(screenId) { DynamicScreenRoute(screenId) }
56
63
 
57
64
  ProvideNavigationEventDispatcherOwner {
58
65
  CompositionLocalProvider(
59
66
  LocalNavigator provides navigator,
67
+ LocalDynamicScreenRegistry provides registry,
60
68
  LocalMaxApi provides maxApi,
61
69
  AppTheme provides theme.value,
62
70
  AppStatusBar provides statusBarHeight,
63
71
  AppNavigationBar provides navigationBarHeight,
64
- ApplicationContext provides mergedContext,
65
- AppConfig provides config,
66
- AppLanguage provides language,
67
- ScaleSizeMaxRate provides mergedContext?.scaleSizeMaxRate,
68
- ) {
72
+ LocalContext provides mergedContext,
73
+ LocalLocalize provides resolvedLocalize,
74
+ LocalFontScaleConfig provides (observerFontScaleConfig ?: LocalFontScaleConfig.current),
75
+ ) {
69
76
  LaunchedEffect(Unit) {
70
77
  setNavigator?.invoke(navigator)
71
78
  }
@@ -88,7 +95,7 @@ fun NavigationContainer(
88
95
  }
89
96
  ) { backStackEntry ->
90
97
  val route = backStackEntry.toRoute<DynamicScreenRoute>()
91
- val screen = DynamicScreenRegistry.getScreen(route.id)
98
+ val screen = registry.getScreen(route.id)
92
99
 
93
100
  if (screen != null) {
94
101
  StackScreen(
@@ -117,7 +124,7 @@ fun NavigationContainer(
117
124
  }
118
125
  ) { backStackEntry ->
119
126
  val route = backStackEntry.toRoute<DynamicDialogRoute>()
120
- val screen = DynamicScreenRegistry.getScreen(route.id)
127
+ val screen = registry.getScreen(route.id)
121
128
 
122
129
  if (screen != null) {
123
130
  StackScreen(
@@ -135,7 +142,7 @@ fun NavigationContainer(
135
142
  DisposableEffect(Unit) {
136
143
  onDispose {
137
144
  navigator.dispose()
138
- DynamicScreenRegistry.unregisterScreen(screenId)
145
+ registry.unregisterScreen(screenId)
139
146
  }
140
147
  }
141
148
  }
@@ -143,3 +150,37 @@ fun NavigationContainer(
143
150
  val LocalMaxApi = staticCompositionLocalOf<IMaxApi?> {
144
151
  error("No MaxApi provided")
145
152
  }
153
+
154
+ private const val FONT_SCALE_OBSERVER_KEY = "font_scale_config"
155
+
156
+ private fun parseLanguage(raw: Map<String, Any?>?): String? {
157
+ val code = (raw?.get("response") as? String)?.trim()?.lowercase()
158
+ if (code.isNullOrEmpty()) return null
159
+ return if (code.startsWith(Localize.EN)) Localize.EN else Localize.VI
160
+ }
161
+
162
+ private fun parseFontScaleConfig(raw: Map<String, Any?>?): FontScaleConfig? {
163
+ if (raw.isNullOrEmpty()) return null
164
+ @Suppress("UNCHECKED_CAST")
165
+ val data = (raw["response"] as? Map<String, Any?>)
166
+ ?: (raw["data"] as? Map<String, Any?>)
167
+ ?: raw
168
+ if (!data.containsKey("useOSFontScale") && !data.containsKey("userScaleRate")) return null
169
+ val useOS = when (val v = data["useOSFontScale"]) {
170
+ is Boolean -> v
171
+ is String -> v.equals("true", ignoreCase = true)
172
+ is Number -> v.toInt() != 0
173
+ else -> true
174
+ }
175
+ val rate = when (val v = data["userScaleRate"]) {
176
+ is Number -> v.toFloat()
177
+ is String -> v.toFloatOrNull()
178
+ else -> null
179
+ }
180
+ val osScale = when (val v = data["osFontScale"]) {
181
+ is Number -> v.toFloat()
182
+ is String -> v.toFloatOrNull()
183
+ else -> null
184
+ }
185
+ return FontScaleConfig(useOSFontScale = useOS, userScaleRate = rate, osFontScale = osScale)
186
+ }
@@ -9,6 +9,7 @@ import androidx.compose.runtime.staticCompositionLocalOf
9
9
  import androidx.compose.ui.Alignment
10
10
  import androidx.compose.ui.Modifier
11
11
  import androidx.navigation.NavController
12
+ import androidx.navigation.toRoute
12
13
  import kotlinx.coroutines.CoroutineScope
13
14
  import kotlinx.coroutines.Dispatchers
14
15
  import kotlinx.coroutines.SupervisorJob
@@ -25,27 +26,34 @@ import kotlin.time.Duration.Companion.milliseconds
25
26
 
26
27
  class Navigator(
27
28
  private val navController: NavController,
28
- private val maxApi: IMaxApi?
29
+ private val maxApi: IMaxApi?,
30
+ private val registry: DynamicScreenRegistry
29
31
  ) {
30
32
  private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
31
33
 
34
+ private fun currentScreen(): DynamicScreen? {
35
+ val entry = navController.currentBackStackEntry ?: return null
36
+ val id = runCatching { entry.toRoute<DynamicScreenRoute>().id }.getOrNull()
37
+ ?: runCatching { entry.toRoute<DynamicDialogRoute>().id }.getOrNull()
38
+ ?: return null
39
+ return registry.getScreen(id)
40
+ }
41
+
32
42
  fun push(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
33
- val route = DynamicScreenRegistry.register(screenName, content, options)
43
+ val route = registry.register(screenName, content, options)
34
44
  navController.navigate(DynamicScreenRoute(route.id))
35
45
  }
36
46
 
37
47
  fun replace(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
38
- if (navController.previousBackStackEntry != null){
39
- val latestScreen = DynamicScreenRegistry.getLatestScreen()
40
- latestScreen?.let { DynamicScreenRegistry.unregisterScreen(it.id) }
41
- navController.popBackStack()
42
- push(screenName, content, options)
43
- }
48
+ val replacedScreen = currentScreen()
49
+ replacedScreen?.let { registry.unregisterScreen(it.id) }
50
+ navController.popBackStack()
51
+ push(screenName, content, options)
44
52
  }
45
53
 
46
54
  fun onBackSafe(callBack: (() -> Unit)? = null) {
47
- val latestScreen = DynamicScreenRegistry.getLatestScreen()
48
- val options = latestScreen?.options
55
+ val currentScreen = currentScreen()
56
+ val options = currentScreen?.options
49
57
  if (options?.onBackHandler != null) {
50
58
  options.onBackHandler.invoke()
51
59
  return
@@ -94,25 +102,24 @@ class Navigator(
94
102
 
95
103
  private suspend fun dismissScreen() {
96
104
  if (navController.previousBackStackEntry != null){
105
+ val dismissing = currentScreen()
97
106
  navController.popBackStack()
98
107
  delay(300L.milliseconds)
99
- DynamicScreenRegistry.getLatestScreen()?.let { it1 ->
100
- DynamicScreenRegistry.unregisterScreen(it1.id)
101
- }
108
+ dismissing?.let { registry.unregisterScreen(it.id) }
102
109
  } else {
103
110
  maxApi?.dismiss { }
104
111
  }
105
112
  }
106
113
 
107
114
  fun present(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
108
- val route = DynamicScreenRegistry.register(screenName, content, options)
115
+ val route = registry.register(screenName, content, options)
109
116
  navController.navigate(DynamicDialogRoute(route.id))
110
117
  }
111
118
 
112
119
  fun reset(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
113
- DynamicScreenRegistry.unregisterAll()
120
+ registry.unregisterAll()
114
121
 
115
- val route = DynamicScreenRegistry.register(screenName, content, options)
122
+ val route = registry.register(screenName, content, options)
116
123
  navController.navigate(DynamicScreenRoute(route.id)) {
117
124
  popUpTo(0) { inclusive = true }
118
125
  }
@@ -123,7 +130,7 @@ class Navigator(
123
130
  barrierDismissible: Boolean = true,
124
131
  onDismiss: (() -> Unit)? = null
125
132
  ){
126
- val id = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
133
+ val id = currentScreen()?.id ?: -1
127
134
  OverplayComponentRegistry.registerOverplay(id, content, OverplayComponentType.MODAL, false, barrierDismissible, onDismiss)
128
135
  }
129
136
 
@@ -134,12 +141,12 @@ class Navigator(
134
141
  onDismiss: (() -> Unit)? = null,
135
142
  bottomSheetHeader: BottomHeader? = null
136
143
  ){
137
- val id = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
144
+ val id = currentScreen()?.id ?: -1
138
145
  OverplayComponentRegistry.registerOverplay(id, content, OverplayComponentType.BOTTOM_SHEET, isSurface, barrierDismissible, onDismiss, bottomSheetHeader)
139
146
  }
140
147
 
141
148
  fun showSnackBar(snackBar: SnackBar, onDismiss: (() -> Unit)? = null) {
142
- val id = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
149
+ val id = currentScreen()?.id ?: -1
143
150
  scope.launch {
144
151
  OverplayComponentRegistry.registerOverplay(
145
152
  id = id,
@@ -182,12 +189,11 @@ data class DynamicScreen(
182
189
  var options: NavigationOptions? = null
183
190
  )
184
191
 
185
- object DynamicScreenRegistry {
192
+ class DynamicScreenRegistry {
186
193
  private val screens = mutableMapOf<Int, DynamicScreen>()
187
- private var idCounter = 1
188
194
 
189
195
  fun register(screenName: String, content: @Composable () -> Unit, options: NavigationOptions?): DynamicScreenRoute {
190
- val id = idCounter++
196
+ val id = nextGlobalId()
191
197
  screens[id] = DynamicScreen(
192
198
  id = id,
193
199
  name = screenName,
@@ -197,7 +203,7 @@ object DynamicScreenRegistry {
197
203
  return DynamicScreenRoute(id)
198
204
  }
199
205
 
200
- fun nextId(): Int = idCounter++
206
+ fun nextId(): Int = nextGlobalId()
201
207
 
202
208
  fun bind(id: Int, screenName: String, content: @Composable () -> Unit, options: NavigationOptions?) {
203
209
  screens[id] = DynamicScreen(
@@ -206,17 +212,13 @@ object DynamicScreenRegistry {
206
212
  content = content,
207
213
  options = options
208
214
  )
209
- if (id >= idCounter) idCounter = id + 1 // keep counter ahead after process-death restore
215
+ advanceGlobalIdPast(id) // keep counter ahead after process-death restore
210
216
  }
211
217
 
212
218
  fun unregisterScreen(id: Int) {
213
219
  screens.remove(id)
214
220
  }
215
221
 
216
- fun getLatestScreen(): DynamicScreen?{
217
- return screens.entries.lastOrNull()?.value
218
- }
219
-
220
222
  fun unregisterAll(){
221
223
  val ids = screens.keys.toList()
222
224
  for (id in ids) {
@@ -229,8 +231,20 @@ object DynamicScreenRegistry {
229
231
  fun setOptions(id: Int, options: NavigationOptions){
230
232
  screens[id]?.options = options
231
233
  }
234
+
235
+ companion object {
236
+ private var globalIdCounter = 1
237
+
238
+ private fun nextGlobalId(): Int = globalIdCounter++
239
+
240
+ private fun advanceGlobalIdPast(id: Int) {
241
+ if (id >= globalIdCounter) globalIdCounter = id + 1
242
+ }
243
+ }
232
244
  }
233
245
 
246
+ val LocalDynamicScreenRegistry = staticCompositionLocalOf<DynamicScreenRegistry?> { null }
247
+
234
248
 
235
249
  sealed class OverplayComponentParams {
236
250
  class Modal(
@@ -69,7 +69,7 @@ import vn.momo.kits.platform.supportsImePadding
69
69
  import vn.momo.kits.navigation.tracking.ScreenTracker
70
70
  import vn.momo.kits.navigation.tracking.ScreenTrackingState
71
71
  import kotlinx.coroutines.delay
72
- import vn.momo.kits.application.ApplicationContext
72
+ import vn.momo.kits.application.LocalContext
73
73
  import kotlin.time.Clock
74
74
  import kotlin.time.Duration.Companion.milliseconds
75
75
  import kotlin.time.ExperimentalTime
@@ -89,10 +89,11 @@ internal fun StackScreen(
89
89
  ) {
90
90
  val navigator = LocalNavigator.current
91
91
  val maxApi = LocalMaxApi.current
92
- val context = ApplicationContext.current
92
+ val context = LocalContext.current
93
93
  val statusBar = AppStatusBar.current
94
94
  val density = LocalDensity.current
95
- val navigation = remember { Navigation(id = id, bottomTabIndex = bottomTabIndex, initOptions = navigationOptions) }
95
+ val screenRegistry = LocalDynamicScreenRegistry.current
96
+ val navigation = remember { Navigation(id = id, bottomTabIndex = bottomTabIndex, initOptions = navigationOptions, registry = screenRegistry) }
96
97
 
97
98
  val options by navigation.currentOptions
98
99