@momo-kits/native-kits 0.161.1-beta.15-debug → 0.161.2-beta.1-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 (35) hide show
  1. package/compose/build.gradle.kts +9 -3
  2. package/compose/build.gradle.kts.backup +8 -2
  3. package/compose/compose.podspec +1 -1
  4. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +9 -2
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +8 -14
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +324 -117
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +4 -3
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +4 -0
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +5 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +5 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +19 -14
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +17 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +27 -12
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +1 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +1 -28
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +63 -49
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +62 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +23 -2
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +12 -4
  22. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +33 -0
  23. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +18 -8
  24. package/gradle/libs.versions.toml +3 -1
  25. package/gradle.properties +1 -1
  26. package/ios/Application/ApplicationEnvironment.swift +2 -6
  27. package/ios/Input/Input.swift +50 -21
  28. package/ios/Input/InputPhoneNumber.swift +17 -17
  29. package/ios/StatusBarTap/StatusBarTap.h +13 -0
  30. package/ios/StatusBarTap/StatusBarTap.m +75 -0
  31. package/ios/Typography/Text.swift +19 -14
  32. package/ios/Typography/Typography.swift +22 -1
  33. package/ios/native-kits.podspec +2 -1
  34. package/package.json +1 -1
  35. package/settings.gradle.kts +15 -3
