@momo-kits/native-kits 0.167.1-duo.1-debug → 0.167.1-duo.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 (177) hide show
  1. package/CLAUDE.md +32 -0
  2. package/compose/build.gradle.kts +15 -1
  3. package/compose/build.gradle.kts.backup +14 -0
  4. package/compose/compose.podspec +1 -1
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +1 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +4 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +143 -31
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +100 -66
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +22 -16
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +66 -18
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +23 -14
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +81 -24
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +4 -1
  56. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  57. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
  58. package/gradle.properties +1 -1
  59. package/ios/Application/ApplicationEnvironment.swift +17 -0
  60. package/ios/Application/ComposeApi.swift +2 -1
  61. package/ios/Application/{Components.swift → LegacyHeader.swift} +7 -65
  62. package/ios/Application/Screen.swift +3 -2
  63. package/ios/Avatar/Avatar.swift +4 -3
  64. package/ios/Badge/Badge.swift +6 -4
  65. package/ios/Badge/BadgeDot.swift +1 -1
  66. package/ios/Badge/BadgeRibbon.swift +0 -1
  67. package/ios/Button/Button.swift +15 -12
  68. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  69. package/ios/Carousel/Carousel.swift +29 -12
  70. package/ios/Checkbox/Checkbox.swift +7 -3
  71. package/ios/Chip/Chip.swift +5 -4
  72. package/ios/Collapse/Collapse.swift +70 -19
  73. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  74. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  75. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  76. package/ios/Divider/Divider.swift +37 -4
  77. package/ios/Extensions/Color++.swift +38 -9
  78. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  79. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  80. package/ios/Icon/Icon.swift +21 -23
  81. package/ios/Icon/Icons.swift +1329 -0
  82. package/ios/IconButton/IconButton.swift +15 -7
  83. package/ios/Image/Image.swift +7 -23
  84. package/ios/Information/Information.swift +27 -9
  85. package/ios/Input/ErrorView.swift +2 -2
  86. package/ios/Input/Input.swift +160 -147
  87. package/ios/Input/InputPhoneNumber.swift +12 -9
  88. package/ios/Input/InputSearch.swift +102 -75
  89. package/ios/Input/InputTextArea.swift +128 -145
  90. package/ios/InputDropDown/InputDropDown.swift +10 -7
  91. package/ios/InputMoney/InputMoney.swift +33 -11
  92. package/ios/InputOTP/InputOTP.swift +22 -12
  93. package/ios/Layout/Card.swift +50 -0
  94. package/ios/Layout/GridContext.swift +123 -3
  95. package/ios/Layout/Item.swift +5 -4
  96. package/ios/Layout/ScreenLayout.swift +312 -0
  97. package/ios/Layout/Section.swift +4 -100
  98. package/ios/Loader/Loader.swift +5 -9
  99. package/ios/Lottie/LottieView.swift +14 -2
  100. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +97 -8
  101. package/ios/Navigation/BottomTab/BottomTab.swift +187 -0
  102. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  103. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  104. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  105. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  106. package/ios/Navigation/Component/Header.swift +493 -0
  107. package/ios/Navigation/Component/HeaderBackground.swift +134 -0
  108. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  109. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  110. package/ios/Navigation/Component/HeaderRight.swift +386 -0
  111. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  112. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  113. package/ios/Navigation/Component/SideBar.swift +176 -0
  114. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
  115. package/ios/Navigation/HostBackGestureBridge.swift +57 -0
  116. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -1
  117. package/ios/Navigation/Navigation.swift +190 -0
  118. package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +92 -81
  119. package/ios/{Application/Navigation → Navigation}/Navigator.swift +185 -36
  120. package/ios/Navigation/StackScreen.swift +782 -0
  121. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  122. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  123. package/ios/Pagination/PaginationScroll.swift +2 -2
  124. package/ios/Popup/PopupDisplay.swift +72 -136
  125. package/ios/Popup/PopupInput.swift +10 -13
  126. package/ios/Popup/PopupLayout.swift +140 -0
  127. package/ios/Popup/PopupNotify.swift +56 -73
  128. package/ios/Popup/PopupPromotion.swift +31 -47
  129. package/ios/PopupView/FullscreenPopup.swift +13 -19
  130. package/ios/PopupView/PopupView.swift +1 -1
  131. package/ios/PopupView/Utils++.swift +0 -8
  132. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  133. package/ios/Radio/Radio.swift +2 -1
  134. package/ios/Rating/Rating.swift +9 -1
  135. package/ios/Resources/dot_loading.json +558 -0
  136. package/ios/Resources/lottie_circle_loader.json +1 -0
  137. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  138. package/ios/Skeleton/Skeleton.swift +22 -27
  139. package/ios/Slider/Slider.swift +16 -18
  140. package/ios/Stepper/Stepper.swift +6 -1
  141. package/ios/Steps/Steps.swift +8 -6
  142. package/ios/SuggestAction/SuggestAction.swift +5 -0
  143. package/ios/Swipeable/SwipeCell.swift +10 -6
  144. package/ios/Switch/Switch.swift +22 -20
  145. package/ios/TabView/TabView.swift +102 -21
  146. package/ios/Tag/Tag.swift +15 -4
  147. package/ios/Template/Logo/Logo.swift +2 -1
  148. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  149. package/ios/Title/Title.swift +6 -17
  150. package/ios/Tooltip/Tooltip.swift +29 -14
  151. package/ios/Typography/Text.swift +61 -19
  152. package/ios/Uploader/Uploader.swift +3 -1
  153. package/ios/native-kits.podspec +2 -1
  154. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  155. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  156. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  157. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  159. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  160. package/package.json +1 -1
  161. package/scripts/gen-ios-icons.mjs +36 -0
  162. package/.claude/settings.local.json +0 -185
  163. package/ios/Application/HeaderRight.swift +0 -306
  164. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  165. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  166. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  167. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  168. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  169. package/ios/Application/Navigation/component/Header.swift +0 -178
  170. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  171. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  172. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  173. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  174. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  175. package/ios/Application/StackScreen.swift +0 -492
  176. package/local.properties +0 -8
  177. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -9,7 +9,7 @@
