@momo-kits/native-kits 0.167.1-beta.16-debug → 0.167.1-beta.17-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 (38) hide show
  1. package/CLAUDE.md +4 -6
  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 +14 -0
  5. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +11 -5
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +2 -2
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
  10. package/gradle.properties +1 -1
  11. package/ios/Application/ApplicationEnvironment.swift +6 -13
  12. package/ios/Template/TrustBanner/TrustBanner.swift +7 -39
  13. package/ios/Title/Title.swift +8 -3
  14. package/ios/Typography/FontScale.swift +11 -14
  15. package/ios/Typography/FontScaleStore.swift +58 -0
  16. package/ios/Typography/KitDesignSystem.swift +17 -0
  17. package/ios/Typography/Text.swift +36 -20
  18. package/ios/Typography/Typography.swift +2 -2
  19. package/ios/native-kits.podspec +1 -1
  20. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  21. package/package.json +1 -1
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +0 -62
  23. package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Example.xcscheme +0 -32
  24. package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  25. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  26. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/WorkspaceSettings.xcsettings +0 -16
  27. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
  28. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -5
  29. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
  30. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
  31. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImage.xcscheme +0 -58
  32. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +0 -58
  33. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SkeletonUI.xcscheme +0 -58
  34. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios-LottiePrivacyInfo.xcscheme +0 -58
  35. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios.xcscheme +0 -58
  36. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -60
  37. package/ios/Application/DesignSystemKits.swift +0 -100
  38. package/ios/Application/DesignSystemKitsPayload.swift +0 -68
package/CLAUDE.md CHANGED
@@ -41,9 +41,7 @@ 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
- - **`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.
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.
47
45
  - **Platform code** goes through `expect`/`actual` in `platform/` — never reference Android/iOS APIs from `commonMain` directly.
48
46
  - **Native bridge:** `IMaxApi` (`vn.momo.maxapi`, exposed as `LocalMaxApi`) is the host-app bridge (dismiss, tracking, device info, language).
49
47
  - Navigation uses a runtime `DynamicScreenRegistry`; overlays (modal/bottom-sheet/snackbar) go through `Navigator` + `OverplayComponentRegistry`.
@@ -71,9 +69,9 @@ The SwiftUI port differs from Compose in one important way: **it follows the hos
71
69
 
72
70
  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.
73
71
 
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.
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.
77
75
  - 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`.
78
76
 
79
77
  ## Code conventions
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.167.1-beta.16-debug"
43
+ version = "0.167.1-beta.17-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.16-debug'
3
+ spec.version = '0.167.1-beta.17-debug'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -1,6 +1,9 @@
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
4
7
  import androidx.compose.runtime.staticCompositionLocalOf
5
8
  import vn.momo.kits.components.TrustBannerData
6
9
 
@@ -16,6 +19,17 @@ data class DesignSystemConfig(
16
19
  val headerGradient: String? = null,
17
20
  )
18
21
 
22
+ // App-wide holder seeded by the host (same pattern as KitFontScale), so direct NavigationContainer
23
+ // callers get the config without passing it. Snapshot state: a config fetched after first frame recomposes.
24
+ object KitDesignSystemConfig {
25
+ var config by mutableStateOf<DesignSystemConfig?>(null)
26
+ private set
27
+
28
+ fun update(config: DesignSystemConfig?) {
29
+ this.config = config
30
+ }
31
+ }
32
+
19
33
  @Immutable
20
34
  data class MiniAppContext(
21
35
  val appIcon: String = "",
@@ -0,0 +1,18 @@
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
+ }
@@ -0,0 +1,22 @@
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
+ }
@@ -8,7 +8,6 @@ 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
12
11
  import vn.momo.kits.application.LocalContext
13
12
  import vn.momo.kits.platform.getScreenDimensions
14
13
  import kotlin.math.max
@@ -21,9 +20,10 @@ const val MAX_DEVICE_SCALE = 5
21
20
  @Composable
