@momo-kits/native-kits 0.167.1-beta.11-debug → 0.167.1-beta.13-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 (31) hide show
  1. package/CLAUDE.md +6 -4
  2. package/compose/build.gradle.kts +1 -1
  3. package/compose/compose.podspec +1 -1
  4. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -19
  5. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +2 -2
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +1 -1
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +1 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +1 -1
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +38 -4
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +10 -13
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +3 -4
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +6 -2
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  15. package/gradle.properties +1 -1
  16. package/ios/Application/ApplicationEnvironment.swift +11 -10
  17. package/ios/Application/DesignSystemKits.swift +100 -0
  18. package/ios/Application/DesignSystemKitsPayload.swift +68 -0
  19. package/ios/Application/Navigation/NavigationContainer.swift +0 -4
  20. package/ios/Colors+Radius+Spacing/Theme.swift +2 -2
  21. package/ios/Template/TrustBanner/TrustBanner.swift +39 -7
  22. package/ios/Typography/FontScale.swift +14 -11
  23. package/ios/Typography/Text.swift +20 -6
  24. package/ios/Typography/Typography.swift +2 -2
  25. package/ios/native-kits.podspec +1 -1
  26. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  27. package/package.json +1 -1
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  30. package/ios/Typography/FontScaleStore.swift +0 -58
  31. package/ios/Typography/KitDesignSystem.swift +0 -17
package/CLAUDE.md CHANGED
@@ -41,7 +41,9 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
41
41
 
42
42
  - **Entry point:** wrap content in `NavigationContainer(...)` from `navigation/`. It builds the `Navigator`, takes a host-supplied `Localize` (falls back to an empty one), and provides the CompositionLocals below. **Do not use** `ApplicationContainer` (`application/NavigationContainer.kt`) — deprecated and does **not** wire `LocalLocalize` / `LocalNavigator` / `LocalMaxApi`.
43
43
  - **Configuration flows via CompositionLocals**, not params. Public ones: `AppTheme`, `AppStatusBar`, `AppNavigationBar` (`const/Theme.kt`); `ApplicationContext`, `AppConfig`, `AppLanguage`, `LocalLocalize` (`application/`); `LocalNavigator`, `LocalMaxApi` (`navigation/`). Read with `X.current`.
44
- - **Design-system config** (`trustBanner`, `headerBar`, `headerGradient`): the host seeds `KitDesignSystemConfig.update(...)` once (same pattern as `KitFontScale`); `NavigationContainer` provides it as `LocalDesignSystemConfig` (param `designSystemConfig` → parent → global) and applies `headerBar` as the header image when the theme has none.
44
+ - **`DesignSystemKits`** (`application/DesignSystemKits.kt`) is the single app-wide holder for the host-owned design-system inputs: font scale (`userScaleRate` / `useOSScaleRate`), whitelist (`isWhiteList`) and the remote `DesignSystemConfig`, bundled as `DesignSystemKitsConfigs`. The host calls `DesignSystemKits.init(source)` once at startup with its `DesignSystemKitsSource` (sync `getConfigs()` seed + `configsFlow`); `init` is idempotent. Without a source, call `setConfigs(...)` directly. Read with `getConfigs()` (snapshot state, recomposes); non-Compose observers use `configsFlow`.
45
+ - **SwiftUI twin:** `ios/Application/DesignSystemKits.swift` mirrors it field for field (`DesignSystemKitsConfigs` with `designSystemConfig: KitConfig?`, `setConfigs` / `getConfigs`), plus the OS Dynamic Type factor (`osFontScale`). There is no `init(source:)` (Swift reserves `init`): the host observes its own bridge and pushes. A host that can't share Swift types (the KMP bridge) calls `DesignSystemKits.shared.setConfigs(json:)` (`ios/Application/DesignSystemKitsPayload.swift`); the JSON field names are a contract with momo-app's serializer (`HostDesignSystemKits.observeConfigsJson`, payload classes in `DesignSystemData.kt`) — change both together. Invalid JSON is ignored. The SwiftUI kit can't link the Kotlin `kits` framework (a second Kotlin/Native runtime would hold a second, never-initialized singleton), so the render-time logic — `FontScale` formula, Dynamic Type observation — is intentionally duplicated in Swift; only values flow across.
46
+ - **Design-system config** (`trustBanner`, `headerBar`, `headerGradient`): `NavigationContainer` provides it as `LocalDesignSystemConfig` (param `designSystemConfig` → parent → `DesignSystemKits`) and applies `headerBar` as the header image when the theme has none.
45
47
  - **Platform code** goes through `expect`/`actual` in `platform/` — never reference Android/iOS APIs from `commonMain` directly.
46
48
  - **Native bridge:** `IMaxApi` (`vn.momo.maxapi`, exposed as `LocalMaxApi`) is the host-app bridge (dismiss, tracking, device info, language).
47
49
  - Navigation uses a runtime `DynamicScreenRegistry`; overlays (modal/bottom-sheet/snackbar) go through `Navigator` + `OverplayComponentRegistry`.
@@ -69,9 +71,9 @@ The SwiftUI port differs from Compose in one important way: **it follows the hos
69
71
 
70
72
  One formula, both platforms: the device-width scale (`width / 375`, capped at `+5pt`) and the OS Dynamic Type scale (capped at `1.5×`) are computed independently and the **larger wins**. When `useOSScaleRate` is false neither applies — the size is just multiplied by `userScaleRate`. Max font scale is fixed at 1.5; the old per-call/per-scope override is deprecated and ignored on both sides.
71
73
 
