@momo-kits/native-kits 0.162.2-sp.7 → 0.162.2-sp.8-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 (201) hide show
  1. package/.claude/settings.local.json +15 -1
  2. package/CLAUDE.md +78 -0
  3. package/build.gradle.kts +11 -0
  4. package/compose/build.gradle.kts +190 -0
  5. package/compose/build.gradle.kts.backup +190 -0
  6. package/compose/compose.podspec +47 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +123 -0
  10. package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
  11. package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
  12. package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
  13. package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
  14. package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
  15. package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
  16. package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
  17. package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
  18. package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
  19. package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
  20. package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
  21. package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
  22. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
  23. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +106 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +294 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +826 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +543 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +112 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +477 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +214 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +216 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +259 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +182 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +328 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +97 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +217 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +470 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +131 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +203 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +175 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +306 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +276 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +2 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +35 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +2 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +253 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +98 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +157 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +360 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +562 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +154 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +225 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +320 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +81 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +309 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +34 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +360 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +128 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +70 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +58 -0
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +105 -0
  132. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  133. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  134. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +184 -0
  135. package/gradle/libs.versions.toml +69 -0
  136. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  137. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  138. package/gradle.properties +26 -0
  139. package/gradlew +252 -0
  140. package/gradlew.bat +94 -0
  141. package/ios/Application/ApplicationEnvironment.swift +4 -2
  142. package/ios/Application/Components.swift +16 -0
  143. package/ios/Application/Localize.swift +277 -0
  144. package/ios/Application/MaxApi.swift +18 -0
  145. package/ios/Application/Navigation/BottomTab/BottomTab.swift +1 -0
  146. package/ios/Application/Navigation/NavigationContainer.swift +27 -3
  147. package/ios/Application/Navigation/Navigator.swift +50 -29
  148. package/ios/Application/Navigation/Overplay/BottomSheet.swift +87 -19
  149. package/ios/Application/Navigation/Overplay/ModalScreen.swift +6 -0
  150. package/ios/Application/Navigation/Tracking/ScreenTracker.swift +117 -0
  151. package/ios/Application/Screen.swift +1 -1
  152. package/ios/Application/StackScreen.swift +49 -1
  153. package/ios/Badge/BadgeRibbon.swift +80 -0
  154. package/ios/Button/Button.swift +68 -12
  155. package/ios/Carousel/Carousel.swift +16 -1
  156. package/ios/Checkbox/Checkbox.swift +14 -20
  157. package/ios/Chip/Chip.swift +35 -26
  158. package/ios/Collapse/Collapse.swift +11 -1
  159. package/ios/Colors+Radius+Spacing/Colors.swift +9 -6
  160. package/ios/Colors+Radius+Spacing/Spacing.swift +3 -1
  161. package/ios/DateTimePicker/DateTimePicker.swift +4 -2
  162. package/ios/DateTimePicker/WheelPicker.swift +7 -3
  163. package/ios/Extensions/ActiveOpacityButtonStyle.swift +14 -0
  164. package/ios/Extensions/Color++.swift +5 -2
  165. package/ios/Icon/Icon.swift +5 -1
  166. package/ios/IconButton/IconButton.swift +35 -7
  167. package/ios/Image/Image.swift +16 -3
  168. package/ios/Information/Information.swift +3 -1
  169. package/ios/Input/ErrorView.swift +3 -1
  170. package/ios/Input/Input.swift +31 -8
  171. package/ios/Input/InputPhoneNumber.swift +14 -3
  172. package/ios/Input/InputSearch.swift +7 -1
  173. package/ios/Input/InputTextArea.swift +78 -34
  174. package/ios/InputDropDown/InputDropDown.swift +4 -1
  175. package/ios/InputMoney/InputMoney.swift +16 -4
  176. package/ios/PARITY_AUDIT.md +641 -0
  177. package/ios/Popup/PopupDisplay.swift +3 -4
  178. package/ios/Popup/PopupNotify.swift +3 -4
  179. package/ios/Popup/PopupPromotion.swift +16 -8
  180. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  181. package/ios/Radio/Radio.swift +12 -13
  182. package/ios/Rating/Rating.swift +1 -1
  183. package/ios/Stepper/Stepper.swift +2 -2
  184. package/ios/Steps/Steps.swift +22 -6
  185. package/ios/SuggestAction/SuggestAction.swift +5 -3
  186. package/ios/Swipeable/SwipeCell.swift +5 -3
  187. package/ios/Switch/Switch.swift +53 -28
  188. package/ios/TabView/TabView.swift +18 -14
  189. package/ios/Template/TrustBanner/TrustBanner.swift +6 -7
  190. package/ios/Tooltip/Tooltip.swift +3 -0
  191. package/ios/Typography/FontScaleStore.swift +19 -0
  192. package/ios/Typography/Text.swift +3 -0
  193. package/ios/Typography/Typography.swift +8 -5
  194. package/ios/Uploader/Uploader.swift +0 -9
  195. package/ios/native-kits.podspec +1 -1
  196. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  197. package/ios-demo/README.md +120 -0
  198. package/local.properties +8 -0
  199. package/package.json +1 -1
  200. package/settings.gradle.kts +64 -0
  201. package/ios/Colors+Radius+Spacing/MoMoUIKitsResources.swift +0 -2
