@momo-kits/native-kits 0.162.2-sp.1-debug → 0.162.2-sp.10-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 (198) hide show
  1. package/.claude/settings.local.json +15 -1
  2. package/CLAUDE.md +78 -0
  3. package/compose/build.gradle.kts +12 -8
  4. package/compose/build.gradle.kts.backup +11 -7
  5. package/compose/compose.podspec +1 -1
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +2 -7
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +8 -11
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +6 -24
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +4 -19
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +1 -10
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +2 -2
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +5 -16
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +6 -101
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +8 -19
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +8 -33
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +1 -1
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +22 -25
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +27 -29
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +3 -26
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +30 -31
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +3 -13
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -5
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +3 -11
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +3 -18
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +11 -11
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +42 -65
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +69 -26
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -5
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +84 -59
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +13 -71
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +10 -51
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +6 -1
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +4 -21
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +1 -1
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +16 -75
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +5 -26
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +1 -1
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +3 -11
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +16 -40
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +3 -9
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -28
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +2 -13
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +4 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +4 -10
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +3 -28
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +8 -47
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +4 -25
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +2 -11
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +1 -17
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +1 -6
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +50 -111
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +1 -5
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +1 -5
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +1 -12
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +4 -96
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +5 -7
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +2 -12
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +9 -21
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +2 -2
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +9 -14
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +8 -11
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +1 -1
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +11 -20
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -1
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -5
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -2
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +94 -4
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -2
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +2 -2
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +3 -7
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +12 -16
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +120 -93
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +48 -33
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +88 -63
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +9 -14
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +13 -31
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +1 -1
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +6 -24
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +11 -6
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +2 -1
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +9 -6
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +1 -1
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +3 -13
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +3 -2
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +1 -40
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -2
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -8
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +2 -17
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +1 -1
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +3 -3
  99. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +2 -3
  100. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  101. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -2
  102. package/gradle/libs.versions.toml +2 -0
  103. package/gradle.properties +1 -1
  104. package/ios/Application/ApplicationEnvironment.swift +15 -2
  105. package/ios/Application/Components.swift +16 -0
  106. package/ios/Application/HeaderRight.swift +79 -45
  107. package/ios/Application/Localize.swift +277 -0
  108. package/ios/Application/MaxApi.swift +18 -0
  109. package/ios/Application/Navigation/BottomTab/BottomTab.swift +3 -1
  110. package/ios/Application/Navigation/NavigationContainer.swift +53 -4
  111. package/ios/Application/Navigation/Navigator.swift +54 -29
  112. package/ios/Application/Navigation/Overplay/BottomSheet.swift +98 -18
  113. package/ios/Application/Navigation/Overplay/ModalScreen.swift +6 -0
  114. package/ios/Application/Navigation/Tracking/ScreenTracker.swift +117 -0
  115. package/ios/Application/Screen.swift +2 -1
  116. package/ios/Application/StackScreen.swift +55 -3
  117. package/ios/Badge/BadgeRibbon.swift +80 -0
  118. package/ios/Button/Button.swift +67 -11
  119. package/ios/Carousel/Carousel.swift +16 -1
  120. package/ios/Checkbox/Checkbox.swift +14 -20
  121. package/ios/Chip/Chip.swift +35 -26
  122. package/ios/Collapse/Collapse.swift +11 -1
  123. package/ios/Colors+Radius+Spacing/Colors.swift +20 -17
  124. package/ios/Colors+Radius+Spacing/Spacing.swift +3 -1
  125. package/ios/Colors+Radius+Spacing/Theme.swift +15 -15
  126. package/ios/DateTimePicker/DateTimePicker.swift +4 -2
  127. package/ios/DateTimePicker/WheelPicker.swift +7 -3
  128. package/ios/Extensions/ActiveOpacityButtonStyle.swift +14 -0
  129. package/ios/Extensions/Color++.swift +32 -0
  130. package/ios/Icon/Icon.swift +5 -1
  131. package/ios/IconButton/IconButton.swift +35 -7
  132. package/ios/Image/Image.swift +16 -3
  133. package/ios/Information/Information.swift +3 -1
  134. package/ios/Input/ErrorView.swift +3 -1
  135. package/ios/Input/Input.swift +31 -8
  136. package/ios/Input/InputPhoneNumber.swift +14 -3
  137. package/ios/Input/InputSearch.swift +7 -1
  138. package/ios/Input/InputTextArea.swift +78 -34
  139. package/ios/InputDropDown/InputDropDown.swift +4 -1
  140. package/ios/InputMoney/InputMoney.swift +16 -4
  141. package/ios/Popup/PopupDisplay.swift +3 -4
  142. package/ios/Popup/PopupNotify.swift +3 -4
  143. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  144. package/ios/Radio/Radio.swift +12 -13
  145. package/ios/Rating/Rating.swift +1 -1
  146. package/ios/Resources/MoMoUIKitsColors.xcassets/AccentColor.colorset/Contents.json +11 -0
  147. package/ios/Resources/MoMoUIKitsColors.xcassets/Background.colorset/Contents.json +38 -0
  148. package/ios/Resources/MoMoUIKitsColors.xcassets/Border.colorset/Contents.json +38 -0
  149. package/ios/Resources/MoMoUIKitsColors.xcassets/Card.colorset/Contents.json +38 -0
  150. package/ios/Resources/MoMoUIKitsColors.xcassets/Contents.json +6 -0
  151. package/ios/Resources/MoMoUIKitsColors.xcassets/Error.colorset/Contents.json +38 -0
  152. package/ios/Resources/MoMoUIKitsColors.xcassets/Primary.colorset/Contents.json +38 -0
  153. package/ios/Resources/MoMoUIKitsColors.xcassets/PrimaryDark.colorset/Contents.json +38 -0
  154. package/ios/Resources/MoMoUIKitsColors.xcassets/PrimaryLight.colorset/Contents.json +38 -0
  155. package/ios/Resources/MoMoUIKitsColors.xcassets/Secondary.colorset/Contents.json +38 -0
  156. package/ios/Resources/MoMoUIKitsColors.xcassets/Success.colorset/Contents.json +38 -0
  157. package/ios/Resources/MoMoUIKitsColors.xcassets/Text.colorset/Contents.json +38 -0
  158. package/ios/Resources/MoMoUIKitsColors.xcassets/TextSecondary.colorset/Contents.json +38 -0
  159. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/Contents.json +9 -0
  160. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-default.colorset/Contents.json +38 -0
  161. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-disable.colorset/Contents.json +38 -0
  162. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-pressed.colorset/Contents.json +38 -0
  163. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-selected.colorset/Contents.json +38 -0
  164. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-surface.colorset/Contents.json +38 -0
  165. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-tonal.colorset/Contents.json +38 -0
  166. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/border-default.colorset/Contents.json +38 -0
  167. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/border-disable.colorset/Contents.json +38 -0
  168. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/gradient.colorset/Contents.json +38 -0
  169. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/primary.colorset/Contents.json +38 -0
  170. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/secondary.colorset/Contents.json +38 -0
  171. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-container.colorset/Contents.json +38 -0
  172. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-primary.colorset/Contents.json +38 -0
  173. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-secondary.colorset/Contents.json +38 -0
  174. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-default.colorset/Contents.json +38 -0
  175. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-disable.colorset/Contents.json +38 -0
  176. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-hint.colorset/Contents.json +38 -0
  177. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-secondary.colorset/Contents.json +38 -0
  178. package/ios/Resources/MoMoUIKitsColors.xcassets/Warning.colorset/Contents.json +38 -0
  179. package/ios/Stepper/Stepper.swift +2 -2
  180. package/ios/Steps/Steps.swift +22 -6
  181. package/ios/SuggestAction/SuggestAction.swift +5 -3
  182. package/ios/Swipeable/SwipeCell.swift +5 -3
  183. package/ios/Switch/Switch.swift +53 -28
  184. package/ios/TabView/TabView.swift +18 -14
  185. package/ios/Template/TrustBanner/TrustBanner.swift +6 -7
  186. package/ios/Tooltip/Tooltip.swift +3 -0
  187. package/ios/Typography/FontScaleStore.swift +19 -0
  188. package/ios/Typography/Text.swift +3 -0
  189. package/ios/Typography/Typography.swift +8 -5
  190. package/ios/Uploader/Uploader.swift +0 -9
  191. package/ios/native-kits.podspec +6 -3
  192. package/ios-demo/MoMoUIKitsDemo.podspec +5 -3
  193. package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoFactory.swift +9 -57
  194. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +21 -3
  195. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LocalizeDemo.swift +188 -0
  196. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +2 -0
  197. package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +5 -4
  198. package/package.json +1 -1