72
- - **Compose** — `const/Typography.kt` (`scaleSize` overloads for `Float`/`Dp`/`TextUnit`/`TextStyle`). The `TextUnit` overload divides by `density.fontScale` so Compose's own `sp` scaling isn't applied twice. Inputs: `LocalContext.current` (`MiniAppContext.userScaleRate` / `useOSScaleRate`, per-field) falling back to the `KitFontScale` global. On iOS, `platform/OsFontScale.ios.kt` observes `UIContentSizeCategoryDidChangeNotification` and overrides `LocalDensity` — Compose MP otherwise reads `fontScale` once at `ComposeUIViewController` creation. Wired at `NavigationContainer`.
73
- - **SwiftUI** — `ios/Typography/FontScale.swift`. `FontScale` is an immutable snapshot with `callAsFunction`, so a view reads `@Environment(\.momoFontScale) private var fontScale` and calls `fontScale(16)`. `FontScaleStore.shared` is the `ObservableObject` holding both inputs and the OS factor (it observes `UIContentSizeCategory.didChangeNotification`); the host pushes its preference with `update(userScaleRate:useOSScaleRate:)`.
74
- - **Never set `\.momoFontScale` directly** — same trap as `\.localize`. Inject via `.momoFontScale(_:context:)`, which holds the store as `@ObservedObject`; that is what re-publishes the snapshot. `NavigationContainer` does it (passing `MiniAppContext` so the per-mini-app override applies); a bare `UIHostingController` root must too. The environment default reads the live store, so an un-injected subtree still gets correct values — just not live ones.
74
+ - **Compose** — `const/Typography.kt` (`scaleSize` overloads for `Float`/`Dp`/`TextUnit`/`TextStyle`). The `TextUnit` overload divides by `density.fontScale` so Compose's own `sp` scaling isn't applied twice. Inputs: `LocalContext.current` (`MiniAppContext.userScaleRate` / `useOSScaleRate`, per-field) falling back to `DesignSystemKits.getConfigs()`. On iOS, `platform/OsFontScale.ios.kt` observes `UIContentSizeCategoryDidChangeNotification` and overrides `LocalDensity` — Compose MP otherwise reads `fontScale` once at `ComposeUIViewController` creation. Wired at `NavigationContainer`.
75
+ - **SwiftUI** — `ios/Typography/FontScale.swift`. `FontScale` is an immutable snapshot with `callAsFunction`, so a view reads `@Environment(\.momoFontScale) private var fontScale` and calls `fontScale(16)`. `DesignSystemKits.shared` is the `ObservableObject` holding the host inputs and the OS factor (it observes `UIContentSizeCategory.didChangeNotification`); the host pushes them with `setConfigs(_:)`.
76
+ - **Never set `\.momoFontScale` directly** — same trap as `\.localize`. Inject via `.momoFontScale(_:context:)`, which holds `DesignSystemKits` as `@ObservedObject`; that is what re-publishes the snapshot. It also injects `\.designSystemKitsConfigs`, which `MomoText` (whitelist font) and `TrustBanner` (fallback after `ApplicationEnvironment.config`) read so a host push re-renders them. `NavigationContainer` does it (passing `MiniAppContext` so the per-mini-app override applies); a bare `UIHostingController` root must too. The environment default reads the live store, so an un-injected subtree still gets correct values — just not live ones.
75
77
  - The global `scaleSize(_:)` and `Font.appFont(size:)` remain for call sites that can't read the environment (UIKit bridges, free functions). They are **not** reactive. `Font.header_default_bold` & friends are computed `static var`s for the same reason — as stored `static let`s they froze the scale at first access. `UIViewRepresentable`s should read `context.environment.momoFontScale` instead, and re-apply the font in `updateUIView`, not only in `makeUIView`.
76
78
 
77
79
  ## Code conventions
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.167.1-beta.11-debug"
43
+ version = "0.167.1-beta.13-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |spec|
2
2
  spec.name = 'compose'
3
- spec.version = '0.167.1-beta.4-debug'
3
+ spec.version = '0.167.1-beta.9-debug'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -1,13 +1,8 @@
1
1
  package vn.momo.kits.application
2
2
 
3
3
  import androidx.compose.runtime.Immutable
4
- import androidx.compose.runtime.getValue
5
- import androidx.compose.runtime.mutableStateOf
6
- import androidx.compose.runtime.setValue
7
4
  import androidx.compose.runtime.staticCompositionLocalOf
8
5
  import vn.momo.kits.components.TrustBannerData
9
- import vn.momo.kits.const.KitDesignSystem
10
- import vn.momo.kits.const.Theme
11
6
 
12
7
  @Immutable
