@momo-kits/native-kits 0.162.3-fontscale.1-debug → 0.162.3-test.2-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.
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.162.3-fontscale.1-debug"
43
+ version = "0.162.3-test.2-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -28,7 +28,7 @@ data class MiniAppContext(
28
28
  val providerId: String = "",
29
29
  val permissions: List<Map<String, Any>>? = emptyList(),
30
30
  val features: FeatureFlags? = null,
31
- val scaleSizeMaxRate: Float? = null,
31
+ val fontScaleConfig: FontScaleConfig? = null,
32
32
  ) {
33
33
  companion object {
34
34
 
@@ -60,7 +60,7 @@ data class MiniAppContext(
60
60
  providerId = parent.providerId.ifBlank { child.providerId },
61
61
  permissions = if (!parent.permissions.isNullOrEmpty()) parent.permissions else child.permissions,
62
62
  features = mergeFeatureFlags(parent.features, child.features),
63
- scaleSizeMaxRate = parent.scaleSizeMaxRate ?: child.scaleSizeMaxRate,
63
+ fontScaleConfig = parent.fontScaleConfig ?: child.fontScaleConfig,
64
64
  )
65
65
  }
66
66
 
@@ -98,4 +98,11 @@ val LocalComponentInformation = staticCompositionLocalOf<ComponentInformation?>
98
98
  null
99
99
  }
100
100
 