@@ -24,13 +24,21 @@ fun getResource(name: String): DrawableResource {
24
24
  @Composable
25
25
  fun readJson(name: String): String {
26
26
  val path = name.plus(".json")
27
+ val candidatePaths = listOf(
28
+ path,
29
+ "composeResources/vn.momo.compose.resources/$path",
30
+ )
27
31
 
28
32
  val jsonContent by rememberState(path, { "" }) {
29
33
  val cached = resourceCache.getOrPut(path) {
30
- val content = try {
31
- readResourceBytes(path).decodeToString()
32
- } catch (_: Exception) {
33
- ""
34
+ var content = ""
35
+ for (candidate in candidatePaths) {
36
+ try {
37
+ content = readResourceBytes(candidate).decodeToString()
38
+ break
39
+ } catch (_: Exception) {
40
+ // try next candidate
41
+ }
34
42
  }
35
43
  ResourceCache.JSON(content)
36
44
  } as ResourceCache.JSON
@@ -0,0 +1,33 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.DisposableEffect
5
+ import platform.Foundation.NSNotification
6
+ import platform.Foundation.NSNotificationCenter
7
+ import platform.Foundation.NSOperationQueue
8
+ import platform.darwin.NSObjectProtocol
9
+
10
+ private const val STATUS_BAR_TAPPED_NOTIFICATION = "statusBarSelected"
11
+
12
+ @Composable
13
+ internal actual fun RegisterScrollToTop(callback: (() -> Unit)?) {
14
+ val maxApi = LocalMaxApi.current
15
+ DisposableEffect(callback) {
16
+ val cb = callback
17
+ val token: NSObjectProtocol? = if (cb != null) {
18
+ NSNotificationCenter.defaultCenter.addObserverForName(
19
+ name = STATUS_BAR_TAPPED_NOTIFICATION,
20
+ `object` = null,
21
+ queue = NSOperationQueue.mainQueue,
22
+ ) { _: NSNotification? ->
23
+ runCatching { cb() }.onFailure {
24
+ maxApi?.logFile("[ScrollToTop] callback threw: ${it.message}") {}
25
+
26
+ }
27
+ }
28
+ } else null
29
+ onDispose {
30
+ token?.let { NSNotificationCenter.defaultCenter.removeObserver(it) }
31
+ }
32
+ }
33
+ }
@@ -12,17 +12,19 @@ import androidx.compose.runtime.getValue
12
12
  import androidx.compose.runtime.mutableStateOf
13
13
  import androidx.compose.runtime.remember
14
14
  import androidx.compose.runtime.setValue
15
+ import androidx.compose.ui.ExperimentalComposeUiApi
15
16
  import androidx.compose.ui.InternalComposeUiApi
16
17
  import androidx.compose.ui.Modifier
17
18
  import androidx.compose.ui.backhandler.LocalCompatNavigationEventDispatcherOwner
18
19
  import androidx.compose.ui.graphics.Color
19
20
  import androidx.compose.ui.graphics.NativePaint
20
21
  import androidx.compose.ui.graphics.toArgb
21
- import androidx.compose.ui.interop.UIKitView
22
22
  import androidx.compose.ui.semantics.SemanticsPropertyKey
23
23
  import androidx.compose.ui.semantics.semantics
24
24
  import androidx.compose.ui.unit.Dp
25
25
  import androidx.compose.ui.unit.dp
26
+ import androidx.compose.ui.viewinterop.UIKitInteropProperties
27
+ import androidx.compose.ui.viewinterop.UIKitView
26
28
  import cocoapods.lottie_ios.CompatibleAnimation
27
29
  import cocoapods.lottie_ios.CompatibleAnimationKeypath
28
30
  import cocoapods.lottie_ios.CompatibleAnimationView
@@ -85,18 +87,25 @@ actual fun getScreenHeight(): Dp {
85
87
  return getScreenDimensions().height.dp
86
88
  }
87
89
 
88
- actual fun getOSVersion(): Int {
89
- return UIDevice.currentDevice.systemVersion.substringBefore(".").toIntOrNull() ?: 0
90
- }
90
+ actual fun getOSVersion(): OSVersion = OSVersion.IOS(
91
+ major = UIDevice.currentDevice.systemVersion.substringBefore(".").toIntOrNull() ?: 0
92
+ )
91
93
 
92
- @OptIn(ExperimentalForeignApi::class)
94
+ @OptIn(ExperimentalForeignApi::class, ExperimentalComposeUiApi::class)
93
95
  @Composable
94
96
  actual fun LottieAnimation(
95
97
  path: String,
96
98
  tintColor: Color?,
97
99
  bgColor: Color?,
98
- modifier: Modifier
100
+ placedAsOverlay: Boolean,
101
+ modifier: Modifier,
102
+ useCompose: Boolean
99
103
  ) {
104
+ if (useCompose) {
105
+ ComposeLottieAnimation(path, tintColor, bgColor, modifier)
106
+ return
107
+ }
108
+
100
109
  var animation by remember { mutableStateOf<CompatibleAnimation?>(null) }
101
110
 
102
111
  LaunchedEffect(Unit) {
@@ -112,11 +121,12 @@ actual fun LottieAnimation(
112
121
  Box(modifier) {
113
122
  UIKitView(
114
123
  modifier = Modifier.fillMaxSize(),
124
+ properties = UIKitInteropProperties(placedAsOverlay = placedAsOverlay),
115
125
  factory = {
116
126
  CompatibleAnimationView(value).apply {
117
127
  translatesAutoresizingMaskIntoConstraints = true
118
128
 
119
- setBackgroundColor(bgColor?.toUIColor() ?: UIColor.whiteColor)
129
+ setBackgroundColor(bgColor?.toUIColor() ?: UIColor.clearColor)
120
130
 
121
131
  setLoopAnimationCount(-1.0)
122
132
  setAnimationSpeed(1.0)
@@ -137,7 +147,7 @@ actual fun LottieAnimation(
137
147
  },
138
148
 
139
149
  update = { view ->
140
- view.setBackgroundColor(bgColor?.toUIColor() ?: UIColor.whiteColor)
150
+ view.setBackgroundColor(bgColor?.toUIColor() ?: UIColor.clearColor)
141
151
 
142
152
  if (tintColor != null) {
143
153
  val uiColor = tintColor.toUIColor()
@@ -13,12 +13,13 @@ androidGradlePlugin = "8.13.2"
13
13
  r8 = "8.9.35"
14
14
  kotlinx-datetime = "0.7.1"
15
15
  airbnb-lottie = "5.2.0"
16
+ compottie = "2.2.0"
16
17
  androidx-activity = "1.9.1"
17
18
  androidx-appcompat = "1.7.0"
18
19
  material = "1.10.0"
19
20
  maxapi = "0.1.1"
20
21
  vanniktechMavenPublish = "0.34.0"
21
- kits = "0.160.1-beta.6"
22
+ kits = "0.159.1-beta.7"
22
23
  nativemaxapi = "0.0.6"
23
24
 
24
25
  [libraries]
@@ -38,6 +39,7 @@ coil-multiplatform-compose = { module = "io.coil-kt.coil3:coil-compose", version
38
39
  coil-multiplatform-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil3-multiplatform" }
39
40
  r8 = { module = "com.android.tools:r8", version.ref = "r8" }
40
41
  airbnb-lottie = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "airbnb-lottie" }
42
+ compottie = { module = "io.github.alexzhirkevich:compottie", version.ref = "compottie" }
41
43
  native-max-api = { group = "vn.momo.maxapi", name = "NativeMaxApi", version.ref = "nativemaxapi" }
42
44
  kits = { module = "vn.momo.kits:kits", version.ref = "kits" }
43
45
  androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
package/gradle.properties CHANGED
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.161.1-beta.15
21
+ version=0.161.2-beta.1
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
@@ -17,7 +17,7 @@ public class MiniAppContext {
17
17
  public var toolkitConfig: [String: Any] = [:]
18
18
  public var providerId: String = "momo"
19
19
  public var permissions: [[String: Any]] = []
20
-
20
+
21
21
  public init(appId: String, appCode: String, appName: Any? = nil, appIcon: String, description: Any? = nil, support: [String: Any] = [:], toolkitConfig: [String: Any] = [:], providerId: String = "momo", permissions: [[String: Any]] = []) {
22
22
  self.appId = appId
23
23
  self.appCode = appCode
@@ -37,15 +37,11 @@ public class KitConfig {
37
37
  public var headerGradient: String? = nil
38
38
  }
39
39
 
40
- public var IsShowBaseLineDebug = false
41
-
42
- public var UseFontScaleSystem = true
43
-
44
40
  public class ApplicationEnvironment: ObservableObject {
45
41
  let applicationContext: MiniAppContext?
46
42
  let composeApi: KitComposeApi?
47
43
  let config: KitConfig?
48
-
44
+
49
45
  public init(applicationContext: MiniAppContext? = nil, composeApi: KitComposeApi? = nil, config: KitConfig? = nil) {
50
46
  self.applicationContext = applicationContext
51
47
  self.composeApi = composeApi
@@ -4,7 +4,7 @@ import Combine
4
4
 
5
5
  public struct Input: View {
6
6
  @Binding public var text: String
7
-
7
+
8
8
  public var placeholder: String
9
9
  public var floatingValue: String
10
10
  public var floatingIcon: String
@@ -22,6 +22,7 @@ public struct Input: View {
22
22
  public var leadingIconColor: Color
23
23
  public var loading: Bool
24
24
  public var required: Bool
25
+ public var maxLength: Int?
25
26
  public var fontWeight: InputFontWeight
26
27
  public var keyboardType: UIKeyboardType
27
28
  public var autofocus: Bool
@@ -29,10 +30,10 @@ public struct Input: View {
29
30
  public var onFocus: (() -> Void)?
30
31
  public var onBlur: (() -> Void)?
31
32
  public var onRightIconPressed: (() -> Void)?
32
-
33
+
33
34
  @State private var isFocused: Bool = false
34
35
  @State private var isPasswordHidden: Bool = true
35
-
36
+
36
37
  public init(
37
38
  text: Binding<String>,
38
39
  placeholder: String = "",
@@ -52,6 +53,7 @@ public struct Input: View {
52
53
  leadingIconColor: Color = Colors.black12,
53
54
  loading: Bool = false,
54
55
  required: Bool = false,
56
+ maxLength: Int? = nil,
55
57
  fontWeight: InputFontWeight = .regular,
56
58
  keyboardType: UIKeyboardType = .default,
57
59
  autofocus: Bool = false,
@@ -78,6 +80,7 @@ public struct Input: View {
78
80
  self.leadingIconColor = leadingIconColor
79
81
  self.loading = loading
80
82
  self.required = required
83
+ self.maxLength = maxLength
81
84
  self.fontWeight = fontWeight
82
85
  self.keyboardType = keyboardType
83
86
  self.autofocus = autofocus
@@ -93,7 +96,7 @@ public struct Input: View {
93
96
  get: { self.text },
94
97
  set: { newValue in
95
98
  self.text = newValue
96
- self.onChangeText?(newValue)
99
+ self.onChangeText?(limitText(newValue))
97
100
  }
98
101
  )
99
102
 
@@ -115,7 +118,7 @@ public struct Input: View {
115
118
  .offset(x: Spacing.S, y: -8)
116
119
  .zIndex(10)
117
120
  }
118
-
121
+
119
122
  // Input container
120
123
  HStack(alignment: .center, spacing: 0) {
121
124
  // Leading icon
@@ -123,7 +126,7 @@ public struct Input: View {
123
126
  Icon(source: leadingIcon, size: size == .small ? 24 : 32, color: leadingIconColor)
124
127
  .padding(.trailing, Spacing.M)
125
128
  }
126
-
129
+
127
130
  // Text input field
128
131
  ZStack(alignment: .leading) {
129
132
  if text.isEmpty {
@@ -133,7 +136,7 @@ public struct Input: View {
133
136
  color: getPlaceholderColor()
134
137
  )
135
138
  }
136
-
139
+
137
140
  if secureTextEntry && isPasswordHidden {
138
141
  SecureInputField(
139
142
  text: $text,
@@ -142,6 +145,7 @@ public struct Input: View {
142
145
  fontWeight: fontWeight == .bold ? .bold : .regular,
143
146
  textColor: UIColor(getTextColor()),
144
147
  isDisabled: disabled || readOnly,
148
+ maxLength: maxLength,
145
149
  onFocusChange: { focused in
146
150
  handleFocusChange(focused)
147
151
  },
@@ -156,9 +160,15 @@ public struct Input: View {
156
160
  .foregroundColor(getTextColor())
157
161
  .disabled(disabled || readOnly)
158
162
  .applyPrimaryCursorColor()
163
+ .onChange(of: text) { newValue in
164
+ let limited = limitText(newValue)
165
+ if limited != newValue {
166
+ text = limited
167
+ }
168
+ }
159
169
  }
160
170
  }
161
-
171
+
162
172
  // Clear button (only show when focused and has text)
163
173
  if isFocused && !text.isEmpty {
164
174
  SwiftUI.Button(action: {
@@ -169,14 +179,14 @@ public struct Input: View {
169
179
  .padding(.leading, Spacing.S).accessibility(identifier: "ic_clear")
170
180
  }
171
181
  }
172
-
182
+
173
183
  // Loading indicator
174
184
  if loading {
175
185
  ActivityIndicator(isAnimating: .constant(true), style: .medium)
176
186
  .frame(width: 16, height: 16)
177
187
  .padding(.leading, Spacing.S)
178
188
  }
179
-
189
+
180
190
  // Right icon (password toggle or custom icon)
181
191
  if secureTextEntry {
182
192
  if !text.isEmpty {
@@ -197,7 +207,7 @@ public struct Input: View {
197
207
  }
198
208
  }
199
209
  .padding(.horizontal, Spacing.M)
200
- .frame(height: scaleSize(size == .small ? 48 : 56))
210
+ .frame(height: scaleSize(size == .small ? 48 : 56, 1.1))
201
211
  .background(
202
212
  RoundedRectangle(cornerRadius: Radius.S)
203
213
  .fill(Colors.black01)
@@ -207,7 +217,7 @@ public struct Input: View {
207
217
  .stroke(borderColor(), lineWidth: 1)
208
218
  )
209
219
  }
210
-
220
+
211
221
  // Error or hint
212
222
  ErrorView(
213
223
  errorMessage: error,
@@ -223,9 +233,9 @@ public struct Input: View {
223
233
  }
224
234
  }
225
235
  }
226
-
236
+
227
237
  // MARK: - Helpers
228
-
238
+
229
239
  private func handleFocusChange(_ focused: Bool) {
230
240
  isFocused = focused
231
241
  if focused {
@@ -234,12 +244,19 @@ public struct Input: View {
234
244
  onBlur?()
235
245
  }
236
246
  }
237
-
247
+
238
248
  private func togglePasswordVisibility() {
239
249
  isPasswordHidden.toggle()
240
250
  onRightIconPressed?()
241
251
  }
242
252
 
253
+ private func limitText(_ value: String) -> String {
254
+ guard let maxLength = maxLength else {
255
+ return value
256
+ }
257
+ return String(value.prefix(maxLength))
258
+ }
259
+
243
260
  private func borderColor() -> Color {
244
261
  if disabled {
245
262
  return Colors.black04 // border.disable
@@ -252,19 +269,19 @@ public struct Input: View {
252
269
  }
253
270
  return Colors.black04 // border.default
254
271
  }
255
-
272
+
256
273
  private func getTextColor() -> Color {
257
274
  return disabled ? Colors.black09 : Colors.black17
258
275
  }
259
-
276
+
260
277
  private func getPlaceholderColor() -> Color {
261
278
  return disabled ? Colors.black09 : Colors.black12
262
279
  }
263
-
280
+
264
281
  private func getFloatingColor() -> Color {
265
282
  return disabled ? Colors.black09 : Colors.black12
266
283
  }
267
-
284
+
268
285
  private func getFloatingIconColor() -> Color {
269
286
  return disabled ? Colors.black09 : floatingIconColor
270
287
  }
@@ -279,6 +296,7 @@ private struct SecureInputField: UIViewRepresentable {
279
296
  var fontWeight: UIFont.Weight
280
297
  var textColor: UIColor
281
298
  var isDisabled: Bool
299
+ var maxLength: Int?
282
300
  var onFocusChange: (Bool) -> Void
283
301
  var onChangeText: ((String) -> Void)?
284
302
 
@@ -307,6 +325,7 @@ private struct SecureInputField: UIViewRepresentable {
307
325
  }
308
326
 
309
327
  func updateUIView(_ textField: UITextField, context: Context) {
328
+ context.coordinator.parent = self
310
329
  if textField.text != text {
311
330
  textField.text = text
312
331
  }
@@ -342,7 +361,7 @@ private struct SecureInputField: UIViewRepresentable {
342
361
  }
343
362
 
344
363
  func textFieldDidEndEditing(_ textField: UITextField) {
345
- parent.text = textField.text ?? ""
364
+ parent.text = limitText(textField.text ?? "")
346
365
  parent.onFocusChange(false)
347
366
  }
348
367
 
@@ -357,7 +376,10 @@ private struct SecureInputField: UIViewRepresentable {
357
376
 
358
377
  @objc func textFieldDidChange(_ textField: UITextField) {
359
378
  if isResettingText { return }
360
- let newText = textField.text ?? ""
379
+ let newText = limitText(textField.text ?? "")
380
+ if textField.text != newText {
381
+ textField.text = newText
382
+ }
361
383
  parent.text = newText
362
384
  parent.onChangeText?(newText)
363
385
  }
@@ -366,6 +388,13 @@ private struct SecureInputField: UIViewRepresentable {
366
388
  textField.resignFirstResponder()
367
389
  return true
368
390
  }
391
+
392
+ private func limitText(_ value: String) -> String {
393
+ guard let maxLength = parent.maxLength else {
394
+ return value
395
+ }
396
+ return String(value.prefix(maxLength))
397
+ }
369
398
  }
370
399
  }
371
400
 
@@ -8,7 +8,7 @@
8
8
  import SwiftUI
9
9
  public struct InputPhoneNumber: View {
10
10
  @Binding public var text: String
11
-
11
+
12
12
  public var placeholder: String
13
13
  public var size: InputSize
14
14
  public var hintText: String
@@ -22,9 +22,9 @@ public struct InputPhoneNumber: View {
22
22
  public var onBlur: (() -> Void)?
23
23
  public var onRightIconPressed: (() -> Void)?
24
24
  public var accessibilityLabel: String?
25
-
25
+
26
26
  @State private var isFocused: Bool = false
27
-
27
+
28
28
  public init(
29
29
  text: Binding<String>,
30
30
  placeholder: String = "0123456789",
@@ -57,7 +57,7 @@ public struct InputPhoneNumber: View {
57
57
  self.onRightIconPressed = onRightIconPressed
58
58
  self.accessibilityLabel = accessibilityLabel
59
59
  }
60
-
60
+
61
61
  // MARK: - Body
62
62
  public var body: some View {
63
63
  let textBinding = Binding<String>(
@@ -67,28 +67,28 @@ public struct InputPhoneNumber: View {
67
67
  self.onChangeText?(newValue)
68
68
  }
69
69
  )
70
-
70
+
71
71
  VStack(alignment: .leading, spacing: 4) {
72
72
  HStack(spacing: 0) {
73
73
  // 🇻🇳 Flag
74
74
  ImageView("https://static.momocdn.net/app/img/icon/ic-qrcode-package/ic_vn_flag.png")
75
75
  .frame(width: 24, height: 24)
76
76
  .padding(.trailing, Spacing.XS)
77
-
77
+
78
78
  MomoText("+84", typography: size == .small ? .headerSSemibold : .headerMBold)
79
79
  .foregroundColor(Colors.black17)
80
-
80
+
81
81
  Rectangle()
82
82
  .fill(Colors.black04)
83
83
  .frame(width: 1, height: size == .small ? 24 : 32)
84
84
  .padding(.horizontal, Spacing.M)
85
-
85
+
86
86
  // Text input
87
87
  ZStack(alignment: .leading) {
88
88
  if text.isEmpty {
89
89
  MomoText(placeholder, typography: size == .small ? .headerSSemibold : .headerMBold, color: Colors.black12)
90
90
  }
91
-
91
+
92
92
  TextField("", text: textBinding, onEditingChanged: { focused in
93
93
  handleFocusChange(focused)
94
94
  })
@@ -100,7 +100,7 @@ public struct InputPhoneNumber: View {
100
100
  .accessibility(identifier: accessibilityLabel ?? "")
101
101
  .accessibilityValue(textBinding.wrappedValue.isEmpty ? placeholder : textBinding.wrappedValue)
102
102
  }
103
-
103
+
104
104
  // Clear button
105
105
  if isFocused && !text.isEmpty {
106
106
  SwiftUI.Button(action: {
@@ -111,13 +111,13 @@ public struct InputPhoneNumber: View {
111
111
  .padding(.leading, Spacing.S).accessibility(identifier: "ic_clear")
112
112
  }
113
113
  }
114
-
114
+
115
115
  // Loading indicator
116
116
  if loading {
117
117
  ActivityIndicator(isAnimating: .constant(true), style: .medium)
118
118
  .frame(width: 16, height: 16)
119
119
  }
120
-
120
+
121
121
  // ✅ Right icon
122
122
  if !rightIcon.isEmpty {
123
123
  SwiftUI.Button(action: { onRightIconPressed?() }) {
@@ -127,7 +127,7 @@ public struct InputPhoneNumber: View {
127
127
  }
128
128
  }
129
129
  .padding(.horizontal, Spacing.M)
130
- .frame(height: scaleSize(size == .small ? 48 : 56))
130
+ .frame(height: scaleSize(size == .small ? 48 : 56, 1.1))
131
131
  .background(
132
132
  RoundedRectangle(cornerRadius: Radius.S)
133
133
  .fill(Colors.black01)
@@ -136,7 +136,7 @@ public struct InputPhoneNumber: View {
136
136
  RoundedRectangle(cornerRadius: Radius.S)
137
137
  .stroke(borderColor(), lineWidth: isFocused ? 1.5 : 1)
138
138
  )
139
-
139
+
140
140
  // Error or hint
141
141
  ErrorView(
142
142
  errorMessage: error,
@@ -145,9 +145,9 @@ public struct InputPhoneNumber: View {
145
145
  )
146
146
  }
147
147
  }
148
-
148
+
149
149
  // MARK: - Helpers
150
-
150
+
151
151
  private func handleFocusChange(_ focused: Bool) {
152
152
  isFocused = focused
153
153
  if focused {
@@ -156,7 +156,7 @@ public struct InputPhoneNumber: View {
156
156
  onBlur?()
157
157
  }
158
158
  }
159
-
159
+
160
160
  private func borderColor() -> Color {
161
161
  if !error.isEmpty { return Colors.red03 }
162
162
  if isFocused { return Colors.primary }
@@ -0,0 +1,13 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /// Posts `MoMoStatusBarTap.notificationName` on every status-bar tap.
6
+ /// The swizzle installs automatically at framework load (`+load`) — calling
7
+ /// `install` is a no-op kept for explicit/manual triggering.
8
+ @interface MoMoStatusBarTap : NSObject
9
+ @property (class, nonatomic, readonly) NSNotificationName notificationName;
10
+ + (void)install;
11
+ @end
12
+
13
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,75 @@
1
+ #import <UIKit/UIKit.h>
2
+ #import <objc/message.h>
3
+ #import <objc/runtime.h>
4
+ #import "StatusBarTap.h"
5
+
6
+ // Performs the actual swizzle once per process. Safe to call multiple times.
7
+ static void mm_installStatusBarTapSwizzleOnce(void) {
8
+ static dispatch_once_t onceToken;
9
+ dispatch_once(&onceToken, ^{
10
+ Class cls = [UIStatusBarManager class];
11
+ SEL originalSelector = NSSelectorFromString(@"handleTapAction:");
12
+ SEL swizzledSelector = NSSelectorFromString(@"mm_handleTapAction:");
13
+
14
+ Method originalMethod = class_getInstanceMethod(cls, originalSelector);
15
+ Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
16
+ if (!originalMethod || !swizzledMethod) return;
17
+
18
+ // Verify the original method matches the (id) -> void shape we expect.
19
+ // Encoding may include byte offsets (e.g. "v32@0:8@16") so we filter
20
+ // digits out before comparing.
21
+ const char *encPtr = method_getTypeEncoding(originalMethod);
22
+ if (encPtr) {
23
+ NSMutableString *enc = [NSMutableString string];
24
+ for (const char *p = encPtr; *p; p++) {
25
+ if (!(*p >= '0' && *p <= '9')) [enc appendFormat:@"%c", *p];
26
+ }
27
+ if (![enc isEqualToString:@"v@:@"]) return;
28
+ }
29
+
30
+ BOOL didAdd = class_addMethod(cls,
31
+ originalSelector,
32
+ method_getImplementation(swizzledMethod),
33
+ method_getTypeEncoding(swizzledMethod));
34
+ if (didAdd) {
35
+ class_replaceMethod(cls,
36
+ swizzledSelector,
37
+ method_getImplementation(originalMethod),
38
+ method_getTypeEncoding(originalMethod));
39
+ } else {
40
+ method_exchangeImplementations(originalMethod, swizzledMethod);
41
+ }
42
+ });
43
+ }
44
+
45
+ @implementation MoMoStatusBarTap
46
+ + (NSNotificationName)notificationName { return @"statusBarSelected"; }
47
+ + (void)install { mm_installStatusBarTapSwizzleOnce(); }
48
+ @end
49
+
50
+ // MARK: - Swizzle target on UIStatusBarManager
51
+ // `mm_handleTapAction:` is the replacement IMP. After the add/exchange,
52
+ // this selector points at the original UIKit IMP — calling it inside
53
+ // the body invokes UIKit's real handler so UIScrollView.scrollsToTop
54
+ // and any other side effects continue to work.
55
+
56
+ @interface UIStatusBarManager (MoMoStatusBarTap)
57
+ @end
58
+
59
+ @implementation UIStatusBarManager (MoMoStatusBarTap)
60
+
61
+ // Auto-install at framework load. Commented out so the swizzle stays
62
+ // opt-in via `[MoMoStatusBarTap install]`. Uncomment to revert to
63
+ // auto-installation before main().
64
+ //
65
+ // + (void)load {
66
+ // mm_installStatusBarTapSwizzleOnce();
67
+ // }
68
+
69
+ - (void)mm_handleTapAction:(id)action {
70
+ [self mm_handleTapAction:action];
71
+ [[NSNotificationCenter defaultCenter] postNotificationName:MoMoStatusBarTap.notificationName
72
+ object:nil];
73
+ }
74
+
75
+ @end