@@ -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
@@ -102,13 +105,16 @@ public final class Navigator: ObservableObject {
102
105
  private var overplayDismissHandler: (() -> Void)?
103
106
 
104
107
  public private(set) var rootRoute: DynamicScreenRoute
108
+ public var composeApi: KitComposeApi?
105
109
 
106
110
  public init(
107
111
  initialScreenName: String = "",
108
112
  initialContent: @escaping () -> AnyView,
109
- options: NavigationOptions? = nil
113
+ options: NavigationOptions? = nil,
114
+ composeApi: KitComposeApi? = nil
110
115
  ) {
111
- self.rootRoute = DynamicScreenRegistry.shared.register(
116
+ self.composeApi = composeApi
117
+ self.rootRoute = registry.register(
112
118
  screenName: initialScreenName,
113
119
  content: initialContent,
114
120
  options: options
@@ -118,7 +124,7 @@ public final class Navigator: ObservableObject {
118
124
  // MARK: Stack
119
125
 
120
126
  public func push<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
121
- let route = DynamicScreenRegistry.shared.register(
127
+ let route = registry.register(
122
128
  screenName: options?.screenName ?? "",
123
129
  content: { AnyView(content()) },
124
130
  options: options
@@ -131,7 +137,7 @@ public final class Navigator: ObservableObject {
131
137
  @ViewBuilder _ content: @escaping () -> V,
132
138
  options: NavigationOptions? = nil
133
139
  ) {
134
- let route = DynamicScreenRegistry.shared.register(
140
+ let route = registry.register(
135
141
  screenName: screenName,
136
142
  content: { AnyView(content()) },
137
143
  options: options
@@ -142,7 +148,7 @@ public final class Navigator: ObservableObject {
142
148
  public func replace<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
143
149
  guard !path.isEmpty else { return }
144
150
  let removed = path.removeLast()
145
- DynamicScreenRegistry.shared.unregister(id: removed.id)
151
+ registry.unregister(id: removed.id)
146
152
  push(content, options: options)
147
153
  }
148
154
 
@@ -153,15 +159,15 @@ public final class Navigator: ObservableObject {
153
159
  ) {
154
160
  guard !path.isEmpty else { return }
155
161
  let removed = path.removeLast()
156
- DynamicScreenRegistry.shared.unregister(id: removed.id)
162
+ registry.unregister(id: removed.id)
157
163
  push(screenName: screenName, content, options: options)
158
164
  }
159
165
 
160
166
  public func reset<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
161
- for route in path { DynamicScreenRegistry.shared.unregister(id: route.id) }
162
- DynamicScreenRegistry.shared.unregister(id: rootRoute.id)
167
+ for route in path { registry.unregister(id: route.id) }
168
+ registry.unregister(id: rootRoute.id)
163
169
  path.removeAll()
164
- let route = DynamicScreenRegistry.shared.register(
170
+ let route = registry.register(
165
171
  screenName: options?.screenName ?? "",
166
172
  content: { AnyView(content()) },
167
173
  options: options
@@ -174,10 +180,10 @@ public final class Navigator: ObservableObject {
174
180
  @ViewBuilder _ content: @escaping () -> V,
175
181
  options: NavigationOptions? = nil
176
182
  ) {
177
- for route in path { DynamicScreenRegistry.shared.unregister(id: route.id) }
178
- DynamicScreenRegistry.shared.unregister(id: rootRoute.id)
183
+ for route in path { registry.unregister(id: route.id) }
184
+ registry.unregister(id: rootRoute.id)
179
185
  path.removeAll()
180
- let route = DynamicScreenRegistry.shared.register(
186
+ let route = registry.register(
181
187
  screenName: screenName,
182
188
  content: { AnyView(content()) },
183
189
  options: options
@@ -188,16 +194,16 @@ public final class Navigator: ObservableObject {
188
194
  public func pop(_ count: Int = 1, callback: (() -> Void)? = nil) {
189
195
  var remaining = count
190
196
  while remaining > 0 {
191
- 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 {
192
205
  hideOverplay()
193
- } else if presented != nil {
194
- let dismissedId = presented?.id
195
- presented = nil
196
- if let id = dismissedId { DynamicScreenRegistry.shared.unregister(id: id) }
197
- } else if !path.isEmpty {
198
- let removed = path.removeLast()
199
- DynamicScreenRegistry.shared.unregister(id: removed.id)
200
- } else {
206
+ } else if !popScreen() {
201
207
  break
202
208
  }
203
209
  remaining -= 1
@@ -205,13 +211,32 @@ public final class Navigator: ObservableObject {
205
211
  callback?()
206
212
  }
207
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
+
208
233
  public func popToRoot() {
209
- for route in path { DynamicScreenRegistry.shared.unregister(id: route.id) }
234
+ for route in path { registry.unregister(id: route.id) }
210
235
  path.removeAll()
211
236
  }
212
237
 
213
238
  public func onBackSafe(callback: (() -> Void)? = nil) {
214
- let latest = DynamicScreenRegistry.shared.latest()
239
+ let latest = registry.latest()
215
240
  if let onBack = latest?.options?.onBackHandler {
216
241
  onBack()
217
242
  return
@@ -252,7 +277,7 @@ public final class Navigator: ObservableObject {
252
277
  // MARK: Modal-style screen (vertical slide)
253
278
 
254
279
  public func present<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
255
- let route = DynamicScreenRegistry.shared.register(
280
+ let route = registry.register(
256
281
  screenName: options?.screenName ?? "",
257
282
  content: { AnyView(content()) },
258
283
  options: options
@@ -265,7 +290,7 @@ public final class Navigator: ObservableObject {
265
290
  @ViewBuilder _ content: @escaping () -> V,
266
291
  options: NavigationOptions? = nil
267
292
  ) {
268
- let route = DynamicScreenRegistry.shared.register(
293
+ let route = registry.register(
269
294
  screenName: screenName,
270
295
  content: { AnyView(content()) },
271
296
  options: options
@@ -275,7 +300,7 @@ public final class Navigator: ObservableObject {
275
300
 
276
301
  public func dismiss() {
277
302
  if let id = presented?.id {
278
- DynamicScreenRegistry.shared.unregister(id: id)
303
+ registry.unregister(id: id)
279
304
  }
280
305
  presented = nil
281
306
  }
@@ -43,41 +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 }
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 }
57
74
 
58
75
  private var backgroundColor: Color {
59
- // Compose uses background.surface (white) for `isSurface`, otherwise
60
- // background.default. Both resolve to white in the current iOS palette.
61
- 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
62
89
  }
63
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
+
64
96
  var body: some View {
65
97
  ZStack(alignment: .bottom) {
66
- // Dimmed scrim (max 0.3 alpha, mirrors Compose).
67
- Color.black.opacity(scrimAlpha)
68
- .ignoresSafeArea()
98
+ // Dimmed scrim additive backgroundAlpha + dynamicAlpha (see above).
99
+ Color.black.opacity(totalScrimAlpha)
69
100
  .contentShape(Rectangle())
70
101
  .onTapGesture {
71
102
  if barrierDismissible { close() }
72
103
  }
73
104
 
74
105
  sheet
75
- .offset(y: sheetOffset)
106
+ .offset(y: effectiveOffset)
76
107
  }
77
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
+ }
78
116
  .onAppear {
79
117
  guard !didAppear else { return }
80
118
  didAppear = true
119
+ ScreenTracker.trackPopupDisplayed(
120
+ maxApi: appEnv.maxApi,
121
+ context: appEnv.applicationContext,
122
+ parentScreenName: ScreenTracker.getLastScreenName()
123
+ )
81
124
  navigator.registerOverplayDismiss { close() }
82
125
  open()
83
126
  }
@@ -102,13 +145,16 @@ struct BottomSheetView<Content: View>: View {
102
145
  .gesture(dragGesture)
103
146
 
104
147
  Rectangle()
105
- .fill(Colors.black04)
148
+ .fill(theme.colors.border.default)
106
149
  .frame(height: 1)
107
150
 
108
151
  content()
152
+
153
+ backgroundColor
154
+ .frame(height: effectiveBottomInset)
109
155
  }
110
156
  .frame(maxWidth: .infinity)
111
- .frame(maxHeight: screenHeight - safeAreaTopInset() - 90, alignment: .bottom)
157
+ .frame(alignment: .bottom)
112
158
  .background(
113
159
  GeometryReader { geo in
114
160
  Color.clear.onAppear { sheetHeight = geo.size.height }
@@ -139,13 +185,33 @@ struct BottomSheetView<Content: View>: View {
139
185
  // MARK: - Drag handling (mirrors Compose's detectDragGestures)
140
186
 
141
187
  private var dragGesture: some Gesture {
142
- DragGesture()
143
- .onChanged { value in
144
- let newOffset = max(value.translation.height, 0)
145
- 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)
146
207
  }
147
- .onEnded { _ in
148
- 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 {
149
215
  close()
150
216
  } else {
151
217
  withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
@@ -159,18 +225,32 @@ struct BottomSheetView<Content: View>: View {
159
225
 
160
226
  private func open() {
161
227
  DispatchQueue.main.async {
162
- withAnimation(.easeOut(duration: 0.1)) { scrimAlpha = 0.3 }
228
+ withAnimation(.timingCurve(0.05, 0.7, 0.1, 1, duration: 0.1)) { backgroundAlpha = maxDynamicAlpha }
163
229
  withAnimation(.timingCurve(0.05, 0.7, 0.1, 1, duration: 0.35)) { sheetOffset = 0 }
164
230
  }
165
231
  }
166
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.
167
235
  private func close() {
168
236
  withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.2)) {
169
237
  sheetOffset = max(sheetHeight, screenHeight)
170
- scrimAlpha = 0
171
238
  }
172
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) {
173
245
  navigator.finishOverplayDismiss()
174
246
  }
175
247
  }
176
248
  }
249
+
250
+ @available(iOS 16.0, *)
251
+ private func safeAreaBottomInset() -> CGFloat {
252
+ let bottomInset = UIApplication.shared.connectedScenes
253
+ .compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom }
254
+ .first ?? 0
255
+ return min(bottomInset, 21)
256
+ }
@@ -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
+ }
@@ -229,6 +229,7 @@ public struct Screen<Content: View>: View {
229
229
  struct Footer: View {
230
230
  var footerView: AnyView
231
231
  var keyboardSize: CGFloat
232
+ var bottomInset: CGFloat = 0
232
233
 
233
234
  var body: some View {
234
235
  VStack {
@@ -236,7 +237,7 @@ struct Footer: View {
236
237
  }
237
238
  .padding(.vertical, Spacing.S)
238
239
  .padding(.horizontal, Spacing.M)
239
- .padding(.bottom, keyboardSize)
240
+ .padding(.bottom, keyboardSize - bottomInset)
240
241
  .background(Colors.black01)
241
242
  .overlay(
242
243
  Rectangle()
@@ -39,6 +39,7 @@ public struct StackScreen<Content: View>: View {
39
39
  var layoutOffset: CGFloat
40
40
  var inputSearchProps: InputSearchProps?
41
41
  var fabProps: FabProps?
42
+ var screenName: String = ""
42
43
  var content: () -> Content
43
44
 
44
45
  public init(
@@ -66,6 +67,7 @@ public struct StackScreen<Content: View>: View {
66
67
  layoutOffset: CGFloat = 56,
67
68
  inputSearchProps: InputSearchProps? = nil,
68
69
  fabProps: FabProps? = nil,
70
+ screenName: String = "",
69
71
  @ViewBuilder content: @escaping () -> Content
70
72
  ) {
71
73
  self.backgroundColor = backgroundColor
@@ -92,12 +94,21 @@ public struct StackScreen<Content: View>: View {
92
94
  self.layoutOffset = layoutOffset
93
95
  self.inputSearchProps = inputSearchProps
94
96
  self.fabProps = fabProps
97
+ self.screenName = screenName
95
98
  self.content = content
96
99
  }
97
100
 
98
101
  @State private var scrollOffset: CGFloat = 0
99
102
  @State private var keyboardHeight: CGFloat = 0
100
103
  @State private var headerRightWidth: CGFloat = 0
104
+ @State private var isBottomTabRoot: Bool = false
105
+
106
+ @Environment(\.applicationEnvironment) private var appEnv
107
+
108
+ // Auto-tracking state (mirrors Compose's `remember { ScreenTrackingState() }` +
109
+ // the once-computed push/back `action`).
110
+ @State private var trackingState = ScreenTrackingState()
111
+ @State private var trackingAction: String = ScreenTracker.getLastScreenName() != nil ? "push" : "back"
101
112
 
102
113
  /// Header background fade tied to scroll: 0 → transparent (over a hero image
103
114
  /// or transparent header), 1 → opaque surface with the drop shadow.
@@ -132,8 +143,13 @@ public struct StackScreen<Content: View>: View {
132
143
  // MARK: - Body
133
144
 
134
145
  public var body: some View {
135
- GeometryReader { _ in
146
+ GeometryReader { geometry in
136
147
  let keyboardSize: CGFloat = useAvoidKeyboard ? max(keyboardHeight, 0) : 0
148
+ let bottomInset = min(geometry.safeAreaInsets.bottom, 21)
149
+ // Skip this screen's own bottom-inset reservation when hosting a footer
150
+ // OR a self-contained BottomTab surface (mirrors Compose's
151
+ // `if (options.footerComponent != null || isBottomTab) 0.dp else navigationBar`).
152
+ let contentBottomInset = (footer == nil && !isBottomTabRoot) ? bottomInset : 0
137
153
 
138
154
  ZStack(alignment: .top) {
139
155
  // Background color
@@ -145,6 +161,7 @@ public struct StackScreen<Content: View>: View {
145
161
  VStack(alignment: horizontalAlignment, spacing: 0) {
146
162
  headerAndContent
147
163
  }
164
+ .padding(.bottom, contentBottomInset)
148
165
 
149
166
  FloatingContent(
150
167
  fabProps: fabProps,
@@ -153,7 +170,7 @@ public struct StackScreen<Content: View>: View {
153
170
  )
154
171
  }
155
172
 
156
- FooterContent(footer: footer, keyboardSize: keyboardSize)
173
+ FooterContent(footer: footer, keyboardSize: keyboardSize, bottomInset: bottomInset)
157
174
  }
158
175
 
159
176
  // Animated search bar overlay (mirrors Compose's SearchAnimated layer).
@@ -172,6 +189,9 @@ public struct StackScreen<Content: View>: View {
172
189
  .onPreferenceChange(HeaderRightWidthKey.self) { width in
173
190
  if headerRightWidth != width { headerRightWidth = width }
174
191
  }
192
+ .onPreferenceChange(IsBottomTabRootKey.self) { isRoot in
193
+ if isBottomTabRoot != isRoot { isBottomTabRoot = isRoot }
194
+ }
175
195
  .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
176
196
  guard useAvoidKeyboard,
177
197
  let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
@@ -183,6 +203,37 @@ public struct StackScreen<Content: View>: View {
183
203
  // taps still reach buttons/fields while also dismissing the keyboard.
184
204
  .modifier(HideKeyboardOnTap(enabled: keyboardShouldPersistTaps))
185
205
  }
206
+ .task(id: screenName) {
207
+ ScreenTracker.trackScreenNavigated(
208
+ maxApi: appEnv.maxApi,
209
+ context: appEnv.applicationContext,
210
+ screenName: screenName,
211
+ action: trackingAction,
212
+ state: trackingState
213
+ )
214
+ do {
215
+ try await Task.sleep(nanoseconds: 2_000_000_000)
216
+ } catch {
217
+ return
218
+ }
219
+ let loadTime = Date().timeIntervalSince1970 * 1000 - trackingState.time
220
+ ScreenTracker.trackScreenDisplayed(
221
+ maxApi: appEnv.maxApi,
222
+ context: appEnv.applicationContext,
223
+ screenName: screenName,
224
+ duration: loadTime,
225
+ state: trackingState
226
+ )
227
+ let interactionTime = Date().timeIntervalSince1970 * 1000 - trackingState.time
228
+ ScreenTracker.trackScreenInteracted(
229
+ maxApi: appEnv.maxApi,
230
+ context: appEnv.applicationContext,
231
+ screenName: screenName,
232
+ duration: interactionTime - loadTime,
233
+ totalDuration: interactionTime,
234
+ state: trackingState
235
+ )
236
+ }
186
237
  }
187
238
 
188
239
  // MARK: - Header + content layer
@@ -440,10 +491,11 @@ private struct HideKeyboardOnTap: ViewModifier {
440
491
  private struct FooterContent: View {
441
492
  let footer: (() -> AnyView)?
442
493
  let keyboardSize: CGFloat
494
+ let bottomInset: CGFloat
443
495
 
444
496
  var body: some View {
445
497
  if let footerView = footer?() {
446
- Footer(footerView: footerView, keyboardSize: keyboardSize)
498
+ Footer(footerView: footerView, keyboardSize: keyboardSize, bottomInset: bottomInset)
447
499
  }
448
500
  }
449
501
  }