13
8
  data class FeatureFlags(
@@ -21,20 +16,6 @@ data class DesignSystemConfig(
21
16
  val headerGradient: String? = null,
22
17
  )
23
18
 
24
- // App-wide holder seeded by the host (same pattern as KitFontScale), so direct NavigationContainer
25
- // callers get the config without passing it. Snapshot state: a config fetched after first frame recomposes.
26
- object KitDesignSystemConfig {
27
- var config by mutableStateOf<DesignSystemConfig?>(null)
28
- private set
29
-
30
- fun update(config: DesignSystemConfig?) {
31
- this.config = config
32
- }
33
- }
34
-
35
- internal fun Theme.withDesignSystemFont(): Theme =
36
- copy(font = if (KitDesignSystem.isWhiteList) "MomoTrustSans" else "SFProText")
37
-
38
19
  @Immutable
39
20
  data class MiniAppContext(
40
21
  val appIcon: String = "",
@@ -0,0 +1,62 @@
1
+ package vn.momo.kits.application
2
+
3
+ import androidx.compose.runtime.Immutable
4
+ import androidx.compose.runtime.getValue
5
+ import androidx.compose.runtime.mutableStateOf
6
+ import androidx.compose.runtime.setValue
7
+ import kotlinx.coroutines.CoroutineScope
8
+ import kotlinx.coroutines.Dispatchers
9
+ import kotlinx.coroutines.Job
10
+ import kotlinx.coroutines.SupervisorJob
11
+ import kotlinx.coroutines.flow.Flow
12
+ import kotlinx.coroutines.flow.MutableStateFlow
13
+ import kotlinx.coroutines.flow.StateFlow
14
+ import kotlinx.coroutines.flow.asStateFlow
15
+ import kotlinx.coroutines.launch
16
+
17
+ @Immutable
18
+ data class DesignSystemKitsConfigs(
19
+ val userScaleRate: Float = 1f,
20
+ val useOSScaleRate: Boolean = true,
21
+ val isWhiteList: Boolean = false,
22
+ val designSystemConfig: DesignSystemConfig? = null,
23
+ )
24
+
25
+ // The kit is sandboxed from the host's storage/config, so the host implements this and hands it to init().
26
+ interface DesignSystemKitsSource {
27
+ // Sync read: seeds the kit before the first frame, so there is no default-then-real flicker.
28
+ fun getConfigs(): DesignSystemKitsConfigs
29
+
30
+ val configsFlow: Flow<DesignSystemKitsConfigs>
31
+ }
32
+
33
+ // App-wide design-system inputs (font scale, whitelist, remote config). scaleSize(), Theme.font and
34
+ // NavigationContainer fall back to these when no MiniAppContext / LocalDesignSystemConfig is provided.
35
+ object DesignSystemKits {
36
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
37
+ private var observeJob: Job? = null
38
+
39
+ // Snapshot state so Compose readers recompose (including the non-composable Theme.font getter);
40
+ // the StateFlow is for non-Compose observers, e.g. the host's SwiftUI bridge, where Compose's
41
+ // global snapshot apply loop may never have started.
42
+ private var currentConfigs by mutableStateOf(DesignSystemKitsConfigs())
43
+ private val mutableConfigsFlow = MutableStateFlow(currentConfigs)
44
+ val configsFlow: StateFlow<DesignSystemKitsConfigs> = mutableConfigsFlow.asStateFlow()
45
+
46
+ // Idempotent: a repeated call (e.g. Activity re-creation) replaces the previous observer.
47
+ fun init(source: DesignSystemKitsSource) {
48
+ observeJob?.cancel()
49
+ setConfigs(source.getConfigs())
50
+ observeJob = scope.launch {
51
+ source.configsFlow.collect { setConfigs(it) }
52
+ }
53
+ }
54
+
55
+ // While init() is observing a source, its next emission overrides a value set here.
56
+ fun setConfigs(configs: DesignSystemKitsConfigs) {
57
+ currentConfigs = configs
58
+ mutableConfigsFlow.value = configs
59
+ }
60
+
61
+ fun getConfigs(): DesignSystemKitsConfigs = currentConfigs
62
+ }
@@ -38,13 +38,13 @@ import vn.momo.kits.components.Icon
38
38
  import vn.momo.kits.components.Text
39
39
  import vn.momo.kits.const.AppTheme
40
40
  import vn.momo.kits.const.Colors
41
- import vn.momo.kits.const.KitDesignSystem
42
41
  import vn.momo.kits.const.Spacing
43
42
  import vn.momo.kits.const.Typography
44
43
  import vn.momo.kits.const.getFontFamily
45
44
  import vn.momo.kits.modifier.kitsAutomationId
46
45
  import vn.momo.kits.modifier.noFeedbackClickable
47
46
  import vn.momo.kits.modifier.setAutomationId
47
+ import vn.momo.kits.navigation.component.headerFontFamily
48
48
  import vn.momo.kits.navigation.component.HEADER_HEIGHT
49
49
  import vn.momo.kits.utils.getAppStatusBarHeight
50
50
  import kotlin.math.max
@@ -362,7 +362,7 @@ private fun LiteScreenLayout(
362
362
  text = title,
363
363
  color = headerColor.tintIconColor,
364
364
  style = titleStyle,
365
- fontFamily = if (KitDesignSystem.isWhiteList) "MoMoTrustDisplay" else "SFProText",
365
+ fontFamily = headerFontFamily,
366
366
  modifier = titleModifier,
367
367
  maxLines = 1,
368
368
  overflow = TextOverflow.Ellipsis
@@ -96,7 +96,7 @@ fun ApplicationContainer(
96
96
 
97
97
  ProvideOsFontScale {
98
98
  CompositionLocalProvider(
99
- AppTheme provides appTheme.withDesignSystemFont(),
99
+ AppTheme provides appTheme,
100
100
  PlatformApi provides composeApi,
101
101
  AppStatusBar provides appStatusBarHeight,
102
102
  LocalContext provides applicationContext,
@@ -23,7 +23,7 @@ fun WidgetContainer(
23
23
  val mergedContext = remember(parentContext, miniAppContext) {
24
24
  MiniAppContext.merge(parentContext, miniAppContext)
25
25
  }
26
- val resolvedTheme = (theme ?: AppTheme.current).withDesignSystemFont()
26
+ val resolvedTheme = theme ?: AppTheme.current
27
27
  val resolvedLocalize = localize ?: LocalLocalize.current
28
28
  val resolvedMaxApi = maxApi ?: LocalMaxApi.current
29
29
 
@@ -266,7 +266,7 @@ fun Input(
266
266
  )
267
267
  }
268
268
 
269
- val passwordFontFamily = getFontFamily("SFProText", FontWeight.Normal)
269
+ val passwordFontFamily = getFontFamily(AppTheme.current.font, FontWeight.Normal)
270
270
  val passwordFontSize = scaleSize(24.sp)
271
271
  val visualTransformation = remember(secureTextEntry, inputState.passHidden, passwordFontFamily) {
272
272
  if (secureTextEntry && !inputState.passHidden)
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
4
4
  import androidx.compose.runtime.staticCompositionLocalOf
5
5
  import androidx.compose.ui.graphics.Color
6
6
  import androidx.compose.ui.unit.dp
7
+ import vn.momo.kits.application.DesignSystemKits
7
8
 
8
9
  @Stable
9
10
  data class Background(
@@ -56,16 +57,49 @@ data class ThemeAssets(
56
57
  val headerBackground: String? = null
57
58
  )
58
59
 
60
+ internal const val LEGACY_DEFAULT_FONT_FAMILY = "SFProText"
61
+ private const val NEW_DEFAULT_FONT_FAMILY = "MomoTrustSans"
62
+
59
63
  @Stable
60
- data class Theme(
64
+ class Theme(
61
65
  val dark: Boolean,
62
66
  val colors: ColorScheme,
63
- val font: String,
67
+ font: String,
64
68
  val assets: ThemeAssets
65
69
  ) {
70
+ private val designSystemFont = font
71
+
72
+ val font: String
73
+ get() = if (DesignSystemKits.getConfigs().isWhiteList) NEW_DEFAULT_FONT_FAMILY else designSystemFont
74
+
66
75
  fun isHeaderImage(): Boolean {
67
76
  return !assets.headerBackground.isNullOrEmpty()
68
77
  }
78
+
79
+ fun copy(
80
+ dark: Boolean = this.dark,
81
+ colors: ColorScheme = this.colors,
82
+ font: String = designSystemFont,
83
+ assets: ThemeAssets = this.assets
84
+ ): Theme = Theme(dark, colors, font, assets)
85
+
86
+ override fun equals(other: Any?): Boolean {
87
+ if (this === other) return true
88
+ if (other !is Theme) return false
89
+ return dark == other.dark && colors == other.colors &&
90
+ designSystemFont == other.designSystemFont && assets == other.assets
91
+ }
92
+
93
+ override fun hashCode(): Int {
94
+ var result = dark.hashCode()
95
+ result = 31 * result + colors.hashCode()
96
+ result = 31 * result + designSystemFont.hashCode()
97
+ result = 31 * result + assets.hashCode()
98
+ return result
99
+ }
100
+
101
+ override fun toString(): String =
102
+ "Theme(dark=$dark, colors=$colors, font=$designSystemFont, assets=$assets)"
69
103
  }
70
104
  val defaultTheme = Theme(
71
105
  dark = false,
@@ -117,7 +151,7 @@ val defaultTheme = Theme(
117
151
  container = Colors.blue_08
118
152
  )
119
153
  ),
120
- font = "SFProText",
154
+ font = LEGACY_DEFAULT_FONT_FAMILY,
121
155
  assets = ThemeAssets(
122
156
  headerBackground = null
123
157
  )
@@ -174,7 +208,7 @@ val darkTheme = Theme(
174
208
  container = Colors.blue_08
175
209
  )
176
210
  ),
177
- font = "SFProText",
211
+ font = LEGACY_DEFAULT_FONT_FAMILY,
178
212
  assets = ThemeAssets(
179
213
  headerBackground = null
180
214
  )
@@ -8,6 +8,7 @@ import androidx.compose.ui.text.font.FontFamily
8
8
  import androidx.compose.ui.text.font.FontWeight
9
9
  import androidx.compose.ui.text.style.TextDecoration
10
10
  import androidx.compose.ui.unit.*
11
+ import vn.momo.kits.application.DesignSystemKits
11
12
  import vn.momo.kits.application.LocalContext
12
13
  import vn.momo.kits.platform.getScreenDimensions
13
14
  import kotlin.math.max
@@ -20,10 +21,9 @@ const val MAX_DEVICE_SCALE = 5
20
21
  @Composable
21
22
  fun scaleSize(size: Float): Float {
22
23
  val context = LocalContext.current
23
- // Host-pushed context wins; otherwise fall back to the app-wide KitFontScale holder.
24
- val useOsScale = context?.useOSScaleRate ?: KitFontScale.useOSScaleRate
24
+ val useOsScale = context?.useOSScaleRate ?: DesignSystemKits.getConfigs().useOSScaleRate
25
25
  if (!useOsScale) {
26
- return size * (context?.userScaleRate ?: KitFontScale.userScaleRate)
26
+ return size * (context?.userScaleRate ?: DesignSystemKits.getConfigs().userScaleRate)
27
27
  }
28
28
  val scaleSizeMaxRate: Float = MAX_FONT_SCALE
29
29
  val deviceWidth = getScreenDimensions().width
@@ -83,19 +83,14 @@ fun scaleSize(textStyle: TextStyle): TextStyle {
83
83
  }
84
84
 
85
85
 
86
- /**
87
- * Tên file font trong tài nguyên native của app host (xem [hostFontFamily]).
88
- *
89
- * SDK không đóng gói font riêng nữa: app host đã bundle sẵn toàn bộ font từ `font-source`
90
- * cho cả React Native lẫn native, nên bản copy trong `composeResources` chỉ làm phình app.
91
- */
92
- private const val DEFAULT_FONT_FILE = "SFProText-Regular.ttf"
86
+ private const val LEGACY_DEFAULT_FONT_FILE = "SFProText-Regular.ttf"
87
+ private const val NEW_DEFAULT_FONT_FILE = "MomoTrustSans-Regular.ttf"
93
88
 
94
89
  private val hostFontFiles: Map<String, String> = mapOf(
95
90
  "SFProText-100" to "SFProText-Thin.ttf",
96
91
  "SFProText-200" to "SFProText-Ultralight.ttf",
97
92
  "SFProText-300" to "SFProText-Light.ttf",
98
- "SFProText-400" to DEFAULT_FONT_FILE,
93
+ "SFProText-400" to LEGACY_DEFAULT_FONT_FILE,
99
94
  "SFProText-500" to "SFProText-Medium.ttf",
100
95
  "SFProText-600" to "SFProText-Semibold.ttf",
101
96
  "SFProText-700" to "SFProText-Bold.ttf",
@@ -121,10 +116,12 @@ private val hostFontFiles: Map<String, String> = mapOf(
121
116
 
122
117
  @Composable
123
118
  fun getFontFamily(fontFamily: String, fontWeight: FontWeight? = FontWeight.Normal): FontFamily {
119
+ val isWhiteList = DesignSystemKits.getConfigs().isWhiteList
120
+ val defaultFile = if (isWhiteList) NEW_DEFAULT_FONT_FILE else LEGACY_DEFAULT_FONT_FILE
124
121
  val key = "$fontFamily-${fontWeight?.weight}"
125
- val fileName = hostFontFiles[key] ?: DEFAULT_FONT_FILE
122
+ val fileName = hostFontFiles[key] ?: defaultFile
126
123
  return hostFontFamily(fileName)
127
- ?: hostFontFamily(DEFAULT_FONT_FILE)
124
+ ?: hostFontFamily(defaultFile)
128
125
  ?: FontFamily.Default
129
126
  }
130
127
 
@@ -20,13 +20,12 @@ import androidx.navigation.toRoute
20
20
  import kotlinx.coroutines.delay
21
21
  import kotlinx.coroutines.flow.collectLatest
22
22
  import vn.momo.kits.application.DesignSystemConfig
23
- import vn.momo.kits.application.KitDesignSystemConfig
23
+ import vn.momo.kits.application.DesignSystemKits
24
24
  import vn.momo.kits.application.LocalContext
25
25
  import vn.momo.kits.application.LocalDesignSystemConfig
26
26
  import vn.momo.kits.application.LocalLocalize
27
27
  import vn.momo.kits.application.Localize
28
28
  import vn.momo.kits.application.MiniAppContext
29
- import vn.momo.kits.application.withDesignSystemFont
30
29
  import vn.momo.kits.const.AppNavigationBar
31
30
  import vn.momo.kits.const.AppStatusBar
32
31
  import vn.momo.kits.const.AppTheme
@@ -64,7 +63,7 @@ fun NavigationContainer(
64
63
  val stackTransition = remember { StackTransitionCoordinator() }
65
64
  val theme = remember { mutableStateOf(initialTheme) }
66
65
  val resolvedDesignSystemConfig =
67
- designSystemConfig ?: LocalDesignSystemConfig.current ?: KitDesignSystemConfig.config
66
+ designSystemConfig ?: LocalDesignSystemConfig.current ?: DesignSystemKits.getConfigs().designSystemConfig
68
67
  val headerBar = resolvedDesignSystemConfig?.headerBar
69
68
  // RN parity: the theme's own header image wins; config headerBar is only the fallback.
70
69
  val resolvedTheme = remember(theme.value, headerBar) {
@@ -99,7 +98,7 @@ fun NavigationContainer(
99
98
  LocalNavigator provides navigator,
100
99
  LocalDynamicScreenRegistry provides registry,
101
100
  LocalMaxApi provides maxApi,
102
- AppTheme provides resolvedTheme.withDesignSystemFont(),
101
+ AppTheme provides resolvedTheme,
103
102
  AppStatusBar provides statusBarHeight,
104
103
  AppNavigationBar provides navigationBarHeight,
105
104
  LocalContext provides mergedContext,
@@ -8,11 +8,15 @@ import androidx.compose.ui.text.style.TextAlign
8
8
  import androidx.compose.ui.text.style.TextOverflow
9
9
  import androidx.compose.ui.unit.sp
10
10
  import androidx.compose.ui.zIndex
11
+ import vn.momo.kits.application.DesignSystemKits
11
12
  import vn.momo.kits.components.Text
12
- import vn.momo.kits.const.KitDesignSystem
13
+ import vn.momo.kits.const.LEGACY_DEFAULT_FONT_FAMILY
13
14
  import vn.momo.kits.const.Typography
14
15
  import vn.momo.kits.modifier.setAutomationId
15
16
 
17
+ internal val headerFontFamily: String
18
+ get() = if (DesignSystemKits.getConfigs().isWhiteList) "MoMoTrustDisplay" else LEGACY_DEFAULT_FONT_FAMILY
19
+
16
20
  @Composable
17
21
  internal fun HeaderTitle(
18
22
  title: String = "",
@@ -28,7 +32,7 @@ internal fun HeaderTitle(
28
32
  fontSize = 15.sp,
29
33
  lineHeight = 22.sp,
30
34
  ),
31
- fontFamily = if (KitDesignSystem.isWhiteList) "MoMoTrustDisplay" else "SFProText",
35
+ fontFamily = headerFontFamily,
32
36
  color = color,
33
37
  maxLines = 1,
34
38
  overflow = TextOverflow.Ellipsis
@@ -27,7 +27,6 @@ import vn.momo.kits.components.Skeleton
27
27
  import vn.momo.kits.components.Text
28
28
  import vn.momo.kits.const.AppTheme
29
29
  import vn.momo.kits.const.Colors
30
- import vn.momo.kits.const.KitDesignSystem
31
30
  import vn.momo.kits.const.Spacing
32
31
  import vn.momo.kits.const.Typography
33
32
  import vn.momo.kits.const.getFontFamily
@@ -80,7 +79,7 @@ internal fun HeaderUser(
80
79
  text = data.title,
81
80
  color = data.tintColor,
82
81
  style = Typography.actionXsBold,
83
- fontFamily = if (KitDesignSystem.isWhiteList) "MoMoTrustDisplay" else "SFProText",
82
+ fontFamily = headerFontFamily,
84
83
  maxLines = 1,
85
84
  overflow = TextOverflow.Ellipsis
86
85
  )
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.167.1-beta.4
21
+ version=0.167.1-beta.9
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
@@ -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 FontScaleStore. Mirrors Compose's
21
- // MiniAppContext.userScaleRate / useOSScaleRate, which win per field over KitFontScale.
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,15 +37,16 @@ public class MiniAppContext {
37
37
  }
38
38
  }
39
39
 
40
- public class KitConfig {
41
- public var trustBanner: TrustBannerData? = nil
42
- public var headerBar: String? = nil
43
- public var headerGradient: String? = nil
44
- }
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
45
 
46
- extension Theme {
47
- func withDesignSystemFont() -> Theme {
48
- copy(font: KitDesignSystem.shared.isWhiteList ? "MomoTrustSans" : "SFProText")
46
+ public init(trustBanner: TrustBannerData? = nil, headerBar: String? = nil, headerGradient: String? = nil) {
47
+ self.trustBanner = trustBanner
48
+ self.headerBar = headerBar
49
+ self.headerGradient = headerGradient
49
50
  }
50
51
  }
51
52
 
@@ -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
+ sslImage: sslImage,
62
+ urlConfig: urlConfig,
63
+ icons: icons,
64
+ momoImage: momoImage
65
+ )
66
+ }
67
+ }
68
+ // AI-GENERATED END: JSON entry point for hosts that can't share Swift types with the kit
@@ -15,7 +15,6 @@ public struct NavigationContainer<Initial: View>: View {
15
15
  @StateObject private var navigator: Navigator
16
16
  @StateObject private var applicationEnvironment: ApplicationEnvironment
17
17
  @StateObject private var localize: Localize
18
- @Environment(\.appTheme) private var theme
19
18
 
20
19
  private let setNavigator: ((Navigator) -> Void)?
21
20
  private let composeApi: KitComposeApi?
@@ -65,7 +64,6 @@ public struct NavigationContainer<Initial: View>: View {
65
64
  .environmentObject(navigator)
66
65
  .environmentObject(applicationEnvironment)
67
66
  .environment(\.applicationEnvironment, applicationEnvironment)
68
- .environment(\.appTheme, theme.withDesignSystemFont())
69
67
  // AI-GENERATED START: route Localize + font-scale injection through the observing modifiers
70
68
  .momoLocalize(localize)
71
69
  .momoFontScale(context: applicationEnvironment.applicationContext)
@@ -75,7 +73,6 @@ public struct NavigationContainer<Initial: View>: View {
75
73
  .environmentObject(navigator)
76
74
  .environmentObject(applicationEnvironment)
77
75
  .environment(\.applicationEnvironment, applicationEnvironment)
78
- .environment(\.appTheme, theme.withDesignSystemFont())
79
76
  // AI-GENERATED START: route Localize + font-scale injection through the observing modifiers
80
77
  .momoLocalize(localize)
81
78
  .momoFontScale(context: applicationEnvironment.applicationContext)
@@ -87,7 +84,6 @@ public struct NavigationContainer<Initial: View>: View {
87
84
  .environmentObject(navigator)
88
85
  .environmentObject(applicationEnvironment)
89
86
  .environment(\.applicationEnvironment, applicationEnvironment)
90
- .environment(\.appTheme, theme.withDesignSystemFont())
91
87
  // AI-GENERATED START: route Localize + font-scale injection through the observing modifiers
92
88
  .momoLocalize(localize)
93
89
  .momoFontScale(context: applicationEnvironment.applicationContext)
@@ -221,7 +221,7 @@ public let defaultTheme = Theme(
221
221
  container: Colors.blue08
222
222
  )
223
223
  ),
224
- font: "SFProText",
224
+ font: "MomoTrustSans",
225
225
  assets: ThemeAssets(headerBackground: nil)
226
226
  )
227
227
 
@@ -277,7 +277,7 @@ public let darkTheme = Theme(
277
277
  container: Colors.blue08
278
278
  )
279
279
  ),
280
- font: "SFProText",
280
+ font: "MomoTrustSans",
281
281
  assets: ThemeAssets(headerBackground: nil)
282
282
  )
283
283
 
@@ -11,7 +11,7 @@ import SDWebImageSwiftUI
11
11
 
12
12
  let jsonUrl = "https://static.momocdn.net/app/json/component-kits/design_system.json"
13
13
 
14
- public struct TrustBannerData: Decodable {
14
+ public struct TrustBannerData: Decodable, Equatable {
15
15
  let content: [String: String]
16
16
  let subContent: [String: String]
17
17
  let pciImage: String
@@ -50,9 +50,41 @@ let defaultBanner = TrustBannerData(
50
50
  momoImage: "https://static.momocdn.net/app/img/kits/trustBanner/ic_secu.png"
51
51
  )
52
52
 
53
+ // AI-GENERATED START: public init so the host can push its remote trust banner config
54
+ public extension TrustBannerData {
55
+ /// Missing images / url fall back to the kit's default banner, like the view does per field.
56
+ init(
57
+ content: [String: String],
58
+ subContent: [String: String],
59
+ pciImage: String?,
60
+ sslImage: String?,
61
+ urlConfig: String?,
62
+ icons: [String]?,
63
+ momoImage: String?
64
+ ) {
65
+ self.init(
66
+ content: content,
67
+ subContent: subContent,
68
+ pciImage: pciImage ?? defaultBanner.pciImage,
69
+ sslImage: sslImage ?? defaultBanner.sslImage,
70
+ urlConfig: urlConfig ?? defaultBanner.urlConfig,
71
+ icons: icons ?? defaultBanner.icons,
72
+ momoImage: momoImage ?? defaultBanner.momoImage
73
+ )
74
+ }
75
+ }
76
+ // AI-GENERATED END: public init so the host can push its remote trust banner config
77
+
53
78
  public struct TrustBanner: View {
54
79
  @Environment(\.applicationEnvironment) var applicationEnvironment
55
80
  @Environment(\.localize) private var localize
81
+ // AI-GENERATED START: container config wins, else the host-pushed DesignSystemKits config (Compose parity)
82
+ @Environment(\.designSystemKitsConfigs) private var designSystemKitsConfigs
83
+
84
+ private var trustBanner: TrustBannerData? {
85
+ (applicationEnvironment.config ?? designSystemKitsConfigs.designSystemConfig)?.trustBanner
86
+ }
87
+ // AI-GENERATED END: container config wins, else the host-pushed DesignSystemKits config (Compose parity)
56
88
 
57
89
  var onPress: ((String)->Void)?
58
90
 
@@ -62,19 +94,19 @@ public struct TrustBanner: View {
62
94
 
63
95
  func onPressSecurity() {
64
96
  if (onPress != nil) {
65
- onPress!(applicationEnvironment.config?.trustBanner?.urlConfig ?? defaultBanner.urlConfig)
97
+ onPress!(trustBanner?.urlConfig ?? defaultBanner.urlConfig)
66
98
  }
67
99
  }
68
100
 
69
101
 
70
102
  public var body: some View {
71
103
  HStack(alignment: .center, spacing: 8) {
72
- WebImage(url: URL(string: applicationEnvironment.config?.trustBanner?.momoImage ?? defaultBanner.momoImage))
104
+ WebImage(url: URL(string: trustBanner?.momoImage ?? defaultBanner.momoImage))
73
105
  .resizable()
74
106
  .scaledToFit()
75
107
  .frame(width: 64, height: 64)
76
108
  VStack(alignment: .leading){
77
- Text(applicationEnvironment.config?.trustBanner?.content[localize.currentLanguage]
109
+ Text(trustBanner?.content[localize.currentLanguage]
78
110
  ?? localize.translateData(defaultBanner.content))
79
111
  .foregroundColor(Color(hex: "484848"))
80
112
  .lineLimit(2)
@@ -84,7 +116,7 @@ public struct TrustBanner: View {
84
116
  .padding(.bottom, 8)
85
117
  HStack {
86
118
  HStack(spacing: 0) {
87
- Text(applicationEnvironment.config?.trustBanner?.subContent[localize.currentLanguage]
119
+ Text(trustBanner?.subContent[localize.currentLanguage]
88
120
  ?? localize.translateData(defaultBanner.subContent))
89
121
  .foregroundColor(Colors.pink03)
90
122
  .font(.system(size: 13, weight: .bold))
@@ -98,11 +130,11 @@ public struct TrustBanner: View {
98
130
 
99
131
  Spacer()
100
132
  HStack{
101
- WebImage(url: URL(string: applicationEnvironment.config?.trustBanner?.pciImage ?? defaultBanner.pciImage))
133
+ WebImage(url: URL(string: trustBanner?.pciImage ?? defaultBanner.pciImage))
102
134
  .resizable()
103
135
  .scaledToFit()
104
136
  .frame(width: 24, height: 20)
105
- WebImage(url: URL(string: applicationEnvironment.config?.trustBanner?.sslImage ?? defaultBanner.sslImage))
137
+ WebImage(url: URL(string: trustBanner?.sslImage ?? defaultBanner.sslImage))
106
138
  .resizable()
107
139
  .scaledToFit()
108
140
  .frame(width: 52, height: 20)
@@ -52,7 +52,7 @@ public struct FontScale: Equatable {
52
52
  }
53
53
 
54
54
  /// A host-pushed mini-app context wins per field, matching Compose's
55
- /// `context?.useOSScaleRate ?: KitFontScale.useOSScaleRate`.
55
+ /// `context?.useOSScaleRate ?: DesignSystemKits.getConfigs().useOSScaleRate`.
56
56
  public func overriding(userScaleRate: CGFloat?, useOSScaleRate: Bool?) -> FontScale {
57
57
  guard userScaleRate != nil || useOSScaleRate != nil else { return self }
58
58
  return FontScale(
@@ -66,7 +66,7 @@ public struct FontScale: Equatable {
66
66
  private struct FontScaleKey: EnvironmentKey {
67
67
  /// Computed, never stored: a bare `UIHostingController` root that never got `.momoFontScale()`
68
68
  /// still reads the live store instead of a value frozen at first access.
69
- static var defaultValue: FontScale { FontScaleStore.shared.snapshot }
69
+ static var defaultValue: FontScale { DesignSystemKits.shared.fontScale }
70
70
  }
71
71
 
72
72
  public extension EnvironmentValues {
@@ -77,9 +77,10 @@ public extension EnvironmentValues {
77
77
  }
78
78
 
79
79
  public extension View {
80
- /// Injects a Dynamic-Type-observing font scale and re-renders this subtree on change.
80
+ /// Injects a Dynamic-Type-observing font scale plus `\.designSystemKitsConfigs` (whitelist, remote config)
81
+ /// and re-renders this subtree on change.
81
82
  /// Use at every `UIHostingController` root that is not inside a `NavigationContainer`.
82
- func momoFontScale(_ store: FontScaleStore = .shared, context: MiniAppContext? = nil) -> some View {
83
+ func momoFontScale(_ store: DesignSystemKits = .shared, context: MiniAppContext? = nil) -> some View {
83
84
  modifier(MomoFontScaleModifier(store: store, context: context))
84
85
  }
85
86
  }
@@ -87,17 +88,19 @@ public extension View {
87
88
  /// `@ObservedObject` is load-bearing, same as `MomoLocalizeModifier`: `@Environment(\.momoFontScale)`
88
89
  /// only invalidates when the boxed snapshot changes, which requires *this* body to re-run on publish.
89
90
  private struct MomoFontScaleModifier: ViewModifier {
90
- @ObservedObject var store: FontScaleStore
91
+ @ObservedObject var store: DesignSystemKits
91
92
  let context: MiniAppContext?
92
93
 
93
94
  func body(content: Content) -> some View {
94
- content.environment(
95
- \.momoFontScale,
96
- store.snapshot.overriding(
97
- userScaleRate: context?.userScaleRate,
98
- useOSScaleRate: context?.useOSScaleRate
95
+ content
96
+ .environment(
97
+ \.momoFontScale,
98
+ store.fontScale.overriding(
99
+ userScaleRate: context?.userScaleRate,
100
+ useOSScaleRate: context?.useOSScaleRate
101
+ )
99
102
  )
100
- )
103
+ .environment(\.designSystemKitsConfigs, store.getConfigs())
101
104
  }
102
105
  }
103
106
  // AI-GENERATED END: reactive font-scale snapshot injected through the environment
@@ -4,7 +4,7 @@ import SwiftUI
4
4
  /// free functions). Inside a SwiftUI view prefer `@Environment(\.momoFontScale)` so the view
5
5
  /// re-renders when Dynamic Type or the host preference changes.
6
6
  public func scaleSize(_ size: CGFloat) -> CGFloat {
7
- FontScaleStore.shared.snapshot(size)
7
+ DesignSystemKits.shared.fontScale(size)
8
8
  }
9
9
 
10
10
  @available(*, deprecated, message: "Max font scale is fixed at 1.5; this override is no longer applied. Drop the second argument.")
@@ -99,7 +99,9 @@ public extension TypographyStyle {
99
99
  }
100
100
  }
101
101
 
102
- private func momoFontName(family: String, weight: Font.Weight) -> String? {
102
+ private func momoFontName(family: String, weight: Font.Weight, isWhiteList: Bool) -> String? {
103
+ // Ngoai whitelist thi tra nil de momoFont() roi ve .system(...), dung nhu truoc DS-791.
104
+ guard isWhiteList else { return nil }
103
105
  guard family == "MomoTrustSans" else { return nil }
104
106
  switch weight {
105
107
  case .ultraLight, .thin: return "MomoTrustSans-ExtraLight"
@@ -113,8 +115,8 @@ private func momoFontName(family: String, weight: Font.Weight) -> String? {
113
115
  }
114
116
  }
115
117
 
116
- private func momoFont(size: CGFloat, weight: Font.Weight, family: String) -> Font {
117
- guard let name = momoFontName(family: family, weight: weight),
118
+ private func momoFont(size: CGFloat, weight: Font.Weight, family: String, isWhiteList: Bool) -> Font {
119
+ guard let name = momoFontName(family: family, weight: weight, isWhiteList: isWhiteList),
118
120
  UIFont(name: name, size: size) != nil else {
119
121
  return .system(size: size, weight: weight)
120
122
  }
@@ -124,6 +126,8 @@ private func momoFont(size: CGFloat, weight: Font.Weight, family: String) -> Fon
124
126
  public struct MomoText: View {
125
127
  @Environment(\.appTheme) private var theme
126
128
  @Environment(\.momoFontScale) private var fontScale
129
+ // From the environment, not DesignSystemKits.shared, so a whitelist flip re-renders the text.
130
+ @Environment(\.designSystemKitsConfigs) private var designSystemKitsConfigs
127
131
  private let content: String
128
132
  private let typography: TypographyStyle
129
133
  private let color: Color?
@@ -184,7 +188,12 @@ public struct MomoText: View {
184
188
  private var baseText: SwiftUI.Text {
185
189
  let keywords = highlightText.filter { !$0.isEmpty }.sorted { $0.count > $1.count }
186
190
  guard !keywords.isEmpty else { return SwiftUI.Text(content) }
187
- let highlightFont = momoFont(size: fontScale(typography.fontSize), weight: TypographyStyle.headerXsSemibold.fontWeight, family: theme.font)
191
+ let highlightFont = momoFont(
192
+ size: fontScale(typography.fontSize),
193
+ weight: TypographyStyle.headerXsSemibold.fontWeight,
194
+ family: theme.font,
195
+ isWhiteList: designSystemKitsConfigs.isWhiteList
196
+ )
188
197
  func isWordChar(_ character: Character?) -> Bool {
189
198
  guard let character else { return false }
190
199
  return character.isLetter || character.isNumber || character == "_"
@@ -221,7 +230,12 @@ public struct MomoText: View {
221
230
  let isStrikethrough = strikethrough || typography.isStrikethrough
222
231
 
223
232
  var text = baseText
224
- .font(momoFont(size: fontScale(typography.fontSize), weight: typography.fontWeight, family: theme.font))
233
+ .font(momoFont(
234
+ size: fontScale(typography.fontSize),
235
+ weight: typography.fontWeight,
236
+ family: theme.font,
237
+ isWhiteList: designSystemKitsConfigs.isWhiteList
238
+ ))
225
239
  .foregroundColor(textColor)
226
240
  .strikethrough(isStrikethrough, color: textColor)
227
241
  .underline(underline, color: textColor)
@@ -10,12 +10,12 @@ public extension View {
10
10
  }
11
11
 
12
12
  public extension Font {
13
- /// Non-reactive: resolves against the current `FontScaleStore` snapshot at each access. The
13
+ /// Non-reactive: resolves against the current `DesignSystemKits` font scale at each access. The
14
14
  /// styles below are computed properties for that reason — as stored `static let`s they would
15
15
  /// freeze the scale at first access and never pick up a Dynamic Type change.
16
16
  /// Inside a view, `@Environment(\.momoFontScale)` + `fontScale.font(size:weight:)` also re-renders.
17
17
  static func appFont(size: CGFloat) -> Font {
18
- FontScaleStore.shared.snapshot.font(size: size)
18
+ DesignSystemKits.shared.fontScale.font(size: size)
19
19
  }
20
20
 
21
21
  // New supported typography styles
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKits'
3
- s.version = '0.167.1-beta.4'
3
+ s.version = '0.167.1-beta.9'
4
4
  s.summary = 'MoMoUIKits for iOS'
5
5
  s.homepage = 'https://momo.vn'
6
6
  s.license = { :type => 'MIT' }
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKitsDemo'
3
- s.version = '0.167.1-beta.4'
3
+ s.version = '0.167.1-beta.9'
4
4
  s.summary = 'Demo browser for MoMoUIKits SwiftUI components'
5
5
  s.homepage = 'https://momo.vn'
6
6
  s.license = { :type => 'MIT' }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.167.1-beta.11-debug",
3
+ "version": "0.167.1-beta.13-debug",
4
4
  "private": false,
5
5
  "dependeNavigationncies": {},
6
6
  "devDependencies": {},
@@ -1,18 +0,0 @@
1
- package vn.momo.kits.const
2
-
3
- import androidx.compose.runtime.getValue
4
- import androidx.compose.runtime.mutableStateOf
5
- import androidx.compose.runtime.setValue
6
-
7
- // Design-system rollout holder, same shape as KitFontScale: the kit is sandboxed and can't read the
8
- // host config, so the host seeds this once at startup from DesignSystemConfig. getFontFamily() reads
9
- // it to decide whether the new default font applies, so every screen follows the flag with no prop
10
- // threading. Backed by snapshot state: writes recompose text automatically.
11
- object KitDesignSystem {
12
- var isWhiteList by mutableStateOf(false)
13
- private set
14
-
15
- fun update(isWhiteList: Boolean) {
16
- this.isWhiteList = isWhiteList
17
- }
18
- }
@@ -1,22 +0,0 @@
1
- package vn.momo.kits.const
2
-
3
- import androidx.compose.runtime.getValue
4
- import androidx.compose.runtime.mutableStateOf
5
- import androidx.compose.runtime.setValue
6
-
7
- // App-wide font-scale holder. The kit is sandboxed and can't read the host cache directly, so the
8
- // host (which sees both core and this kit) seeds this once at startup and pushes runtime updates into
9
- // it. scaleSize() reads it as the fallback, so every screen — including direct NavigationContainer
10
- // screens with no host-pushed MiniAppContext — scales with no per-render prop and no async flicker.
11
- // Backed by snapshot state: writes recompose text automatically.
12
- object KitFontScale {
13
- var userScaleRate by mutableStateOf(1f)
14
- private set
15
- var useOSScaleRate by mutableStateOf(true)
16
- private set
17
-
18
- fun update(userScaleRate: Float, useOSScaleRate: Boolean) {
19
- this.userScaleRate = userScaleRate
20
- this.useOSScaleRate = useOSScaleRate
21
- }
22
- }
@@ -1,58 +0,0 @@
1
- import SwiftUI
2
-
3
- /// App-wide font-scale inputs for the SwiftUI kit, and the single place they publish from.
4
- /// The host writes `update(...)` at startup (from the cache-backed `FontScaleProvider`) and again
5
- /// whenever the in-app font-size preference changes; the OS Dynamic Type factor is tracked here.
6
- /// Mirrors Compose's `KitFontScale` + `ProvideOsFontScale` pair, including its live behaviour —
7
- /// views re-render on change as long as the subtree got `.momoFontScale()` (`NavigationContainer`
8
- /// applies it). When `useOSScaleRate` is false the kit applies `userScaleRate` instead of the OS factor.
9
- public final class FontScaleStore: ObservableObject {
10
- public static let shared = FontScaleStore()
11
-
12
- @Published public private(set) var userScaleRate: CGFloat = 1
13
- @Published public private(set) var useOSScaleRate: Bool = true
14
- @Published public private(set) var osFontScale: CGFloat = FontScaleStore.currentOsFontScale()
15
-
16
- private init() {
17
- NotificationCenter.default.addObserver(
18
- self,
19
- selector: #selector(contentSizeCategoryDidChange),
20
- name: UIContentSizeCategory.didChangeNotification,
21
- object: nil
22
- )
23
- }
24
-
25
- public var snapshot: FontScale {
26
- FontScale(
27
- userScaleRate: userScaleRate,
28
- useOSScaleRate: useOSScaleRate,
29
- osFontScale: osFontScale
30
- )
31
- }
32
-
33
- public func update(userScaleRate: CGFloat, useOSScaleRate: Bool) {
34
- onMain {
35
- // Re-assigning an unchanged value would still publish and invalidate every kit subtree.
36
- if self.userScaleRate != userScaleRate { self.userScaleRate = userScaleRate }
37
- if self.useOSScaleRate != useOSScaleRate { self.useOSScaleRate = useOSScaleRate }
38
- }
39
- }
40
-
41
- @objc private func contentSizeCategoryDidChange() {
42
- let next = Self.currentOsFontScale()
43
- guard next != osFontScale else { return }
44
- osFontScale = next
45
- }
46
-
47
- /// Same factor Compose's iOS `ProvideOsFontScale` derives from
48
- /// `UIFont.preferredFont(forTextStyle: .body).pointSize / 17`, so both iOS surfaces scale alike.
49
- static func currentOsFontScale() -> CGFloat {
50
- let base = UIFont.systemFont(ofSize: UIFont.labelFontSize)
51
- return UIFontMetrics.default.scaledFont(for: base).pointSize / base.pointSize
52
- }
53
-
54
- /// Synchronous when already on main — an async hop would defer the change by a frame.
55
- private func onMain(_ work: @escaping () -> Void) {
56
- if Thread.isMainThread { work() } else { DispatchQueue.main.async(execute: work) }
57
- }
58
- }
@@ -1,17 +0,0 @@
1
- import Foundation
2
-
3
- /// Design-system rollout flag for the SwiftUI kit, mirroring Compose's `KitDesignSystem`.
4
- /// The kit is sandboxed and can't read the host config, so the host writes `update(_:)` once at
5
- /// startup from `DesignSystemConfig`. Startup-only and non-reactive, like `FontScaleStore` was
6
- /// before it gained live updates: there is no SwiftUI navigation container to re-render from.
7
- public final class KitDesignSystem {
8
- public static let shared = KitDesignSystem()
9
-
10
- public private(set) var isWhiteList: Bool = false
11
-
12
- private init() {}
13
-
14
- public func update(isWhiteList: Bool) {
15
- self.isWhiteList = isWhiteList
16
- }
17
- }