9
9
  import Foundation
10
10
 
11
11
  public final class ScreenTrackingState {
12
- public var time: Double = Date().timeIntervalSince1970 * 1000
12
+ public var time: Double = ScreenTracker.now()
13
13
  public var releaseNavigated: Bool = false
14
14
  public var releaseLoad: Bool = false
15
15
  public var releaseInteraction: Bool = false
@@ -22,6 +22,12 @@ public final class ScreenTrackingState {
22
22
  public enum ScreenTracker {
23
23
  private static var lastScreenName: String?
24
24
 
25
+ /// Epoch milliseconds, the clock Compose tracks with.
26
+ public static func now() -> Double { Date().timeIntervalSince1970 * 1000 }
27
+
28
+ /// Compose sends whole milliseconds (`Long`).
29
+ private static func millis(_ value: Double) -> Int64 { Int64(value.rounded()) }
30
+
25
31
  public static func trackScreenNavigated(
26
32
  maxApi: MaxApi?,
27
33
  context: MiniAppContext?,
@@ -35,7 +41,8 @@ public enum ScreenTracker {
35
41
  lastScreenName = screenName
36
42
 
37
43
  var params = buildBaseParams(context: context, screenName: screenName)
38
- params["pre_screen_name"] = previousScreen
44
+ // keeps the key with a null value; a subscript assignment of nil would drop it
45
+ params.updateValue(previousScreen, forKey: "pre_screen_name")
39
46
  params["state"] = "navigated"
40
47
  params["action"] = action
41
48
  maxApi?.track(eventName: "auto_screen_navigated", params: params)
@@ -55,7 +62,7 @@ public enum ScreenTracker {
55
62
 
56
63
  var params = buildBaseParams(context: context, screenName: screenName)
57
64
  params["state"] = "load"
58
- params["duration"] = duration
65
+ params["duration"] = millis(duration)
59
66
  maxApi?.track(eventName: "auto_screen_displayed", params: params)
60
67
 
61
68
  state.releaseLoad = true
@@ -73,8 +80,8 @@ public enum ScreenTracker {
73
80
 
74
81
  var params = buildBaseParams(context: context, screenName: screenName)
75
82
  params["state"] = "interaction"
76
- params["duration"] = duration
77
- params["total_duration"] = totalDuration
83
+ params["duration"] = millis(duration)
84
+ params["total_duration"] = millis(totalDuration)
78
85
  maxApi?.track(eventName: "auto_screen_interacted", params: params)
79
86
 
80
87
  state.releaseInteraction = true
@@ -65,21 +65,18 @@ public struct OTPKeyboard: View {
65
65
  HStack {
66
66
  KeyboardButton("", action: self.onPressKey).disabled(true).opacity(0)
67
67
  KeyboardButton("0", action: self.onPressKey)
68
+ // Frame inside the label so the whole key is tappable, not just the glyph.
68
69
  SwiftUI.Button(action: onPressBack) {
69
- HStack {
70
- Image(
71
- systemName: "delete.backward"
72
- ).foregroundColor(Colors.text)
73
- }
74
- }.frame(maxWidth: .infinity, minHeight: 48)
75
- .clipShape(RoundedRectangle(cornerRadius: 8))
70
+ Image(systemName: "delete.backward")
71
+ .foregroundColor(Colors.text)
72
+ .frame(maxWidth: .infinity, minHeight: 48)
73
+ .contentShape(Rectangle())
74
+ }
76
75
  }
77
76
  }
78
77
  .background(Colors.background)
79
78
  .valueChanged(value: value) { otp in
80
- if onChange != nil {
81
- self.onChange!(otp)
82
- }
79
+ onChange?(otp)
83
80
  }
84
81
  }
85
82
 
@@ -128,7 +125,7 @@ private struct ValueView: View {
128
125
 
129
126
  public var body: some View {
130
127
  return HStack {
131
- ForEach(0 ..< valueLength, id: \.self) { i in
128
+ ForEach(0 ..< max(valueLength, 0), id: \.self) { i in
132
129
  if i < value.count {
133
130
  Circle().foregroundColor(Colors.text).frame(width: 8, height: 8)
134
131
  } else {
@@ -14,10 +14,10 @@ import SwiftUI
14
14
  private let INDICATOR_WIDTH: CGFloat = 24
15
15
  private let PROGRESS_WIDTH: CGFloat = 72
16
16
 
17
- /// Memoized offset calculation, mirroring Compose's `calculateOffset`.
17
+ /// Mirrors Compose's `calculateOffset`; clamped because UIKit bounce overshoots the scroll range.
18
18
  private func calculateOffset(currentPosition: CGFloat, maxBound: CGFloat) -> CGFloat {
19
19
  guard maxBound > 0 else { return 0 }
20
- return (PROGRESS_WIDTH - INDICATOR_WIDTH) * (currentPosition / maxBound)
20
+ return (PROGRESS_WIDTH - INDICATOR_WIDTH) * min(max(currentPosition / maxBound, 0), 1)
21
21
  }
22
22
 
23
23
  // MARK: - PaginationScroll
@@ -1,4 +1,3 @@
1
- import SDWebImageSwiftUI
2
1
  import SwiftUI
3
2
 
4
3
  // MARK: - PopupButtonDirection
@@ -16,45 +15,13 @@ public enum PopupButtonType {
16
15
  case button
17
16
  }
18
17
 
19
- extension View {
20
- func measureLineHeights(font: Font,
21
- oneLine: Binding<CGFloat>,
22
- twoLines: Binding<CGFloat>) -> some View {
23
- overlay(
24
- VStack(spacing: 0) {
25
- Text("A")
26
- .font(font)
27
- .ignoreBoldTextSetting()
28
- .lineLimit(1)
29
- .fixedSize()
30
- .background(
31
- GeometryReader { g in
32
- Color.clear.onAppear { oneLine.wrappedValue = g.size.height }
33
- }
34
- )
35
- .hidden()
36
-
37
- Text("A\nA")
38
- .font(font)
39
- .ignoreBoldTextSetting()
40
- .lineLimit(2)
41
- .fixedSize(horizontal: false, vertical: true)
42
- .background(
43
- GeometryReader { g in
44
- Color.clear.onAppear { twoLines.wrappedValue = g.size.height }
45
- }
46
- )
47
- .hidden()
48
- }
49
- .frame(width: 0, height: 0)
50
- )
51
- }
52
- }
53
-
54
18
  // MARK: - PopupDisplay
55
19
 
56
20
  public struct PopupDisplay: View {
21
+ @Environment(\.appTheme) private var theme
57
22
  @Environment(\.localize) private var localize
23
+ @Environment(\.momoFontScale) private var fontScale
24
+ @Environment(\.screenLayout) private var layout
58
25
 
59
26
  // MARK: Lifecycle
60
27
 
@@ -75,109 +42,53 @@ public struct PopupDisplay: View {
75
42
  }
76
43
 
77
44
  // MARK: Public
78
-
45
+
79
46
  @State private var oneLineH: CGFloat = 0
80
47
  @State private var twoLineH: CGFloat = 0
48
+ @State private var textHeight: CGFloat = .zero
49
+ @State private var actionsHeight: CGFloat = 0
81
50
 
82
- private var fallbackLH: CGFloat { UIFont.preferredFont(forTextStyle: .body).lineHeight }
83
- private var perLine: CGFloat { max(twoLineH - oneLineH, oneLineH > 0 ? oneLineH : fallbackLH) }
84
-
85
- private func heightForLines(lines: CGFloat) -> CGFloat {
86
- guard oneLineH > 0 && twoLineH > 0 else { return ceil(fallbackLH * lines) }
87
- return ceil(oneLineH + max(0, lines - 1) * (twoLineH - oneLineH))
51
+ // ~8.5 lines as laid out; nominal line height until measured
52
+ private var maxHeight8_5: CGFloat {
53
+ popupLinesHeight(8.5, oneLine: oneLineH, twoLines: twoLineH)
54
+ ?? fontScale(TypographyStyle.bodyDefaultRegular.lineHeight) * 8.5
88
55
  }
89
- private var maxHeight8_5: CGFloat { heightForLines(lines: 8.5) }
90
56
 
57
+ private var shouldUseColumn: Bool {
58
+ // UTF-16 length, as PopupNotify counts it
59
+ buttonDirection == .column || (buttonDirection == .auto && (closeButtonTitle.utf16.count > 12 || actionButtonTitle.utf16.count > 12))
60
+ }
91
61
 
92
- @State private var textHeight: CGFloat = .zero
93
- @State private var isScrollable = false
94
-
95
62
  public var body: some View {
96
- var shouldUseColumn: Bool {
97
- buttonDirection == .column || (buttonDirection == .auto && (closeButtonTitle.count > 12 || actionButtonTitle.count > 12))
98
- }
99
-
100
- VStack(spacing: 12) {
101
- ZStack(alignment: .topTrailing) {
102
- if(isShowCloseIcon) {
103
- SwiftUI.Button(action: onClose) {
104
- Image(systemName: "xmark.circle.fill")
105
- .resizable()
106
- .frame(width: 22, height: 22)
107
- .overlay(RoundedRectangle(cornerRadius: Radius.L).stroke(Colors.black01, lineWidth: 2))
108
- }.foregroundColor(Colors.black20)
109
- .background(Colors.black01.cornerRadius(15))
110
- .padding(.trailing, -11)
111
- .padding(.top, -11)
112
- .zIndex(1)
113
- .accessibility(identifier: "ic_popup_close")
114
- }
115
-
116
- VStack(spacing: 0) {
117
- if(!url.isEmpty) {
118
- WebImage(url: URL(string: url), isAnimating: .constant(true))
119
- .resizable()
120
- .placeholder {
121
- Rectangle()
122
- .fill(Color.gray.opacity(0.3))
123
- .aspectRatio(1.777, contentMode: .fit)
124
- .frame(maxWidth: .infinity, maxHeight: 184)
125
- }
126
- .aspectRatio(1.777, contentMode: .fit)
127
- .frame(maxWidth: .infinity, alignment: .center)
128
- .clipShape(RoundedCorner(radius: 15, corners: [.topLeft, .topRight]))
129
- .clipped()
130
- }
131
- VStack(alignment: .leading, spacing: 0) {
132
- MomoText(title, typography: .headerDefaultBold)
133
- .padding(.top, 24)
134
- .padding(.bottom, 8)
135
- .lineLimit(2)
136
- .accessibility(identifier: "title_popup_permission")
137
-
138
- Group {
139
- if isScrollable {
140
- ScrollView(showsIndicators: false) {
141
- MomoText(description, typography: .bodyDefaultRegular)
142
- .multilineTextAlignment(.leading)
143
- .background(GeometryReader { geo in
144
- Color.clear.onAppear { textHeight = geo.size.height }
145
- })
146
- .measureLineHeights(font: .body_default_regular,
147
- oneLine: $oneLineH,
148
- twoLines: $twoLineH)
149
- .accessibility(identifier: "description_popup_permission")
150
- }
151
- // Cap the visible height to ~8.5 lines
152
- .frame(height: min(maxHeight8_5, textHeight))
153
- } else {
154
- MomoText(description, typography: .bodyDefaultRegular)
155
- .multilineTextAlignment(.leading)
156
- .background(GeometryReader { geo in
157
- Color.clear.onAppear {
158
- textHeight = geo.size.height
159
- // Trigger scroll at ~8.5 lines
160
- isScrollable = textHeight > maxHeight8_5
161
- }
162
- })
163
- .measureLineHeights(font: .body_default_regular,
164
- oneLine: $oneLineH,
165
- twoLines: $twoLineH)
166
- .accessibility(identifier: "description_popup_permission")
167
- }
63
+ ZStack(alignment: .topTrailing) {
64
+ VStack(spacing: 0) {
65
+ // scrolls when iPhone Duo landscape is too short; actions stay visible
66
+ PopupScrollBody(maxHeight: layout.popupMaxHeight.map { $0 - actionsHeight }) {
67
+ VStack(spacing: 0) {
68
+ if !url.isEmpty {
69
+ PopupHeaderImage(url: url)
168
70
  }
169
- .padding(.bottom, 8)
170
-
171
- if(!errorCode.isEmpty) {
172
- MomoText(localize.translate("errorCode") + errorCode, typography: .descriptionXsRegular, color: Colors.black12)
173
- .lineLimit(1)
71
+ VStack(alignment: .leading, spacing: 0) {
72
+ MomoText(title, typography: .headerDefaultBold, maxLines: 2, accessibilityId: "title_popup_permission")
73
+ .padding(.top, 24)
174
74
  .padding(.bottom, 8)
75
+
76
+ descriptionContent
77
+ .padding(.bottom, 8)
78
+
79
+ if !errorCode.isEmpty {
80
+ MomoText(localize.translate("errorCode") + errorCode, typography: .descriptionXsRegular, color: theme.colors.text.hint, maxLines: 1)
81
+ .padding(.bottom, 8)
82
+ }
175
83
  }
84
+ .frame(maxWidth: .infinity, alignment: .leading)
85
+ .padding(.horizontal, 24)
86
+ .padding(.bottom, 16)
87
+ .measureLineHeights(typography: .bodyDefaultRegular, oneLine: $oneLineH, twoLines: $twoLineH)
176
88
  }
177
- .frame(maxWidth: .infinity, alignment: .leading)
178
- .padding(.horizontal, 24)
179
- .padding(.bottom, 16)
180
-
89
+ }
90
+
91
+ Group {
181
92
  if shouldUseColumn {
182
93
  if buttonType == .text {
183
94
  ColumnTextButtons
@@ -192,14 +103,39 @@ public struct PopupDisplay: View {
192
103
  }
193
104
  }
194
105
  }
195
- .fixedSize(horizontal: false, vertical: true)
106
+ .popupReadHeight($actionsHeight)
107
+ }
108
+ .fixedSize(horizontal: false, vertical: true)
109
+ .background(theme.colors.background.surface)
110
+ .clipShape(RoundedRectangle(cornerRadius: Radius.L))
111
+
112
+ if isShowCloseIcon {
113
+ PopupCloseButton(action: onClose)
114
+ .offset(x: 11, y: -11)
115
+ }
116
+ }
117
+ .frame(maxWidth: layout.popupMaxWidth)
118
+ .padding(.horizontal, 12)
119
+ .accessibilityElement(children: .contain)
120
+ .accessibilityIdentifier("popup_notify")
121
+ }
122
+
123
+ @ViewBuilder
124
+ private var descriptionContent: some View {
125
+ if textHeight > maxHeight8_5 {
126
+ ScrollView(showsIndicators: false) {
127
+ descriptionText
196
128
  }
129
+ .frame(height: maxHeight8_5)
130
+ } else {
131
+ descriptionText
197
132
  }
198
- .background(Color.white.cornerRadius(15))
199
- .frame(maxWidth: popupMaxWidth - 24)
200
- .padding(.horizontal, 12)
201
- .accessibilityElement(children: .ignore)
202
- .accessibility(identifier: "popup_notify")
133
+ }
134
+
135
+ private var descriptionText: some View {
136
+ MomoText(description, typography: .bodyDefaultRegular, accessibilityId: "description_popup_permission")
137
+ .frame(maxWidth: .infinity, alignment: .leading)
138
+ .popupReadHeight($textHeight)
203
139
  }
204
140
 
205
141
  // MARK: Internal
@@ -249,7 +185,7 @@ public struct PopupDisplay: View {
249
185
  }
250
186
 
251
187
  var RowButtons: some View {
252
- HStack {
188
+ HStack(spacing: Spacing.S) {
253
189
  if(!closeButtonTitle.isEmpty) {
254
190
  Button(title: closeButtonTitle, action: onPressCloseButton, type: .text, size: .medium).accessibility(identifier: "btn_popup_cancel")
255
191
  }
@@ -262,7 +198,7 @@ public struct PopupDisplay: View {
262
198
  }
263
199
 
264
200
  var ColumnButtons: some View {
265
- VStack(alignment: .trailing) {
201
+ VStack(alignment: .trailing, spacing: Spacing.S) {
266
202
  Button(
267
203
  title: actionButtonTitle,
268
204
  action: onPressAction,
@@ -1,7 +1,9 @@
1
- import SDWebImageSwiftUI
2
1
  import SwiftUI
3
2
 
4
3
  public struct PopupInput: View {
4
+ @Environment(\.appTheme) private var theme
5
+ @Environment(\.screenLayout) private var layout
6
+
5
7
  // MARK: Lifecycle
6
8
 
7
9
  public init(isPresented: Binding<Bool>, title: String = "", description: String = "", numberOfButtons: Int = 1, actionButtonTitle: String = "", closeButtonTitle: String = "", onPressAction: @escaping () -> Void = {}, onPressCloseButton: @escaping () -> Void = {}, onClose: @escaping () -> Void = {}, inputPlaceholder: String = "", inputValue: Binding<String> = Binding.constant(""), onTextChange: ((String) -> Void)? = nil) {
@@ -24,22 +26,13 @@ public struct PopupInput: View {
24
26
  public var body: some View {
25
27
  VStack(spacing: 12) {
26
28
  ZStack(alignment: .topTrailing) {
27
- SwiftUI.Button(action: onPressClose) {
28
- Image(systemName: "xmark.circle.fill")
29
- .resizable()
30
- .frame(width: 16, height: 16)
31
- }.foregroundColor(Colors.black20)
32
- .background(Colors.black01.cornerRadius(8))
33
- .offset(x: 6, y: -6)
34
- .zIndex(1)
35
-
36
29
  VStack {
37
30
  VStack(alignment: .leading) {
38
- MomoText(title, typography: .headerDefaultBold, color: .black)
31
+ MomoText(title, typography: .headerDefaultBold)
39
32
  .padding(.top, 12)
40
33
  .padding(.bottom, 24)
41
34
 
42
- MomoText(description, typography: .bodyDefaultRegular, color: .black)
35
+ MomoText(description, typography: .bodyDefaultRegular)
43
36
  .opacity(0.6)
44
37
  .multilineTextAlignment(.leading)
45
38
  .padding(.bottom, 20)
@@ -61,11 +54,15 @@ public struct PopupInput: View {
61
54
  }
62
55
 
63
56
  }.clipped()
57
+
58
+ PopupCloseButton(action: onPressClose)
59
+ .offset(x: 11, y: -11)
64
60
  }
65
61
  }
66
- .background(Color.white.cornerRadius(8))
62
+ .background(theme.colors.background.surface.cornerRadius(8))
67
63
  .shadow(color: .black.opacity(0.08), radius: 2, x: 0, y: 0)
68
64
  .shadow(color: .black.opacity(0.16), radius: 24, x: 0, y: 0)
65
+ .frame(maxWidth: layout.popupMaxWidth)
69
66
  .padding(.horizontal, 40)
70
67
  }
71
68
 
@@ -0,0 +1,140 @@
1
+ import SDWebImageSwiftUI
2
+ import SwiftUI
3
+
4
+ // Width of a system alert on the iPhone Duo inner display, in both orientations (RN Popup/utils.ts).
5
+ private let duoPopupMaxWidth: CGFloat = 440
6
+
7
+ extension ScreenLayout {
8
+ var isDuoDevice: Bool { isIPhoneDuoDevice(windowSize: CGSize(width: width, height: height)) }
9
+
10
+ /// Card width cap (Compose `popupMaxWidth`); unbounded off iPhone Duo, where the popup's own padding sets the width.
11
+ var popupMaxWidth: CGFloat {
12
+ isDuoDevice ? duoPopupMaxWidth : .infinity
13
+ }
14
+
15
+ /// Room left for a popup on iPhone Duo, where landscape is too short for it; nil elsewhere.
16
+ var popupMaxHeight: CGFloat? {
17
+ guard isDuoDevice else { return nil }
18
+ return height - duoTopInset - insets.bottom - Spacing.XL * 2
19
+ }
20
+ }
21
+
22
+ /// Scrolls `content` once it is taller than `maxHeight`; otherwise sizes to it.
23
+ struct PopupScrollBody<Content: View>: View {
24
+ let maxHeight: CGFloat?
25
+ @ViewBuilder let content: () -> Content
26
+
27
+ @State private var contentHeight: CGFloat = 0
28
+
29
+ var body: some View {
30
+ if let maxHeight {
31
+ ScrollView(.vertical, showsIndicators: false) {
32
+ content()
33
+ .background(
34
+ GeometryReader { proxy in
35
+ Color.clear
36
+ .onAppear { contentHeight = proxy.size.height }
37
+ .onChange(of: proxy.size.height) { contentHeight = $0 }
38
+ }
39
+ )
40
+ }
41
+ .frame(height: min(contentHeight, max(maxHeight, 0)))
42
+ } else {
43
+ content()
44
+ }
45
+ }
46
+ }
47
+
48
+ /// Height of `lines` (may be fractional) lines measured by `measureLineHeights`; nil until measured.
49
+ func popupLinesHeight(_ lines: CGFloat, oneLine: CGFloat, twoLines: CGFloat) -> CGFloat? {
50
+ guard oneLine > 0, twoLines > oneLine else { return nil }
51
+ return oneLine + (lines - 1) * (twoLines - oneLine)
52
+ }
53
+
54
+ extension View {
55
+ /// Measures one and two lines of `typography` as MomoText lays them out, its line spacing included.
56
+ func measureLineHeights(typography: TypographyStyle,
57
+ oneLine: Binding<CGFloat>,
58
+ twoLines: Binding<CGFloat>) -> some View {
59
+ overlay(
60
+ VStack(spacing: 0) {
61
+ MomoText("A", typography: typography)
62
+ .fixedSize()
63
+ .popupReadHeight(oneLine)
64
+ MomoText("A\nA", typography: typography)
65
+ .fixedSize()
66
+ .popupReadHeight(twoLines)
67
+ }
68
+ .frame(width: 0, height: 0)
69
+ .hidden()
70
+ .accessibilityHidden(true)
71
+ )
72
+ }
73
+
74
+ func popupReadHeight(_ height: Binding<CGFloat>) -> some View {
75
+ background(
76
+ GeometryReader { proxy in
77
+ Color.clear
78
+ .onAppear { height.wrappedValue = proxy.size.height }
79
+ .onChange(of: proxy.size.height) { height.wrappedValue = $0 }
80
+ }
81
+ )
82
+ }
83
+ }
84
+
85
+ /// 22pt round close button shared by the popups (Compose `PopupNotify` / `PopupPromotion`).
86
+ struct PopupCloseButton: View {
87
+ @Environment(\.appTheme) private var theme
88
+ var tapSize: CGFloat = 22
89
+ let action: () -> Void
90
+
91
+ var body: some View {
92
+ SwiftUI.Button(action: action) {
93
+ Circle()
94
+ .fill(theme.colors.text.default)
95
+ .overlay(Circle().strokeBorder(theme.colors.background.surface, lineWidth: 2))
96
+ .overlay(Icon(source: "navigation_close", size: 16, color: theme.colors.background.surface))
97
+ .frame(width: 22, height: 22)
98
+ .frame(width: tapSize, height: tapSize)
99
+ .contentShape(Rectangle())
100
+ }
101
+ .accessibilityIdentifier("ic_popup_close")
102
+ }
103
+ }
104
+
105
+ /// Compose `Image` states: skeleton while loading, `media_fail` once loading fails.
106
+ struct PopupImagePlaceholder: View {
107
+ @Environment(\.appTheme) private var theme
108
+ let failed: Bool
109
+
110
+ var body: some View {
111
+ if failed {
112
+ Icon(source: "media_fail", color: theme.colors.text.disable)
113
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
114
+ } else {
115
+ Skeleton()
116
+ }
117
+ }
118
+ }
119
+
120
+ /// 16:9 header image, cropped like Compose `Image` (ContentScale.Crop).
121
+ struct PopupHeaderImage: View {
122
+ let url: String
123
+
124
+ @State private var failed = false
125
+
126
+ var body: some View {
127
+ Color.clear
128
+ .aspectRatio(1.777, contentMode: .fit)
129
+ .frame(maxWidth: .infinity)
130
+ .overlay(
131
+ WebImage(url: URL(string: url), isAnimating: .constant(true))
132
+ .onSuccess { _, _, _ in failed = false }
133
+ .onFailure { _ in failed = true }
134
+ .resizable()
135
+ .placeholder { PopupImagePlaceholder(failed: failed || URL(string: url) == nil) }
136
+ .scaledToFill()
137
+ )
138
+ .clipped()
139
+ }
140
+ }