@@ -56,6 +56,7 @@ public struct BottomTab: View {
56
56
  }
57
57
  .frame(maxWidth: .infinity, maxHeight: .infinity)
58
58
  .ignoresSafeArea(edges: .bottom)
59
+ .preference(key: IsBottomTabRootKey.self, value: true)
59
60
  }
60
61
 
61
62
  @ViewBuilder
@@ -14,9 +14,11 @@ import SwiftUI
14
14
  public struct NavigationContainer<Initial: View>: View {
15
15
  @StateObject private var navigator: Navigator
16
16
  @StateObject private var applicationEnvironment: ApplicationEnvironment
17
+ @StateObject private var localize: Localize
17
18
 
18
19
  private let setNavigator: ((Navigator) -> Void)?
19
20
  private let composeApi: KitComposeApi?
21
+ private let maxApi: MaxApi?
20
22
 
21
23
  public init(
22
24
  initialScreenName: String = "",
@@ -25,6 +27,8 @@ public struct NavigationContainer<Initial: View>: View {
25
27
  applicationContext: MiniAppContext? = nil,
26
28
  composeApi: KitComposeApi? = nil,
27
29
  config: KitConfig? = nil,
30
+ maxApi: MaxApi? = nil,
31
+ localize: Localize? = nil,
28
32
  setNavigator: ((Navigator) -> Void)? = nil
29
33
  ) {
30
34
  let nav = Navigator(
@@ -38,10 +42,13 @@ public struct NavigationContainer<Initial: View>: View {
38
42
  wrappedValue: ApplicationEnvironment(
39
43
  applicationContext: applicationContext,
40
44
  composeApi: composeApi,
41
- config: config
45
+ config: config,
46
+ maxApi: maxApi
42
47
  )
43
48
  )
49
+ _localize = StateObject(wrappedValue: localize ?? Localize())
44
50
  self.composeApi = composeApi
51
+ self.maxApi = maxApi
45
52
  self.setNavigator = setNavigator
46
53
  }
47
54
 
@@ -55,11 +62,13 @@ public struct NavigationContainer<Initial: View>: View {
55
62
  .environmentObject(navigator)
56
63
  .environmentObject(applicationEnvironment)
57
64
  .environment(\.applicationEnvironment, applicationEnvironment)
65
+ .environmentObject(localize)
58
66
  .fullScreenCover(item: $navigator.presented) { route in
59
67
  screenView(forDialog: route)
60
68
  .environmentObject(navigator)
61
69
  .environmentObject(applicationEnvironment)
62
70
  .environment(\.applicationEnvironment, applicationEnvironment)
71
+ .environmentObject(localize)
63
72
  }
64
73
  .overlay {
65
74
  if let item = navigator.overplay {
@@ -67,6 +76,7 @@ public struct NavigationContainer<Initial: View>: View {
67
76
  .environmentObject(navigator)
68
77
  .environmentObject(applicationEnvironment)
69
78
  .environment(\.applicationEnvironment, applicationEnvironment)
79
+ .environmentObject(localize)
70
80
  }
71
81
  }
72
82
  .onAppear {
@@ -74,12 +84,19 @@ public struct NavigationContainer<Initial: View>: View {
74
84
  navigator.composeApi = composeApi
75
85
  }
76
86
  setNavigator?(navigator)
87
+ // Mirrors Compose's LaunchedEffect(maxApi, resolvedLocalize): host-reported
88
+ // language wins over the default "vi" only when it resolves to English.
89
+ maxApi?.getLanguage { data in
90
+ if parseLanguage(data) == Localize.EN {
91
+ localize.changeLanguage(Localize.EN)
92
+ }
93
+ }
77
94
  }
78
95
  }
79
96
 
80
97
  @ViewBuilder
81
98
  private func screenView(for route: DynamicScreenRoute) -> some View {
82
- if let screen = DynamicScreenRegistry.shared.getScreen(id: route.id) {
99
+ if let screen = navigator.registry.getScreen(id: route.id) {
83
100
  ScreenHost(screen: screen, navigator: navigator)
84
101
  .navigationBarBackButtonHidden(true)
85
102
  } else {
@@ -89,7 +106,7 @@ public struct NavigationContainer<Initial: View>: View {
89
106
 
90
107
  @ViewBuilder
91
108
  private func screenView(forDialog route: DynamicDialogRoute) -> some View {
92
- if let screen = DynamicScreenRegistry.shared.getScreen(id: route.id) {
109
+ if let screen = navigator.registry.getScreen(id: route.id) {
93
110
  ScreenHost(screen: screen, navigator: navigator)
94
111
  } else {
95
112
  EmptyView()
@@ -128,6 +145,7 @@ private struct ScreenHost: View {
128
145
  layoutOffset: options.layoutOffset,
129
146
  inputSearchProps: options.inputSearchProps,
130
147
  fabProps: options.fabProps,
148
+ screenName: screen.name,
131
149
  content: { screen.content() }
132
150
  )
133
151
  }
@@ -174,3 +192,9 @@ private func safeAreaBottomInset() -> CGFloat {
174
192
  .compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom }
175
193
  .first ?? 0
176
194
  }
195
+
196
+ private func parseLanguage(_ raw: [String: Any?]?) -> String? {
197
+ guard let response = (raw?["response"] as? String)?.trimmingCharacters(in: .whitespaces).lowercased(),
198
+ !response.isEmpty else { return nil }
199
+ return response.hasPrefix(Localize.EN) ? Localize.EN : Localize.VI
200
+ }
@@ -30,12 +30,10 @@ public struct DynamicScreen {
30
30
  }
31
31
 
32
32
  public final class DynamicScreenRegistry {
33
- public static let shared = DynamicScreenRegistry()
34
-
35
33
  private var screens: [Int: DynamicScreen] = [:]
36
34
  private var idCounter: Int = 1
37
35
 
38
- private init() {}
36
+ public init() {}
39
37
 
40
38
  @discardableResult
41
39
  public func register(
@@ -95,6 +93,11 @@ public final class Navigator: ObservableObject {
95
93
  @Published public var presented: DynamicDialogRoute?
96
94
  @Published public var overplay: OverplayItem?
97
95
 
96
+ /// Owned per-Navigator (mirrors Compose's `remember { DynamicScreenRegistry() }` +
97
+ /// disposal): screens registered by this navigator are released via ARC when the
98
+ /// navigator itself deallocates, instead of accumulating in a process-wide singleton.
99
+ public let registry = DynamicScreenRegistry()
100
+
98
101
  /// Animated-close handler registered by the active overplay component
99
102
  /// (mirrors Compose's `OverplayComponentRegistry.bindClose`). When set, a
100
103
  /// dismiss request runs the component's exit animation before the item is
@@ -111,7 +114,7 @@ public final class Navigator: ObservableObject {
111
114
  composeApi: KitComposeApi? = nil
112
115
  ) {
113
116
  self.composeApi = composeApi
114
- self.rootRoute = DynamicScreenRegistry.shared.register(
117
+ self.rootRoute = registry.register(
115
118
  screenName: initialScreenName,
116
119
  content: initialContent,
117
120
  options: options
@@ -121,7 +124,7 @@ public final class Navigator: ObservableObject {
121
124
  // MARK: Stack
122
125
 
123
126
  public func push<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
124
- let route = DynamicScreenRegistry.shared.register(
127
+ let route = registry.register(
125
128
  screenName: options?.screenName ?? "",
126
129
  content: { AnyView(content()) },
127
130
  options: options
@@ -134,7 +137,7 @@ public final class Navigator: ObservableObject {
134
137
  @ViewBuilder _ content: @escaping () -> V,
135
138
  options: NavigationOptions? = nil
136
139
  ) {
137
- let route = DynamicScreenRegistry.shared.register(
140
+ let route = registry.register(
138
141
  screenName: screenName,
139
142
  content: { AnyView(content()) },
140
143
  options: options
@@ -145,7 +148,7 @@ public final class Navigator: ObservableObject {
145
148
  public func replace<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
146
149
  guard !path.isEmpty else { return }
147
150
  let removed = path.removeLast()
148
- DynamicScreenRegistry.shared.unregister(id: removed.id)
151
+ registry.unregister(id: removed.id)
149
152
  push(content, options: options)
150
153
  }
151
154
 
@@ -156,15 +159,15 @@ public final class Navigator: ObservableObject {
156
159
  ) {
157
160
  guard !path.isEmpty else { return }
158
161
  let removed = path.removeLast()
159
- DynamicScreenRegistry.shared.unregister(id: removed.id)
162
+ registry.unregister(id: removed.id)
160
163
  push(screenName: screenName, content, options: options)
161
164
  }
162
165
 
163
166
  public func reset<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
164
- for route in path { DynamicScreenRegistry.shared.unregister(id: route.id) }
165
- DynamicScreenRegistry.shared.unregister(id: rootRoute.id)
167
+ for route in path { registry.unregister(id: route.id) }
168
+ registry.unregister(id: rootRoute.id)
166
169
  path.removeAll()
167
- let route = DynamicScreenRegistry.shared.register(
170
+ let route = registry.register(
168
171
  screenName: options?.screenName ?? "",
169
172
  content: { AnyView(content()) },
170
173
  options: options
@@ -177,10 +180,10 @@ public final class Navigator: ObservableObject {
177
180
  @ViewBuilder _ content: @escaping () -> V,
178
181
  options: NavigationOptions? = nil
179
182
  ) {
180
- for route in path { DynamicScreenRegistry.shared.unregister(id: route.id) }
181
- DynamicScreenRegistry.shared.unregister(id: rootRoute.id)
183
+ for route in path { registry.unregister(id: route.id) }
184
+ registry.unregister(id: rootRoute.id)
182
185
  path.removeAll()
183
- let route = DynamicScreenRegistry.shared.register(
186
+ let route = registry.register(
184
187
  screenName: screenName,
185
188
  content: { AnyView(content()) },
186
189
  options: options
@@ -191,17 +194,16 @@ public final class Navigator: ObservableObject {
191
194
  public func pop(_ count: Int = 1, callback: (() -> Void)? = nil) {
192
195
  var remaining = count
193
196
  while remaining > 0 {
194
- if overplay != nil {
197
+ if overplay?.type == .snackBar {
198
+ // Mirrors Kotlin: a snackbar doesn't block back-navigation, so popping
199
+ // while one is showing pops the screen underneath and clears the
200
+ // snackbar immediately, without playing its dismiss animation.
201
+ overplayDismissHandler = nil
202
+ overplay = nil
203
+ if !popScreen() { break }
204
+ } else if overplay != nil {
195
205
  hideOverplay()
196
- } else if presented != nil {
197
- let dismissedId = presented?.id
198
- presented = nil
199
- if let id = dismissedId { DynamicScreenRegistry.shared.unregister(id: id) }
200
- } else if !path.isEmpty {
201
- let removed = path.removeLast()
202
- DynamicScreenRegistry.shared.unregister(id: removed.id)
203
- } else {
204
- composeApi?.requestCallback(funcName: "dismiss", params: nil, onResponse: nil)
206
+ } else if !popScreen() {
205
207
  break
206
208
  }
207
209
  remaining -= 1
@@ -209,13 +211,32 @@ public final class Navigator: ObservableObject {
209
211
  callback?()
210
212
  }
211
213
 
214
+ /// Pops the topmost presented screen or stack entry. Returns `false` (and requests
215
+ /// a host dismiss) when there is nothing left to pop.
216
+ @discardableResult
217
+ private func popScreen() -> Bool {
218
+ if presented != nil {
219
+ let dismissedId = presented?.id
220
+ presented = nil
221
+ if let id = dismissedId { registry.unregister(id: id) }
222
+ return true
223
+ } else if !path.isEmpty {
224
+ let removed = path.removeLast()
225
+ registry.unregister(id: removed.id)
226
+ return true
227
+ } else {
228
+ composeApi?.requestCallback(funcName: "dismiss", params: nil, onResponse: nil)
229
+ return false
230
+ }
231
+ }
232
+
212
233
  public func popToRoot() {
213
- for route in path { DynamicScreenRegistry.shared.unregister(id: route.id) }
234
+ for route in path { registry.unregister(id: route.id) }
214
235
  path.removeAll()
215
236
  }
216
237
 
217
238
  public func onBackSafe(callback: (() -> Void)? = nil) {
218
- let latest = DynamicScreenRegistry.shared.latest()
239
+ let latest = registry.latest()
219
240
  if let onBack = latest?.options?.onBackHandler {
220
241
  onBack()
221
242
  return
@@ -256,7 +277,7 @@ public final class Navigator: ObservableObject {
256
277
  // MARK: Modal-style screen (vertical slide)
257
278
 
258
279
  public func present<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
259
- let route = DynamicScreenRegistry.shared.register(
280
+ let route = registry.register(
260
281
  screenName: options?.screenName ?? "",
261
282
  content: { AnyView(content()) },
262
283
  options: options
@@ -269,7 +290,7 @@ public final class Navigator: ObservableObject {
269
290
  @ViewBuilder _ content: @escaping () -> V,
270
291
  options: NavigationOptions? = nil
271
292
  ) {
272
- let route = DynamicScreenRegistry.shared.register(
293
+ let route = registry.register(
273
294
  screenName: screenName,
274
295
  content: { AnyView(content()) },
275
296
  options: options
@@ -279,7 +300,7 @@ public final class Navigator: ObservableObject {
279
300
 
280
301
  public func dismiss() {
281
302
  if let id = presented?.id {
282
- DynamicScreenRegistry.shared.unregister(id: id)
303
+ registry.unregister(id: id)
283
304
  }
284
305
  presented = nil
285
306
  }
@@ -43,42 +43,84 @@ struct BottomSheetView<Content: View>: View {
43
43
  let content: () -> Content
44
44
 
45
45
  @EnvironmentObject private var navigator: Navigator
46
+ @Environment(\.appTheme) private var theme
47
+ @Environment(\.applicationEnvironment) private var appEnv
46
48
 
47
49
  /// Vertical translation of the sheet; starts off-screen and animates to 0.
48
50
  @State private var sheetOffset: CGFloat = UIScreen.main.bounds.height
49
- @State private var scrimAlpha: CGFloat = 0
51
+ /// Mirrors Compose's `backgroundAlpha`: only touched by open()/close(), never by drag.
52
+ @State private var backgroundAlpha: CGFloat = 0
50
53
  @State private var sheetHeight: CGFloat = 0
51
54
  @State private var didAppear = false
55
+ @State private var keyboardHeight: CGFloat = 0
56
+ /// Live drag offset, isolated from `sheetOffset` so a touch-move only invalidates
57
+ /// this gesture-scoped value instead of forcing the whole body (including
58
+ /// `content()`) to reconstruct every frame — mirrors the pattern already used in
59
+ /// `ios/PopupView/PopupView.swift` for smooth drag-to-dismiss.
60
+ @GestureState private var dragTranslation: CGFloat = 0
52
61
 
53
62
  /// Mirrors Compose's `sheetCloseOffset` (100dp drag threshold to dismiss).
54
63
  private let closeThreshold: CGFloat = 100
55
64
 
65
+ /// Mirrors Compose's per-term peak (0.3f). Combined with `backgroundAlpha` the
66
+ /// steady-state (fully open) scrim peaks at 0.6, matching ModalScreen's peak.
67
+ private let maxDynamicAlpha: CGFloat = 0.3
68
+
56
69
  private var screenHeight: CGFloat { UIScreen.main.bounds.height }
57
70
  private var bottomInset: CGFloat { safeAreaBottomInset() }
71
+ /// While the keyboard is up, it already covers the home-indicator area, so it
72
+ /// replaces (rather than adds to) the usual bottom-inset filler.
73
+ private var effectiveBottomInset: CGFloat { keyboardHeight > 0 ? keyboardHeight : bottomInset }
58
74
 
59
75
  private var backgroundColor: Color {
60
- // Compose uses background.surface (white) for `isSurface`, otherwise
61
- // background.default. Both resolve to white in the current iOS palette.
62
- isSurface ? Colors.black01 : Colors.black01
76
+ isSurface ? theme.colors.background.surface : theme.colors.background.default
77
+ }
78
+
79
+ private var effectiveOffset: CGFloat { sheetOffset + dragTranslation }
80
+
81
+ /// Mirrors Compose's `dynamicAlpha`: a plain derived value (not itself animated)
82
+ /// that tracks the current offset live, shrinking toward 0 as the sheet is
83
+ /// dragged down.
84
+ private var dynamicAlpha: CGFloat {
85
+ let denom = sheetHeight > 0 ? sheetHeight : screenHeight
86
+ guard denom > 0 else { return maxDynamicAlpha }
87
+ let progress = min(max(1 - effectiveOffset / denom, 0), 1)
88
+ return progress * maxDynamicAlpha
63
89
  }
64
90
 
91
+ /// Mirrors Compose's `alpha = backgroundAlpha + dynamicAlpha`: additive, so the
92
+ /// scrim peaks at 0.6 at rest and only fades to a 0.3 floor while dragging
93
+ /// (backgroundAlpha holds steady until an explicit close()).
94
+ private var totalScrimAlpha: CGFloat { backgroundAlpha + dynamicAlpha }
95
+
65
96
  var body: some View {
66
97
  ZStack(alignment: .bottom) {
67
- // Dimmed scrim (max 0.3 alpha, mirrors Compose).
68
- Color.black.opacity(scrimAlpha)
69
- .ignoresSafeArea()
98
+ // Dimmed scrim additive backgroundAlpha + dynamicAlpha (see above).
99
+ Color.black.opacity(totalScrimAlpha)
70
100
  .contentShape(Rectangle())
71
101
  .onTapGesture {
72
102
  if barrierDismissible { close() }
73
103
  }
74
104
 
75
105
  sheet
76
- .offset(y: sheetOffset)
106
+ .offset(y: effectiveOffset)
77
107
  }
78
108
  .frame(maxWidth: .infinity, maxHeight: .infinity)
109
+ .ignoresSafeArea()
110
+ .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
111
+ guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
112
+ withAnimation(.easeOut(duration: 0.25)) {
113
+ keyboardHeight = UIScreen.main.bounds.height - endFrame.origin.y
114
+ }
115
+ }
79
116
  .onAppear {
80
117
  guard !didAppear else { return }
81
118
  didAppear = true
119
+ ScreenTracker.trackPopupDisplayed(
120
+ maxApi: appEnv.maxApi,
121
+ context: appEnv.applicationContext,
122
+ parentScreenName: ScreenTracker.getLastScreenName()
123
+ )
82
124
  navigator.registerOverplayDismiss { close() }
83
125
  open()
84
126
  }
@@ -103,16 +145,16 @@ struct BottomSheetView<Content: View>: View {
103
145
  .gesture(dragGesture)
104
146
 
105
147
  Rectangle()
106
- .fill(Colors.black04)
148
+ .fill(theme.colors.border.default)
107
149
  .frame(height: 1)
108
150
 
109
151
  content()
110
152
 
111
153
  backgroundColor
112
- .frame(height: bottomInset)
154
+ .frame(height: effectiveBottomInset)
113
155
  }
114
156
  .frame(maxWidth: .infinity)
115
- .frame(maxHeight: screenHeight - safeAreaTopInset() - 90, alignment: .bottom)
157
+ .frame(alignment: .bottom)
116
158
  .background(
117
159
  GeometryReader { geo in
118
160
  Color.clear.onAppear { sheetHeight = geo.size.height }
@@ -143,13 +185,33 @@ struct BottomSheetView<Content: View>: View {
143
185
  // MARK: - Drag handling (mirrors Compose's detectDragGestures)
144
186
 
145
187
  private var dragGesture: some Gesture {
146
- DragGesture()
147
- .onChanged { value in
148
- let newOffset = max(value.translation.height, 0)
149
- sheetOffset = newOffset
188
+ // `.global` (not the default `.local`) so the gesture tracks against the
189
+ // window rather than the handle's own view, which is itself repositioned
190
+ // every frame by this same gesture's output (`sheet.offset(y: effectiveOffset)`
191
+ // in `body`) — the local coordinate space chasing its own moving reference
192
+ // frame is a plausible source of a continuous drag-time jitter.
193
+ DragGesture(coordinateSpace: .global)
194
+ .onChanged { _ in
195
+ // Touching the handle should immediately settle `sheetOffset` to its
196
+ // resting value (0) with no animation, so a drag that starts while the
197
+ // open animation — or the keyboard-driven resize, which shifts this
198
+ // same bottom-anchored top edge — is still in flight doesn't fight a
199
+ // moving target. Without this, the live drag value chases an animated
200
+ // one, producing a couple of corrective jumps before it settles.
201
+ if sheetOffset != 0 {
202
+ sheetOffset = 0
203
+ }
204
+ }
205
+ .updating($dragTranslation) { value, state, _ in
206
+ state = max(value.translation.height, 0)
150
207
  }
151
- .onEnded { _ in
152
- if sheetOffset > closeThreshold {
208
+ .onEnded { value in
209
+ let finalOffset = max(value.translation.height, 0)
210
+ // Capture into persisted state BEFORE @GestureState resets to 0,
211
+ // avoiding a one-frame snap back to rest before close()'s own
212
+ // slide-out animation starts (mirrors PopupView's `lastDragPosition`).
213
+ sheetOffset = finalOffset
214
+ if finalOffset > closeThreshold {
153
215
  close()
154
216
  } else {
155
217
  withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
@@ -163,17 +225,23 @@ struct BottomSheetView<Content: View>: View {
163
225
 
164
226
  private func open() {
165
227
  DispatchQueue.main.async {
166
- withAnimation(.easeOut(duration: 0.1)) { scrimAlpha = 0.3 }
228
+ withAnimation(.timingCurve(0.05, 0.7, 0.1, 1, duration: 0.1)) { backgroundAlpha = maxDynamicAlpha }
167
229
  withAnimation(.timingCurve(0.05, 0.7, 0.1, 1, duration: 0.35)) { sheetOffset = 0 }
168
230
  }
169
231
  }
170
232
 
233
+ /// Mirrors Compose's `closeEvent`: slide out over 200ms, then fade the scrim
234
+ /// out over 100ms only after the slide finishes, then dismiss.
171
235
  private func close() {
172
236
  withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.2)) {
173
237
  sheetOffset = max(sheetHeight, screenHeight)
174
- scrimAlpha = 0
175
238
  }
176
239
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
240
+ withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.1)) {
241
+ backgroundAlpha = 0
242
+ }
243
+ }
244
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
177
245
  navigator.finishOverplayDismiss()
178
246
  }
179
247
  }
@@ -15,6 +15,7 @@ struct ModalScreen<Content: View>: View {
15
15
  let content: () -> Content
16
16
 
17
17
  @EnvironmentObject private var navigator: Navigator
18
+ @Environment(\.applicationEnvironment) private var appEnv
18
19
 
19
20
  @State private var scrimOpacity: CGFloat = 0
20
21
  @State private var contentScale: CGFloat = 0.8
@@ -38,6 +39,11 @@ struct ModalScreen<Content: View>: View {
38
39
  .onAppear {
39
40
  guard !didAppear else { return }
40
41
  didAppear = true
42
+ ScreenTracker.trackPopupDisplayed(
43
+ maxApi: appEnv.maxApi,
44
+ context: appEnv.applicationContext,
45
+ parentScreenName: ScreenTracker.getLastScreenName()
46
+ )
41
47
  navigator.registerOverplayDismiss { close() }
42
48
  open()
43
49
  }
@@ -0,0 +1,117 @@
1
+ //
2
+ // ScreenTracker.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/tracking/ScreenTracker.kt`. Pure
6
+ // logic, no platform dependency beyond the `MaxApi` bridge.
7
+ //
8
+
9
+ import Foundation
10
+
11
+ public final class ScreenTrackingState {
12
+ public var time: Double = Date().timeIntervalSince1970 * 1000
13
+ public var releaseNavigated: Bool = false
14
+ public var releaseLoad: Bool = false
15
+ public var releaseInteraction: Bool = false
16
+ public var previousScreenName: String?
17
+ public var action: String = "push"
18
+
19
+ public init() {}
20
+ }
21
+
22
+ public enum ScreenTracker {
23
+ private static var lastScreenName: String?
24
+
25
+ public static func trackScreenNavigated(
26
+ maxApi: MaxApi?,
27
+ context: MiniAppContext?,
28
+ screenName: String,
29
+ action: String,
30
+ state: ScreenTrackingState
31
+ ) {
32
+ guard !state.releaseNavigated else { return }
33
+
34
+ let previousScreen = lastScreenName
35
+ lastScreenName = screenName
36
+
37
+ var params = buildBaseParams(context: context, screenName: screenName)
38
+ params["pre_screen_name"] = previousScreen
39
+ params["state"] = "navigated"
40
+ params["action"] = action
41
+ maxApi?.track(eventName: "auto_screen_navigated", params: params)
42
+
43
+ state.releaseNavigated = true
44
+ state.previousScreenName = previousScreen
45
+ }
46
+
47
+ public static func trackScreenDisplayed(
48
+ maxApi: MaxApi?,
49
+ context: MiniAppContext?,
50
+ screenName: String,
51
+ duration: Double,
52
+ state: ScreenTrackingState
53
+ ) {
54
+ guard !state.releaseLoad else { return }
55
+
56
+ var params = buildBaseParams(context: context, screenName: screenName)
57
+ params["state"] = "load"
58
+ params["duration"] = duration
59
+ maxApi?.track(eventName: "auto_screen_displayed", params: params)
60
+
61
+ state.releaseLoad = true
62
+ }
63
+
64
+ public static func trackScreenInteracted(
65
+ maxApi: MaxApi?,
66
+ context: MiniAppContext?,
67
+ screenName: String,
68
+ duration: Double,
69
+ totalDuration: Double,
70
+ state: ScreenTrackingState
71
+ ) {
72
+ guard !state.releaseInteraction else { return }
73
+
74
+ var params = buildBaseParams(context: context, screenName: screenName)
75
+ params["state"] = "interaction"
76
+ params["duration"] = duration
77
+ params["total_duration"] = totalDuration
78
+ maxApi?.track(eventName: "auto_screen_interacted", params: params)
79
+
80
+ state.releaseInteraction = true
81
+ }
82
+
83
+ public static func trackPopupDisplayed(
84
+ maxApi: MaxApi?,
85
+ context: MiniAppContext?,
86
+ parentScreenName: String?
87
+ ) {
88
+ maxApi?.track(eventName: "auto_popup_displayed", params: [
89
+ "screen_name": parentScreenName,
90
+ "tracking_source": 1,
91
+ "app_id": context?.appId,
92
+ "feature_code": context?.appCode,
93
+ "kits_version": nil,
94
+ "component_name": "Modal"
95
+ ])
96
+ }
97
+
98
+ private static func buildBaseParams(
99
+ context: MiniAppContext?,
100
+ screenName: String,
101
+ componentName: String = "Screen"
102
+ ) -> [String: Any?] {
103
+ [
104
+ "app_id": context?.appId,
105
+ "feature_code": context?.appCode,
106
+ "screen_name": screenName,
107
+ "component_name": componentName,
108
+ "tracking_source": 1
109
+ ]
110
+ }
111
+
112
+ public static func getLastScreenName() -> String? { lastScreenName }
113
+
114
+ public static func reset() {
115
+ lastScreenName = nil
116
+ }
117
+ }
@@ -237,7 +237,7 @@ struct Footer: View {
237
237
  }
238
238
  .padding(.vertical, Spacing.S)
239
239
  .padding(.horizontal, Spacing.M)
240
- .padding(.bottom, keyboardSize + bottomInset)
240
+ .padding(.bottom, keyboardSize - bottomInset)
241
241
  .background(Colors.black01)
242
242
  .overlay(
243
243
  Rectangle()