101
- val ScaleSizeMaxRate = staticCompositionLocalOf<Float?> { null }
101
+ @Immutable
102
+ data class FontScaleConfig(
103
+ val useOSFontScale: Boolean = true,
104
+ val userScaleRate: Float? = null,
105
+ val osFontScale: Float? = null,
106
+ )
107
+
108
+ val LocalFontScaleConfig = staticCompositionLocalOf { FontScaleConfig() }
@@ -36,6 +36,7 @@ fun Text(
36
36
  softWrap: Boolean = true,
37
37
  accessibilityId: String? = null,
38
38
  inlineContent: Map<String, InlineTextContent> = mapOf(),
39
+ userScaleRate: Float? = null,
39
40
  ) {
40
41
  Text(
41
42
  text = AnnotatedString(text),
@@ -52,7 +53,8 @@ fun Text(
52
53
  letterSpacing = letterSpacing,
53
54
  softWrap = softWrap,
54
55
  accessibilityId = accessibilityId,
55
- inlineContent = inlineContent
56
+ inlineContent = inlineContent,
57
+ userScaleRate = userScaleRate
56
58
  )
57
59
  }
58
60
 
@@ -74,13 +76,14 @@ fun Text(
74
76
  softWrap: Boolean = true,
75
77
  accessibilityId: String? = null,
76
78
  inlineContent: Map<String, InlineTextContent> = mapOf(),
79
+ userScaleRate: Float? = null,
77
80
  ) {
78
81
  // Cache theme access to avoid repeated lookups
79
82
  val theme = AppTheme.current
80
83
 
81
84
  // Call @Composable functions directly in composable context
82
- val scaledFontSize = scaleSize(style.fontSize)
83
- val scaledLineHeight = scaleSize(style.lineHeight)
85
+ val scaledFontSize = scaleSize(style.fontSize, userScaleRate)
86
+ val scaledLineHeight = scaleSize(style.lineHeight, userScaleRate)
84
87
  val fontFamilyResult = getFontFamily(fontFamily ?: theme.font, style.fontWeight)
85
88
 
86
89
  // Now memoize the results
@@ -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
  )
@@ -42,6 +42,11 @@ fun NavigationContainer(
42
42
 
43
43
  val resolvedLocalize = localize ?: fallbackLocalize
44
44
 
45
+ // AI-GENERATED START: self-seed first-frame font scale by reading font_scale_config straight from async storage
46
+ var observerFontScaleConfig by remember(maxApi) {
47
+ mutableStateOf(seedFontScaleConfig(maxApi) ?: mergedContext?.fontScaleConfig)
48
+ }
49
+ // AI-GENERATED END: self-seed first-frame font scale by reading font_scale_config straight from async storage
45
50
  LaunchedEffect(maxApi, resolvedLocalize) {
46
51
  val api = maxApi ?: return@LaunchedEffect
47
52
  runCatching {
@@ -50,14 +55,8 @@ fun NavigationContainer(
50
55
  resolvedLocalize.changeLanguage(Localize.EN)
51
56
  }
52
57
  })
53
- }
54
- // TEMP: verify FontScale MaxApi end-to-end via dynamic dispatch — remove after test
55
- runCatching {
56
- api.maxDispatcher.dispatchAsync("getFontScale", arrayOf<Any?>()) { resp ->
57
- println("[FontScale][native-kit] getFontScale -> $resp")
58
- }
59
- api.maxDispatcher.dispatchAsync("setFontScale", arrayOf<Any?>(1.5f)) { resp ->
60
- println("[FontScale][native-kit] setFontScale -> $resp")
58
+ api.observer(FONT_SCALE_OBSERVER_KEY).collect { data ->
59
+ observerFontScaleConfig = parseFontScaleConfig(data)
61
60
  }
62
61
  }
63
62
  }
@@ -76,6 +75,7 @@ fun NavigationContainer(
76
75
  AppNavigationBar provides navigationBarHeight,
77
76
  LocalContext provides mergedContext,
78
77
  LocalLocalize provides resolvedLocalize,
78
+ LocalFontScaleConfig provides (observerFontScaleConfig ?: LocalFontScaleConfig.current),
79
79
  ) {
80
80
  LaunchedEffect(Unit) {
81
81
  setNavigator?.invoke(navigator)
@@ -155,9 +155,49 @@ val LocalMaxApi = staticCompositionLocalOf<IMaxApi?> {
155
155
  error("No MaxApi provided")
156
156
  }
157
157
 
158
+ private const val FONT_SCALE_OBSERVER_KEY = "font_scale_config"
159
+
158
160
  private fun parseLanguage(raw: Map<String, Any?>?): String? {
159
161
  val code = (raw?.get("response") as? String)?.trim()?.lowercase()
160
162
  if (code.isNullOrEmpty()) return null
161
163
  return if (code.startsWith(Localize.EN)) Localize.EN else Localize.VI
162
164
  }
163
165
 
166
+ // AI-GENERATED START: synchronous first-frame font scale seed read straight from the font_scale_config async-storage key
167
+ /**
168
+ * One-shot synchronous read of the persisted `font_scale_config` async-storage value for the kit's
169
+ * first frame (zero-flash), so the host no longer has to push it through [MiniAppContext]. Live
170
+ * changes still arrive via the `font_scale_config` observer below. Returns null on any failure so the
171
+ * caller falls back to the merged-context value, then the kit default.
172
+ */
173
+ private fun seedFontScaleConfig(maxApi: IMaxApi?): FontScaleConfig? {
174
+ val raw = runCatching { maxApi?.getItem(FONT_SCALE_OBSERVER_KEY) }.getOrNull()
175
+ return parseFontScaleConfig(raw)
176
+ }
177
+ // AI-GENERATED END: synchronous first-frame font scale seed read straight from the font_scale_config async-storage key
178
+
179
+ private fun parseFontScaleConfig(raw: Map<String, Any?>?): FontScaleConfig? {
180
+ if (raw.isNullOrEmpty()) return null
181
+ @Suppress("UNCHECKED_CAST")
182
+ val data = (raw["response"] as? Map<String, Any?>)
183
+ ?: (raw["data"] as? Map<String, Any?>)
184
+ ?: raw
185
+ if (!data.containsKey("useOSFontScale") && !data.containsKey("userScaleRate")) return null
186
+ val useOS = when (val v = data["useOSFontScale"]) {
187
+ is Boolean -> v
188
+ is String -> v.equals("true", ignoreCase = true)
189
+ is Number -> v.toInt() != 0
190
+ else -> true
191
+ }
192
+ val rate = when (val v = data["userScaleRate"]) {
193
+ is Number -> v.toFloat()
194
+ is String -> v.toFloatOrNull()
195
+ else -> null
196
+ }
197
+ val osScale = when (val v = data["osFontScale"]) {
198
+ is Number -> v.toFloat()
199
+ is String -> v.toFloatOrNull()
200
+ else -> null
201
+ }
202
+ return FontScaleConfig(useOSFontScale = useOS, userScaleRate = rate, osFontScale = osScale)
203
+ }
package/gradle.properties CHANGED
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.162.3-fontscale.1
21
+ version=0.162.3-test.2
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
@@ -46,5 +46,6 @@ public class ApplicationEnvironment: ObservableObject {
46
46
  self.applicationContext = applicationContext
47
47
  self.composeApi = composeApi
48
48
  self.config = config
49
+ FontScaleStore.shared.bind(composeApi)
49
50
  }
50
51
  }
@@ -0,0 +1,59 @@
1
+ import SwiftUI
2
+
3
+ public struct FontScaleConfig {
4
+ public var useOSFontScale: Bool
5
+ public var userScaleRate: CGFloat?
6
+
7
+ public init(useOSFontScale: Bool = true, userScaleRate: CGFloat? = nil) {
8
+ self.useOSFontScale = useOSFontScale
9
+ self.userScaleRate = userScaleRate
10
+ }
11
+ }
12
+
13
+ public final class FontScaleStore: ObservableObject {
14
+ public static let shared = FontScaleStore()
15
+
16
+ @Published public private(set) var config = FontScaleConfig()
17
+
18
+ private var didBind = false
19
+ private var observerId: String?
20
+
21
+ private init() {}
22
+
23
+ public func update(_ newConfig: FontScaleConfig) {
24
+ if Thread.isMainThread {
25
+ config = newConfig
26
+ } else {
27
+ DispatchQueue.main.async { [weak self] in self?.config = newConfig }
28
+ }
29
+ }
30
+
31
+ public func bind(_ api: KitComposeApi?) {
32
+ guard let api = api, !didBind else { return }
33
+ didBind = true
34
+
35
+ apply(api.request(funcName: "getFontScaleConfig", params: nil))
36
+ observerId = api.request(funcName: "observer", params: ["font_scale_config"]) { [weak self] response in
37
+ self?.apply(response)
38
+ }
39
+ }
40
+
41
+ private func apply(_ response: String) {
42
+ guard let parsed = FontScaleStore.parse(response) else { return }
43
+ update(parsed)
44
+ }
45
+
46
+ static func parse(_ response: String) -> FontScaleConfig? {
47
+ guard
48
+ let data = response.data(using: .utf8),
49
+ let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
50
+ else { return nil }
51
+
52
+ let obj = (json["response"] as? [String: Any]) ?? (json["data"] as? [String: Any]) ?? json
53
+ guard obj["useOSFontScale"] != nil || obj["userScaleRate"] != nil else { return nil }
54
+
55
+ let useOS = (obj["useOSFontScale"] as? Bool) ?? true
56
+ let rate = (obj["userScaleRate"] as? NSNumber).map { CGFloat($0.doubleValue) }
57
+ return FontScaleConfig(useOSFontScale: useOS, userScaleRate: rate)
58
+ }
59
+ }
@@ -1,27 +1,30 @@
1
1
  import SwiftUI
2
2
 
3
3
  public func scaleSize(_ size: CGFloat, _ scaleRate: CGFloat? = nil) -> CGFloat {
4
+ let config = FontScaleStore.shared.config
4
5
  let defaultScreenSize: CGFloat = 375
5
6
  let maxFontScale: CGFloat = scaleRate ?? 1.5
6
7
  let maxDeviceScale: CGFloat = 5
7
8
 
8
- let deviceWidth = UIScreen.main.bounds.width
9
- let deviceScale = deviceWidth / defaultScreenSize
9
+ if !config.useOSFontScale {
10
+ let customRate = config.userScaleRate ?? 1
11
+ return customRate > 1 ? size * customRate : size
12
+ }
13
+
14
+ let deviceScale = UIScreen.main.bounds.width / defaultScreenSize
10
15
 
11
16
  let defaultFont = UIFont.systemFont(ofSize: UIFont.labelFontSize)
12
17
  let scaledFont = UIFontMetrics.default.scaledFont(for: defaultFont)
13
- let fontScale = scaledFont.pointSize / defaultFont.pointSize
18
+ let osFontScale = scaledFont.pointSize / defaultFont.pointSize
14
19
 
15
20
  var fontSizeDeviceScale = size
16
21
  var fontSizeOSScale = size
17
22
 
18
-
19
23
  if deviceScale > 1 {
20
24
  fontSizeDeviceScale = min(fontSizeDeviceScale * deviceScale, fontSizeDeviceScale + maxDeviceScale)
21
25
  }
22
-
23
- if fontScale > 1 {
24
- fontSizeOSScale = min(fontSizeOSScale * fontScale, fontSizeOSScale * maxFontScale)
26
+ if osFontScale > 1 {
27
+ fontSizeOSScale = min(fontSizeOSScale * osFontScale, fontSizeOSScale * maxFontScale)
25
28
  }
26
29
 
27
30
  return max(fontSizeDeviceScale, fontSizeOSScale)
@@ -118,6 +121,7 @@ public struct MomoText: View {
118
121
  private let content: String
119
122
  private let typography: TypographyStyle
120
123
  private let color: Color
124
+ @ObservedObject private var fontScale = FontScaleStore.shared
121
125
 
122
126
  public init(
123
127
  _ content: String,
@@ -130,6 +134,7 @@ public struct MomoText: View {
130
134
  }
131
135
 
132
136
  public var body: some View {
137
+ _ = fontScale.config
133
138
  let text = SwiftUI.Text(content)
134
139
  .font(.system(size: scaleSize(typography.fontSize), weight: typography.fontWeight))
135
140
  .foregroundColor(color)
@@ -11,30 +11,35 @@ public extension View {
11
11
 
12
12
  public extension Font {
13
13
  static func appFont(size: CGFloat) -> Font {
14
+ let config = FontScaleStore.shared.config
14
15
  let defaultScreenSize: CGFloat = 375
15
16
  let maxFontScale: CGFloat = 1.5
16
17
  let maxDeviceScale: CGFloat = 5
17
18
 
18
- let deviceWidth = UIScreen.main.bounds.width
19
- let deviceScale = deviceWidth / defaultScreenSize
19
+ if !config.useOSFontScale {
20
+ let customRate = config.userScaleRate ?? 1
21
+ let scaled = customRate > 1 ? size * customRate : size
22
+ return Font.system(size: scaled)
23
+ }
24
+
25
+ let deviceScale = UIScreen.main.bounds.width / defaultScreenSize
20
26
 
21
27
  let defaultFont = UIFont.systemFont(ofSize: UIFont.labelFontSize)
22
28
  let scaledFont = UIFontMetrics.default.scaledFont(for: defaultFont)
23
- let fontScale = scaledFont.pointSize / defaultFont.pointSize
29
+ let osFontScale = scaledFont.pointSize / defaultFont.pointSize
24
30
 
25
31
  var fontSize = size
26
32
 
27
33
  if deviceScale > 1 {
28
34
  fontSize = min(fontSize * deviceScale, fontSize + maxDeviceScale)
29
35
  }
30
-
31
- if fontScale > 1 {
32
- fontSize = min(fontSize * fontScale, fontSize * maxFontScale)
36
+ if osFontScale > 1 {
37
+ fontSize = min(fontSize * osFontScale, fontSize * maxFontScale)
33
38
  }
34
39
 
35
40
  return Font.system(size: fontSize)
36
41
  }
37
-
42
+
38
43
  // New supported typography styles
39
44
  static let headline_default_bold = appFont(size: 24).weight(.bold)
40
45
  static let header_m_bold = appFont(size: 18).weight(.bold)
@@ -52,7 +57,7 @@ public extension Font {
52
57
  static let action_s_bold = appFont(size: 14).weight(.bold)
53
58
  static let action_xs_bold = appFont(size: 12).weight(.bold)
54
59
  static let action_xxs_bold = appFont(size: 10).weight(.bold)
55
-
60
+
56
61
  // Legacy styles
57
62
  static let headline_default = appFont(size: 28).weight(.semibold)
58
63
  static let headline_s = appFont(size: 24).weight(.semibold)
@@ -77,7 +82,7 @@ public extension Font {
77
82
  static let action_xxs = appFont(size: 10).weight(.bold)
78
83
  static let action_xs = appFont(size: 12).weight(.bold)
79
84
  static let action_s = appFont(size: 15).weight(.bold)
80
-
85
+
81
86
  // deprecated
82
87
  static let h1 = appFont(size: 36).weight(.semibold) //headline_xl
83
88
  static let h2 = appFont(size: 32).weight(.semibold)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.162.3-fontscale.1-debug",
3
+ "version": "0.162.3-test.2-debug",
4
4
  "private": false,
5
5
  "dependencies": {},
6
6
  "devDependencies": {},
@@ -1,17 +0,0 @@
1
- package vn.momo.kits.components
2
-
3
- import androidx.compose.runtime.Composable
4
- import androidx.compose.runtime.CompositionLocalProvider
5
- import vn.momo.kits.application.ScaleSizeMaxRate
6
-
7
- @Composable
8
- fun ScaleSizeScope(
9
- scaleSizeMaxRate: Float? = null,
10
- content: @Composable () -> Unit
11
- ) {
12
- CompositionLocalProvider(
13
- ScaleSizeMaxRate provides scaleSizeMaxRate,
14
- ) {
15
- content()
16
- }
17
- }