22
21
  fun scaleSize(size: Float): Float {
23
22
  val context = LocalContext.current
24
- val useOsScale = context?.useOSScaleRate ?: DesignSystemKits.getConfigs().useOSScaleRate
23
+ // Host-pushed context wins; otherwise fall back to the app-wide KitFontScale holder.
24
+ val useOsScale = context?.useOSScaleRate ?: KitFontScale.useOSScaleRate
25
25
  if (!useOsScale) {
26
- return size * (context?.userScaleRate ?: DesignSystemKits.getConfigs().userScaleRate)
26
+ return size * (context?.userScaleRate ?: KitFontScale.userScaleRate)
27
27
  }
28
28
  val scaleSizeMaxRate: Float = MAX_FONT_SCALE
29
29
  val deviceWidth = getScreenDimensions().width
@@ -83,6 +83,12 @@ 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
+ */
86
92
  private const val LEGACY_DEFAULT_FONT_FILE = "SFProText-Regular.ttf"
87
93
  private const val NEW_DEFAULT_FONT_FILE = "MomoTrustSans-Regular.ttf"
88
94
 
@@ -118,11 +124,11 @@ private const val NEW_DEFAULT_FONT_FAMILY = "MomoTrustSans"
118
124
  private const val LEGACY_DEFAULT_FONT_FAMILY = "SFProText"
119
125
 
120
126
  internal val defaultFontFamily: String
121
- get() = if (DesignSystemKits.getConfigs().isWhiteList) NEW_DEFAULT_FONT_FAMILY else LEGACY_DEFAULT_FONT_FAMILY
127
+ get() = if (KitDesignSystem.isWhiteList) NEW_DEFAULT_FONT_FAMILY else LEGACY_DEFAULT_FONT_FAMILY
122
128
 
123
129
  @Composable
124
130
  fun getFontFamily(fontFamily: String, fontWeight: FontWeight? = FontWeight.Normal): FontFamily {
125
- val isWhiteList = DesignSystemKits.getConfigs().isWhiteList
131
+ val isWhiteList = KitDesignSystem.isWhiteList
126
132
  val defaultFile = if (isWhiteList) NEW_DEFAULT_FONT_FILE else LEGACY_DEFAULT_FONT_FILE
127
133
  val key = "$fontFamily-${fontWeight?.weight}"
128
134
  val fileName = hostFontFiles[key] ?: defaultFile
@@ -20,7 +20,7 @@ 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.DesignSystemKits
23
+ import vn.momo.kits.application.KitDesignSystemConfig
24
24
  import vn.momo.kits.application.LocalContext
25
25
  import vn.momo.kits.application.LocalDesignSystemConfig
26
26
  import vn.momo.kits.application.LocalLocalize
@@ -63,7 +63,7 @@ fun NavigationContainer(
63
63
  val stackTransition = remember { StackTransitionCoordinator() }
64
64
  val theme = remember { mutableStateOf(initialTheme) }
65
65
  val resolvedDesignSystemConfig =
66
- designSystemConfig ?: LocalDesignSystemConfig.current ?: DesignSystemKits.getConfigs().designSystemConfig
66
+ designSystemConfig ?: LocalDesignSystemConfig.current ?: KitDesignSystemConfig.config
67
67
  val headerBar = resolvedDesignSystemConfig?.headerBar
68
68
  // RN parity: the theme's own header image wins; config headerBar is only the fallback.
69
69
  val resolvedTheme = remember(theme.value, headerBar) {
@@ -8,13 +8,13 @@ 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
12
11
  import vn.momo.kits.components.Text
12
+ import vn.momo.kits.const.KitDesignSystem
13
13
  import vn.momo.kits.const.Typography
14
14
  import vn.momo.kits.modifier.setAutomationId
15
15
 
16
16
  internal val headerFontFamily: String?
17
- get() = if (DesignSystemKits.getConfigs().isWhiteList) "MoMoTrustDisplay" else null
17
+ get() = if (KitDesignSystem.isWhiteList) "MoMoTrustDisplay" else null
18
18
 
19
19
  @Composable
20
20
  internal fun HeaderTitle(
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.16
21
+ version=0.167.1-beta.17
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 DesignSystemKits. Mirrors Compose's
21
- // MiniAppContext.userScaleRate / useOSScaleRate, which win per field over DesignSystemKits.
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.
22
22
  public var userScaleRate: CGFloat? = nil
23
23
  public var useOSScaleRate: Bool? = nil
24
24
 
@@ -37,17 +37,10 @@ public class MiniAppContext {
37
37
  }
38
38
  }
39
39
 
40
- // Value type so DesignSystemKitsConfigs can dedupe host pushes; mirrors Compose's DesignSystemConfig.
41
- public struct KitConfig: Equatable {
42
- public var trustBanner: TrustBannerData?
43
- public var headerBar: String?
44
- public var headerGradient: String?
45
-
46
- public init(trustBanner: TrustBannerData? = nil, headerBar: String? = nil, headerGradient: String? = nil) {
47
- self.trustBanner = trustBanner
48
- self.headerBar = headerBar
49
- self.headerGradient = headerGradient
50
- }
40
+ public class KitConfig {
41
+ public var trustBanner: TrustBannerData? = nil
42
+ public var headerBar: String? = nil
43
+ public var headerGradient: String? = nil
51
44
  }
52
45
 
53
46
  public class ApplicationEnvironment: ObservableObject {
@@ -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, Equatable {
14
+ public struct TrustBannerData: Decodable {
15
15
  let content: [String: String]
16
16
  let subContent: [String: String]
17
17
  let pciImage: String
@@ -50,41 +50,9 @@ 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
-
78
53
  public struct TrustBanner: View {
79
54
  @Environment(\.applicationEnvironment) var applicationEnvironment
80
55
  @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)
88
56
 
89
57
  var onPress: ((String)->Void)?
90
58
 
@@ -94,19 +62,19 @@ public struct TrustBanner: View {
94
62
 
95
63
  func onPressSecurity() {
96
64
  if (onPress != nil) {
97
- onPress!(trustBanner?.urlConfig ?? defaultBanner.urlConfig)
65
+ onPress!(applicationEnvironment.config?.trustBanner?.urlConfig ?? defaultBanner.urlConfig)
98
66
  }
99
67
  }
100
68
 
101
69
 
102
70
  public var body: some View {
103
71
  HStack(alignment: .center, spacing: 8) {
104
- WebImage(url: URL(string: trustBanner?.momoImage ?? defaultBanner.momoImage))
72
+ WebImage(url: URL(string: applicationEnvironment.config?.trustBanner?.momoImage ?? defaultBanner.momoImage))
105
73
  .resizable()
106
74
  .scaledToFit()
107
75
  .frame(width: 64, height: 64)
108
76
  VStack(alignment: .leading){
109
- Text(trustBanner?.content[localize.currentLanguage]
77
+ Text(applicationEnvironment.config?.trustBanner?.content[localize.currentLanguage]
110
78
  ?? localize.translateData(defaultBanner.content))
111
79
  .foregroundColor(Color(hex: "484848"))
112
80
  .lineLimit(2)
@@ -116,7 +84,7 @@ public struct TrustBanner: View {
116
84
  .padding(.bottom, 8)
117
85
  HStack {
118
86
  HStack(spacing: 0) {
119
- Text(trustBanner?.subContent[localize.currentLanguage]
87
+ Text(applicationEnvironment.config?.trustBanner?.subContent[localize.currentLanguage]
120
88
  ?? localize.translateData(defaultBanner.subContent))
121
89
  .foregroundColor(Colors.pink03)
122
90
  .font(.system(size: 13, weight: .bold))
@@ -130,11 +98,11 @@ public struct TrustBanner: View {
130
98
 
131
99
  Spacer()
132
100
  HStack{
133
- WebImage(url: URL(string: trustBanner?.pciImage ?? defaultBanner.pciImage))
101
+ WebImage(url: URL(string: applicationEnvironment.config?.trustBanner?.pciImage ?? defaultBanner.pciImage))
134
102
  .resizable()
135
103
  .scaledToFit()
136
104
  .frame(width: 24, height: 20)
137
- WebImage(url: URL(string: trustBanner?.sslImage ?? defaultBanner.sslImage))
105
+ WebImage(url: URL(string: applicationEnvironment.config?.trustBanner?.sslImage ?? defaultBanner.sslImage))
138
106
  .resizable()
139
107
  .scaledToFit()
140
108
  .frame(width: 52, height: 20)
@@ -152,11 +152,16 @@ public struct Title: View {
152
152
  }
153
153
 
154
154
  private var titleText: some View {
155
- SwiftUI.Text(title)
156
- .font(.system(size: fontScale(titleFontSize), weight: titleFontWeight))
155
+ let fontSize = fontScale(titleFontSize)
156
+ // lineSpacing is added on top of the font's natural line height, not on top of fontSize.
157
+ let lineSpacing = max(0, fontScale(titleLineHeight)
158
+ - UIFont.systemFont(ofSize: fontSize, weight: uiFontWeight(titleFontWeight)).lineHeight)
159
+
160
+ return SwiftUI.Text(title)
161
+ .font(.system(size: fontSize, weight: titleFontWeight))
157
162
  .ignoreBoldTextSetting()
158
163
  .foregroundColor(theme.colors.text.default)
159
- .lineSpacing(fontScale(titleLineHeight) - fontScale(titleFontSize))
164
+ .lineSpacing(lineSpacing)
160
165
  .lineLimit(titleMaxLines)
161
166
  .truncationMode(.tail)
162
167
  }
@@ -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 ?: DesignSystemKits.getConfigs().useOSScaleRate`.
55
+ /// `context?.useOSScaleRate ?: KitFontScale.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 { DesignSystemKits.shared.fontScale }
69
+ static var defaultValue: FontScale { FontScaleStore.shared.snapshot }
70
70
  }
71
71
 
72
72
  public extension EnvironmentValues {
@@ -77,10 +77,9 @@ public extension EnvironmentValues {
77
77
  }
78
78
 
79
79
  public extension View {
80
- /// Injects a Dynamic-Type-observing font scale plus `\.designSystemKitsConfigs` (whitelist, remote config)
81
- /// and re-renders this subtree on change.
80
+ /// Injects a Dynamic-Type-observing font scale and re-renders this subtree on change.
82
81
  /// Use at every `UIHostingController` root that is not inside a `NavigationContainer`.
83
- func momoFontScale(_ store: DesignSystemKits = .shared, context: MiniAppContext? = nil) -> some View {
82
+ func momoFontScale(_ store: FontScaleStore = .shared, context: MiniAppContext? = nil) -> some View {
84
83
  modifier(MomoFontScaleModifier(store: store, context: context))
85
84
  }
86
85
  }
@@ -88,19 +87,17 @@ public extension View {
88
87
  /// `@ObservedObject` is load-bearing, same as `MomoLocalizeModifier`: `@Environment(\.momoFontScale)`
89
88
  /// only invalidates when the boxed snapshot changes, which requires *this* body to re-run on publish.
90
89
  private struct MomoFontScaleModifier: ViewModifier {
91
- @ObservedObject var store: DesignSystemKits
90
+ @ObservedObject var store: FontScaleStore
92
91
  let context: MiniAppContext?
93
92
 
94
93
  func body(content: Content) -> some View {
95
- content
96
- .environment(
97
- \.momoFontScale,
98
- store.fontScale.overriding(
99
- userScaleRate: context?.userScaleRate,
100
- useOSScaleRate: context?.useOSScaleRate
101
- )
94
+ content.environment(
95
+ \.momoFontScale,
96
+ store.snapshot.overriding(
97
+ userScaleRate: context?.userScaleRate,
98
+ useOSScaleRate: context?.useOSScaleRate
102
99
  )
103
- .environment(\.designSystemKitsConfigs, store.getConfigs())
100
+ )
104
101
  }
105
102
  }
106
103
  // AI-GENERATED END: reactive font-scale snapshot injected through the environment
@@ -0,0 +1,58 @@
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
+ }
@@ -0,0 +1,17 @@
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
+ }
@@ -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
- DesignSystemKits.shared.fontScale(size)
7
+ FontScaleStore.shared.snapshot(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,9 +99,9 @@ public extension TypographyStyle {
99
99
  }
100
100
  }
101
101
 
102
- private func momoFontName(family: String, weight: Font.Weight, isWhiteList: Bool) -> String? {
102
+ private func momoFontName(family: String, weight: Font.Weight) -> String? {
103
103
  // Ngoai whitelist thi tra nil de momoFont() roi ve .system(...), dung nhu truoc DS-791.
104
- guard isWhiteList else { return nil }
104
+ guard KitDesignSystem.shared.isWhiteList else { return nil }
105
105
  guard family == "MomoTrustSans" else { return nil }
106
106
  switch weight {
107
107
  case .ultraLight, .thin: return "MomoTrustSans-ExtraLight"
@@ -115,19 +115,40 @@ private func momoFontName(family: String, weight: Font.Weight, isWhiteList: Bool
115
115
  }
116
116
  }
117
117
 
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
+ private func momoFont(size: CGFloat, weight: Font.Weight, family: String) -> Font {
119
+ guard let name = momoFontName(family: family, weight: weight),
120
120
  UIFont(name: name, size: size) != nil else {
121
121
  return .system(size: size, weight: weight)
122
122
  }
123
123
  return .custom(name, fixedSize: size)
124
124
  }
125
125
 
126
+ // UIFont twin of momoFont(); only used to read the real line height for lineSpacing.
127
+ private func momoUIFont(size: CGFloat, weight: Font.Weight, family: String) -> UIFont {
128
+ if let name = momoFontName(family: family, weight: weight),
129
+ let font = UIFont(name: name, size: size) {
130
+ return font
131
+ }
132
+ return .systemFont(ofSize: size, weight: uiFontWeight(weight))
133
+ }
134
+
135
+ func uiFontWeight(_ weight: Font.Weight) -> UIFont.Weight {
136
+ switch weight {
137
+ case .ultraLight: return .ultraLight
138
+ case .thin: return .thin
139
+ case .light: return .light
140
+ case .medium: return .medium
141
+ case .semibold: return .semibold
142
+ case .bold: return .bold
143
+ case .heavy: return .heavy
144
+ case .black: return .black
145
+ default: return .regular
146
+ }
147
+ }
148
+
126
149
  public struct MomoText: View {
127
150
  @Environment(\.appTheme) private var theme
128
151
  @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
131
152
  private let content: String
132
153
  private let typography: TypographyStyle
133
154
  private let color: Color?
@@ -188,12 +209,7 @@ public struct MomoText: View {
188
209
  private var baseText: SwiftUI.Text {
189
210
  let keywords = highlightText.filter { !$0.isEmpty }.sorted { $0.count > $1.count }
190
211
  guard !keywords.isEmpty else { return SwiftUI.Text(content) }
191
- let highlightFont = momoFont(
192
- size: fontScale(typography.fontSize),
193
- weight: TypographyStyle.headerXsSemibold.fontWeight,
194
- family: theme.font,
195
- isWhiteList: designSystemKitsConfigs.isWhiteList
196
- )
212
+ let highlightFont = momoFont(size: fontScale(typography.fontSize), weight: TypographyStyle.headerXsSemibold.fontWeight, family: theme.font)
197
213
  func isWordChar(_ character: Character?) -> Bool {
198
214
  guard let character else { return false }
199
215
  return character.isLetter || character.isNumber || character == "_"
@@ -229,13 +245,13 @@ public struct MomoText: View {
229
245
  // *Strikethrough styles) or the explicit `strikethrough` flag.
230
246
  let isStrikethrough = strikethrough || typography.isStrikethrough
231
247
 
248
+ let fontSize = fontScale(typography.fontSize)
249
+ // lineSpacing is added on top of the font's natural line height, not on top of fontSize.
250
+ let lineSpacing = max(0, fontScale(typography.lineHeight)
251
+ - momoUIFont(size: fontSize, weight: typography.fontWeight, family: theme.font).lineHeight)
252
+
232
253
  var text = baseText
233
- .font(momoFont(
234
- size: fontScale(typography.fontSize),
235
- weight: typography.fontWeight,
236
- family: theme.font,
237
- isWhiteList: designSystemKitsConfigs.isWhiteList
238
- ))
254
+ .font(momoFont(size: fontSize, weight: typography.fontWeight, family: theme.font))
239
255
  .foregroundColor(textColor)
240
256
  .strikethrough(isStrikethrough, color: textColor)
241
257
  .underline(underline, color: textColor)
@@ -246,7 +262,7 @@ public struct MomoText: View {
246
262
 
247
263
  return text
248
264
  .multilineTextAlignment(textAlign)
249
- .lineSpacing(fontScale(typography.lineHeight) - fontScale(typography.fontSize))
265
+ .lineSpacing(lineSpacing)
250
266
  .modifier(LineLimitModifier(range: lineLimitRange))
251
267
  .truncationMode(truncationMode)
252
268
  .fixedSize(horizontal: !softWrap, vertical: false)
@@ -10,12 +10,12 @@ public extension View {
10
10
  }
11
11
 
12
12
  public extension Font {
13
- /// Non-reactive: resolves against the current `DesignSystemKits` font scale at each access. The
13
+ /// Non-reactive: resolves against the current `FontScaleStore` snapshot 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
- DesignSystemKits.shared.fontScale.font(size: size)
18
+ FontScaleStore.shared.snapshot.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.16'
3
+ s.version = '0.167.1-beta.17'
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.16'
3
+ s.version = '0.167.1-beta.17'
4
4
  s.summary = 'Demo browser for MoMoUIKits SwiftUI components'
5
5
  s.homepage = 'https://momo.vn'
6
6
  s.license = { :type => 'MIT' }