@momo-kits/native-kits 0.111.1-rc.3 → 0.112.1-beta.10

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.
@@ -178,6 +178,7 @@ fun Header(
178
178
  val statusBarHeight = AppStatusBar.current ?: getStatusBarHeight()
179
179
  val tintIconColor = AppTheme.current.colors.text.default
180
180
  val backgroundButton = Colors.black_01.copy(alpha = 0.6f)
181
+ val miniAppContext = ApplicationContext.current
181
182
 
182
183
  var colorFraction by remember { mutableStateOf(0f) }
183
184
 
@@ -242,10 +243,12 @@ fun Header(
242
243
  }
243
244
  }
244
245
 
245
- Box(Modifier.graphicsLayer {
246
- alpha = if (animatedHeader !== null) opacity ?: 1f else 1f
247
- }) {
248
- headerRight?.invoke()
246
+ if (inputSearchProps == null && miniAppContext != null) {
247
+ Box(Modifier.graphicsLayer {
248
+ alpha = if (animatedHeader !== null) opacity ?: 1f else 1f
249
+ }) {
250
+ headerRight?.invoke()
251
+ }
249
252
  }
250
253
  }
251
254
 
@@ -0,0 +1,250 @@
1
+ package vn.momo.kits.application
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.border
5
+ import androidx.compose.foundation.clickable
6
+ import androidx.compose.foundation.layout.Arrangement
7
+ import androidx.compose.foundation.layout.Box
8
+ import androidx.compose.foundation.layout.Row
9
+ import androidx.compose.foundation.layout.height
10
+ import androidx.compose.foundation.layout.offset
11
+ import androidx.compose.foundation.layout.padding
12
+ import androidx.compose.foundation.layout.size
13
+ import androidx.compose.foundation.layout.width
14
+ import androidx.compose.foundation.shape.CircleShape
15
+ import androidx.compose.foundation.shape.RoundedCornerShape
16
+ import androidx.compose.runtime.Composable
17
+ import androidx.compose.runtime.getValue
18
+ import androidx.compose.runtime.mutableStateOf
19
+ import androidx.compose.runtime.remember
20
+ import androidx.compose.runtime.setValue
21
+ import androidx.compose.ui.Alignment
22
+ import androidx.compose.ui.Modifier
23
+ import androidx.compose.ui.draw.clip
24
+ import androidx.compose.ui.graphics.Color
25
+ import androidx.compose.ui.unit.dp
26
+ import vn.momo.kits.components.BadgeDot
27
+ import vn.momo.kits.components.DotSize
28
+ import vn.momo.kits.components.Icon
29
+ import vn.momo.kits.const.Colors
30
+
31
+ data class HeaderRightData(
32
+ val useShortcut: Boolean = false,
33
+ val useMore: Boolean = false,
34
+ val tools: List<ToolGroup> = emptyList(),
35
+ val toolCallback: ((String) -> Unit)? = { _ -> }
36
+ )
37
+
38
+ data class Tool(
39
+ val key: String,
40
+ val icon: String,
41
+ val showBadge: Boolean = false,
42
+ val name: Map<String, String> = emptyMap()
43
+ ) {
44
+ fun toMap(): Map<String, Any> {
45
+ return mapOf(
46
+ "key" to key,
47
+ "icon" to icon,
48
+ "showBadge" to showBadge,
49
+ "name" to name
50
+ )
51
+ }
52
+ }
53
+
54
+ data class ToolGroup(
55
+ val title: Map<String, String> = emptyMap(),
56
+ val items: List<Tool>
57
+ ) {
58
+ fun toMap(): Map<String, Any> {
59
+ return mapOf(
60
+ "title" to title,
61
+ "items" to items.map { it.toMap() }
62
+ )
63
+ }
64
+ }
65
+
66
+ data class NavigationButtonConfig(
67
+ val icon: String,
68
+ val onPress: () -> Unit
69
+ )
70
+
71
+ object Spacing {
72
+ val M = 16.dp
73
+ val S = 8.dp
74
+ val XXS = 2.dp
75
+ }
76
+
77
+ @Composable
78
+ fun HeaderRight(
79
+ headerRight: HeaderRightData? = null,
80
+ ) {
81
+ Row(
82
+ verticalAlignment = Alignment.CenterVertically,
83
+ ) {
84
+ ToolkitHeaderRight(headerRight = headerRight)
85
+ }
86
+ }
87
+
88
+ @Composable
89
+ fun ToolkitHeaderRight(headerRight: HeaderRightData? = null) {
90
+ var isFavorite by remember { mutableStateOf(false) }
91
+ val isLoading by remember { mutableStateOf(false) }
92
+ val api = PlatformApi.current as? ComposeApi
93
+ val context = ApplicationContext.current
94
+
95
+ fun onPressShortcut() {
96
+ api?.request("onToolAction", mapOf("item" to mapOf("key" to "onFavorite"))) {
97
+ isFavorite = !isFavorite
98
+ }
99
+ }
100
+
101
+ fun onPressHelpCenter() {
102
+ val paramMap = mapOf(
103
+ "appId" to context?.appId,
104
+ "code" to context?.appCode,
105
+ "name" to context?.appName,
106
+ "icon" to context?.appIcon,
107
+ "description" to context?.description
108
+ )
109
+ api?.request("showHelpCenter", paramMap) {
110
+
111
+ }
112
+ }
113
+
114
+ fun onPressClose() {
115
+ api?.request("dismiss", "")
116
+ }
117
+
118
+ fun onPressMore() {
119
+ api?.request("showTools",
120
+ mapOf(
121
+ "tools" to headerRight?.tools?.map { it.toMap() },
122
+ "context" to mapOf(
123
+ "appId" to context?.appId,
124
+ "code" to context?.appCode,
125
+ "name" to context?.appName,
126
+ "icon" to context?.appIcon,
127
+ "description" to context?.description
128
+ )
129
+ )
130
+ ) {
131
+
132
+ }
133
+ }
134
+
135
+ fun getNavigationButtonConfig(
136
+ tools: List<ToolGroup>? = emptyList(),
137
+ useMore: Boolean? = false,
138
+ isFavorite: Boolean = false,
139
+ onPressShortcut: () -> Unit,
140
+ onPressMore: () -> Unit
141
+ ): NavigationButtonConfig {
142
+ val totalTools = tools?.sumOf { it.items.size } ?: 0
143
+ var icon = if (isFavorite) "pin_star_checked" else "pin_star"
144
+ var onClickHandler = onPressShortcut
145
+ if (totalTools > 1 || useMore == true) {
146
+ icon = "navigation_more_icon"
147
+ onClickHandler = onPressMore
148
+ } else if (totalTools == 1 && !tools.isNullOrEmpty()) {
149
+ val singleTool = tools.first().items.firstOrNull()
150
+ if (singleTool != null) {
151
+ icon = singleTool.icon
152
+ onClickHandler = {
153
+ headerRight?.toolCallback?.invoke(singleTool.key)
154
+ }
155
+ }
156
+ }
157
+ return NavigationButtonConfig(icon, onClickHandler)
158
+ }
159
+
160
+ val navButtonConfig = getNavigationButtonConfig(
161
+ headerRight?.tools,
162
+ headerRight?.useMore,
163
+ isFavorite,
164
+ ::onPressShortcut,
165
+ ::onPressMore
166
+ )
167
+
168
+ val showBadge = headerRight?.tools?.any { group ->
169
+ group.items.any { it.showBadge }
170
+ }
171
+
172
+ Row(
173
+ verticalAlignment = Alignment.CenterVertically
174
+ ) {
175
+ if (headerRight?.useShortcut == true) {
176
+ NavigationButton(
177
+ disabled = isLoading,
178
+ icon = navButtonConfig.icon,
179
+ showBadge = showBadge,
180
+ onClick = navButtonConfig.onPress
181
+ )
182
+ }
183
+ Row(
184
+ verticalAlignment = Alignment.CenterVertically,
185
+ horizontalArrangement = Arrangement.Center,
186
+ modifier = Modifier
187
+ .padding(start = Spacing.S)
188
+ .border(0.2.dp, Color(0x33000000), shape = RoundedCornerShape(14.dp))
189
+ .width(65.dp)
190
+ .height(28.dp)
191
+ .clip(shape = RoundedCornerShape(14.dp))
192
+ .background(Color(0x99FFFFFF))
193
+ ) {
194
+ Icon(source = "help_center", size = 20.dp, modifier = Modifier.padding(4.dp).clickable {
195
+ onPressHelpCenter()
196
+ })
197
+ Box(
198
+ modifier = Modifier
199
+ .width(0.5.dp)
200
+ .height(12.dp)
201
+ .background(Colors.black_20)
202
+ )
203
+ Icon(
204
+ source = "16_navigation_close_circle",
205
+ size = 20.dp,
206
+ modifier = Modifier.padding(4.dp).clickable { onPressClose() }
207
+ )
208
+ }
209
+ }
210
+ }
211
+
212
+ @Composable
213
+ fun NavigationButton(
214
+ disabled: Boolean,
215
+ icon: String,
216
+ showBadge: Boolean? = false,
217
+ onClick: () -> Unit
218
+ ) {
219
+ Box(
220
+ modifier = Modifier
221
+ .size(28.dp)
222
+ .clickable(enabled = !disabled, onClick = onClick)
223
+ ) {
224
+ Box(
225
+ modifier = Modifier
226
+ .matchParentSize()
227
+ .clip(CircleShape)
228
+ .background(Color(0x99FFFFFF))
229
+ .border(0.2.dp, Color(0x33000000), CircleShape)
230
+ )
231
+ Box(
232
+ modifier = Modifier.matchParentSize(),
233
+ contentAlignment = Alignment.Center
234
+ ) {
235
+ Icon(
236
+ source = icon,
237
+ size = 16.dp
238
+ )
239
+ }
240
+
241
+ if (showBadge != true) {
242
+ BadgeDot(
243
+ size = DotSize.Small,
244
+ modifier = Modifier
245
+ .align(Alignment.TopEnd)
246
+ .offset(x = -Spacing.XXS, y = -Spacing.XXS)
247
+ )
248
+ }
249
+ }
250
+ }
@@ -22,40 +22,20 @@ import vn.momo.kits.const.defaultTheme
22
22
  import vn.momo.kits.platform.disableOverscroll
23
23
  import vn.momo.kits.platform.getStatusBarHeight
24
24
 
25
- val PlatformApi = staticCompositionLocalOf<Any?> { null }
26
-
27
25
  interface ComposeApi {
28
- fun request(funcName: String, params: String?): String
29
- fun request(funcName: String, params: String?, onResponse: ((String) -> Unit)?): String
30
- fun requestCallback(funcName: String, params: String?, onResponse: ((String) -> Unit)?)
26
+ fun request(funcName: String, params: Any?): String
27
+ fun request(funcName: String, params: Any?, onResponse: ((String) -> Unit)?): String
28
+ fun requestCallback(funcName: String, params: Any?, onResponse: ((String) -> Unit)?)
31
29
  fun removeCallback(id: String)
32
30
  }
33
31
 
34
- val AppNavigator = staticCompositionLocalOf<Navigator> {
35
- error("no navigator provided!")
36
- }
37
-
38
32
  class Navigator {
39
- fun push(content: @Composable () -> Unit) {
40
- }
41
-
42
- fun present(content: @Composable () -> Unit) {
43
-
44
- }
45
-
46
- fun reset(content: @Composable () -> Unit) {
47
-
48
- }
49
-
50
- fun pop() {
51
- }
52
-
53
- fun replace(content: @Composable () -> Unit) {
54
- }
55
-
56
- fun popToTop() {
57
- }
58
-
33
+ fun push(content: @Composable () -> Unit) {}
34
+ fun present(content: @Composable () -> Unit) {}
35
+ fun reset(content: @Composable () -> Unit) {}
36
+ fun pop() {}
37
+ fun replace(content: @Composable () -> Unit) {}
38
+ fun popToTop() {}
59
39
  fun showModal(
60
40
  onClose: () -> Unit = {},
61
41
  canBackgroundClose: Boolean = true,
@@ -63,8 +43,25 @@ class Navigator {
63
43
  ) {
64
44
  }
65
45
 
66
- fun showBottomSheet(content: @Composable () -> Unit) {
67
- }
46
+ fun showBottomSheet(content: @Composable () -> Unit) {}
47
+ }
48
+
49
+ data class MiniAppContext(
50
+ val appIcon: String = "",
51
+ val appName: Any? = null,
52
+ val appId: String = "",
53
+ val appCode: String = "",
54
+ val description: Any? = null,
55
+ )
56
+
57
+ val PlatformApi = staticCompositionLocalOf<Any?> { null }
58
+
59
+ val AppNavigator = staticCompositionLocalOf<Navigator> {
60
+ error("no navigator provided!")
61
+ }
62
+
63
+ val ApplicationContext = staticCompositionLocalOf<MiniAppContext?> {
64
+ error("no context provided!")
68
65
  }
69
66
 
70
67
  @Composable
@@ -72,7 +69,8 @@ fun ApplicationContainer(
72
69
  theme: Theme = defaultTheme,
73
70
  composeApi: ComposeApi? = null,
74
71
  statusBarHeight: Dp? = AppStatusBar.current ?: getStatusBarHeight(),
75
- content: @Composable () -> Unit
72
+ applicationContext: MiniAppContext? = null,
73
+ content: @Composable () -> Unit,
76
74
  ) {
77
75
  var appTheme by remember { mutableStateOf(theme) }
78
76
  val coroutineScope = rememberCoroutineScope()
@@ -97,8 +95,7 @@ fun ApplicationContainer(
97
95
  }
98
96
  }
99
97
  }
100
- }
101
- catch (e: Exception) {
98
+ } catch (e: Exception) {
102
99
  print("@@ == NavigationContainer fetch config error $e")
103
100
  }
104
101
  }
@@ -109,6 +106,7 @@ fun ApplicationContainer(
109
106
  PlatformApi provides composeApi,
110
107
  AppNavigator provides Navigator(),
111
108
  AppStatusBar provides statusBarHeight,
109
+ ApplicationContext provides applicationContext,
112
110
  ) {
113
111
  content()
114
112
  }
@@ -62,7 +62,7 @@ fun Screen(
62
62
  onContentLayout: ((LayoutCoordinates) -> Unit)? = null,
63
63
  useAvoidKeyboard: Boolean = true,
64
64
  footer: @Composable (() -> Unit)? = null,
65
- headerRight: @Composable (() -> Unit)? = null,
65
+ headerRight: @Composable (() -> Unit)? = { HeaderRight() },
66
66
  fabProps: FabProps? = null,
67
67
  animatedHeader: AnimatedHeader? = null,
68
68
  layoutOffset: Dp = 56.dp,
@@ -17,9 +17,9 @@ enum class DotSize(val size: Int) {
17
17
  }
18
18
 
19
19
  @Composable
20
- fun BadgeDot(size: DotSize = DotSize.Large) {
20
+ fun BadgeDot(size: DotSize = DotSize.Large, modifier: Modifier = Modifier) {
21
21
  Box(
22
- modifier = Modifier
22
+ modifier = modifier
23
23
  .size(size.size.dp)
24
24
  .border(width = 1.dp, color = Colors.black_01, shape = RoundedCornerShape(Radius.S))
25
25
  .background(color = Colors.red_03, shape = RoundedCornerShape(Radius.S))
@@ -39,15 +39,15 @@ fun getFont(font: String): FontFamily {
39
39
  fun getFontFamily(fontFamily: String, fontWeight: FontWeight? = FontWeight.Normal): FontFamily {
40
40
  val key = "$fontFamily-${fontWeight?.weight}"
41
41
  val fontMap = mapOf(
42
- "SFProText-100" to "sfprotext_thin.ttf",
43
- "SFProText-200" to "sfprotext_ultralight.ttf",
42
+ "SFProText-100" to "sfprotext_thin.otf",
43
+ "SFProText-200" to "sfprotext_ultralight.otf",
44
44
  "SFProText-300" to "sfprotext_light.ttf",
45
45
  "SFProText-400" to "sfprotext_regular.ttf",
46
46
  "SFProText-500" to "sfprotext_medium.ttf",
47
- "SFProText-600" to "sfprotext_medium.ttf",
48
- "SFProText-700" to "sfprotext_semibold.ttf",
49
- "SFProText-800" to "sfprotext_bold.ttf",
50
- "SFProText-900" to "sfprotext_heavy.ttf",
47
+ "SFProText-600" to "sfprotext_semibold.ttf",
48
+ "SFProText-700" to "sfprotext_bold.ttf",
49
+ "SFProText-800" to "sfprotext_heavy.ttf",
50
+ "SFProText-900" to "sfprotext_black.otf",
51
51
 
52
52
  "MoMoSignature-100" to "momosignature.otf",
53
53
  "MoMoSignature-200" to "momosignature.otf",
@@ -1318,5 +1318,10 @@ val Icons = mapOf(
1318
1318
  "ic_checked" to "https://img.mservice.com.vn/app/img/kits/checked_ic.png",
1319
1319
  "ic_minus" to "https://img.mservice.com.vn/app/img/kits/minus.png",
1320
1320
  "ic_navigation" to "https://static.momocdn.net/app/img/kits/ic_navigation.png",
1321
- "ic_money_bag" to "https://static.momocdn.net/app/img/kits/ic_money_bag.png"
1321
+ "ic_money_bag" to "https://static.momocdn.net/app/img/kits/ic_money_bag.png",
1322
+ "ic_support" to "https://static.momocdn.net/app/img/kits/ic_support.png",
1323
+ "navigation_more_icon" to "https://static.momocdn.net/app/img/kits/navigation_more_icon.png",
1324
+ "help_center" to "https://static.momocdn.net/app/img/kits/help-center.png",
1325
+ "pin_star" to "https://static.momocdn.net/app/img/kits/pin_star.png",
1326
+ "pin_star_checked" to "https://static.momocdn.net/app/img/kits/pin_star_checked.png"
1322
1327
  )
@@ -0,0 +1,223 @@
1
+ //
2
+ // Components.swift
3
+ // Pods
4
+ //
5
+ // Created by sophia on 24/1/25.
6
+ //
7
+
8
+ import SwiftUI
9
+
10
+ public enum TitlePosition {
11
+ case left, center
12
+ }
13
+
14
+ public enum HeaderType {
15
+ case `default`, extended, none
16
+ }
17
+
18
+
19
+ public struct AnimatedHeader {
20
+ public var type: HeaderType = .default
21
+ public var composable: (_ scrollState: Int) -> AnyView = { _ in AnyView(EmptyView()) }
22
+
23
+ public init(
24
+ type: HeaderType = .default,
25
+ composable: @escaping (_ scrollState: Int) -> AnyView = { _ in AnyView(EmptyView()) }
26
+ ) {
27
+ self.type = type
28
+ self.composable = composable
29
+ }
30
+ }
31
+
32
+ func safeAreaTopInset() -> CGFloat {
33
+ if #available(iOS 15.0, *) {
34
+ return UIApplication.shared.connectedScenes
35
+ .compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.top }
36
+ .first ?? 0
37
+ } else {
38
+ return UIApplication.shared.connectedScenes
39
+ .compactMap { ($0 as? UIWindowScene)?.windows.first?.safeAreaInsets.top }
40
+ .first ?? 0
41
+ }
42
+ }
43
+
44
+
45
+ public struct HeaderBackground: View {
46
+ var headerType: HeaderType = .default
47
+ var scrollState: CGFloat
48
+
49
+ public var body: some View {
50
+
51
+ let statusBarHeight = safeAreaTopInset()
52
+
53
+ let opacity = 1 - CGFloat(scrollState) / 200
54
+ let backgroundColor = scrollState == 0 ? Color.clear : Colors.black01
55
+
56
+ Group {
57
+ if headerType == .default {
58
+ let height = statusBarHeight + 52
59
+ ZStack {
60
+
61
+ Rectangle()
62
+ .fill(backgroundColor)
63
+ .frame(height: height)
64
+ .shadow(color: Colors.black20.opacity(0.2), radius: 10, x: 0, y: -2)
65
+ .overlay(Rectangle()
66
+ .fill(LinearGradient(
67
+ gradient: Gradient(colors: [
68
+ Color(red: 1, green: 0.8, blue: 0.87),
69
+ Color(red: 1, green: 0.8, blue: 0.87).opacity(0.5)
70
+ ]),
71
+ startPoint: .top,
72
+ endPoint: .bottom))
73
+ .opacity(opacity)
74
+ .frame(height: height))
75
+ .overlay(Rectangle()
76
+ .fill(Colors.black04)
77
+ .frame(height: 1)
78
+ .frame(maxWidth: .infinity)
79
+ .alignmentGuide(.bottom) { _ in 0 }, alignment: .bottom
80
+ )
81
+ }
82
+ .frame(maxWidth: .infinity)
83
+
84
+ } else if headerType == .extended {
85
+ ZStack {
86
+ Rectangle()
87
+ .fill(LinearGradient(
88
+ gradient: Gradient(colors: [
89
+ Color(red: 1, green: 0.8, blue: 0.87),
90
+ Color(red: 1, green: 0.8, blue: 0.87).opacity(0)
91
+ ]),
92
+ startPoint: .top,
93
+ endPoint: .bottom))
94
+ .opacity(opacity)
95
+ .frame(height: 154)
96
+
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ }
103
+
104
+ public struct Header: View {
105
+ var headerType: HeaderType = .default
106
+ var titlePosition: TitlePosition
107
+ var title: String
108
+ var headerRight: (() -> AnyView)?
109
+ var goBack: (() -> Void)?
110
+ var opacity: CGFloat? = nil
111
+ var animatedHeader: AnimatedHeader? = nil
112
+ var scrollState: CGFloat = 0
113
+ var inputSearchProps: InputSearchProps? = nil
114
+
115
+ @State private var colorFraction: CGFloat = 0
116
+
117
+ public init(
118
+ headerType: HeaderType = .default,
119
+ titlePosition: TitlePosition,
120
+ title: String,
121
+ headerRight: (() -> AnyView)? = nil,
122
+ goBack: (() -> Void)? = nil,
123
+ opacity: CGFloat? = nil,
124
+ animatedHeader: AnimatedHeader? = nil,
125
+ scrollState: CGFloat = 0,
126
+ inputSearchProps: InputSearchProps? = nil
127
+ ) {
128
+ self.headerType = headerType
129
+ self.titlePosition = titlePosition
130
+ self.title = title
131
+ self.headerRight = headerRight
132
+ self.goBack = goBack
133
+ self.opacity = opacity
134
+ self.animatedHeader = animatedHeader
135
+ self.scrollState = scrollState
136
+ self.inputSearchProps = inputSearchProps
137
+ }
138
+
139
+ public var body: some View {
140
+
141
+ let statusBarHeight = safeAreaTopInset()
142
+ let tintIconColor = Colors.black17
143
+ let backgroundButton = Colors.black01.opacity(0.6)
144
+
145
+ if headerType != .none {
146
+ VStack(spacing: 0) {
147
+ ZStack {
148
+ HStack {
149
+ if let goBack = goBack {
150
+ SwiftUI.Button(action: goBack) {
151
+ Circle()
152
+ .stroke(Color.black.opacity(0.2), lineWidth: 0.2)
153
+ .background(Circle().fill(backgroundButton))
154
+ .frame(width: 28, height: 28)
155
+ .overlay(
156
+ Image(systemName: "arrow.backward")
157
+ .foregroundColor(.black)
158
+ )
159
+ }
160
+
161
+ }
162
+
163
+ Spacer() // Pushes headerRight to the right
164
+
165
+ if let headerRight = headerRight {
166
+ headerRight()
167
+ }
168
+ }
169
+ .frame(height: 52)
170
+ .padding(.horizontal)
171
+
172
+
173
+
174
+ HStack {
175
+ if titlePosition == .left, goBack != nil {
176
+ Spacer().frame(width: 40)
177
+ }
178
+
179
+ if inputSearchProps != nil {
180
+ if let inputProps = inputSearchProps {
181
+ InputSearch(
182
+ text: inputProps.$text,
183
+ buttonText: inputProps.buttonText,
184
+ showButtonText: inputProps.showButtonText,
185
+ showBorder: inputProps.showBorder,
186
+ placeholder: inputProps.placeholder,
187
+ onChangeText: inputProps.onChangeText,
188
+ onPressButtonText: inputProps.onPressButtonText,
189
+ error: inputProps.error,
190
+ disabled: inputProps.disabled,
191
+ icon: inputProps.icon,
192
+ iconColor: inputProps.iconColor,
193
+ onRightIconPressed: inputProps.onRightIconPressed,
194
+ onFocus: inputProps.onFocus,
195
+ onBlur: inputProps.onBlur,
196
+ loading: inputProps.loading,
197
+ fontWeight: inputProps.fontWeight,
198
+ keyboardType: inputProps.keyboardType
199
+ )
200
+ }
201
+ } else {
202
+ Text(title)
203
+ .font(.headline)
204
+ .foregroundColor(tintIconColor)
205
+ .frame(maxWidth: titlePosition == .center ? .infinity : nil, alignment: titlePosition == .center ? .center : .leading)
206
+ }
207
+
208
+ Spacer()
209
+ }
210
+ .frame(height: 52)
211
+ .padding(.horizontal)
212
+
213
+ }
214
+ .background(Color.clear)
215
+ }
216
+ }
217
+ }
218
+
219
+ }
220
+
221
+
222
+
223
+
@@ -0,0 +1,148 @@
1
+ //
2
+ // FloatingButton.swift
3
+ // Pods
4
+ //
5
+ // Created by sophia on 6/2/25.
6
+ //
7
+
8
+ import SwiftUI
9
+ import Combine
10
+
11
+ public enum FABSize {
12
+ case small
13
+ case `default`
14
+ }
15
+
16
+ public enum FABPosition {
17
+ case end
18
+ case center
19
+ }
20
+
21
+ public struct FabProps {
22
+ let icon: String
23
+ let iconColor: Color?
24
+ let label: String?
25
+ let onClick: () -> Void
26
+ let size: FABSize
27
+ let bottom: CGFloat?
28
+ let scrollOffset: CGFloat?
29
+ let position: FABPosition?
30
+
31
+ public init(
32
+ icon: String,
33
+ iconColor: Color? = nil,
34
+ label: String? = nil,
35
+ onClick: @escaping () -> Void,
36
+ size: FABSize = .default,
37
+ bottom: CGFloat? = nil,
38
+ scrollOffset: CGFloat? = nil,
39
+ position: FABPosition? = .end
40
+ ) {
41
+ self.icon = icon
42
+ self.iconColor = iconColor
43
+ self.label = label
44
+ self.onClick = onClick
45
+ self.size = size
46
+ self.bottom = bottom
47
+ self.scrollOffset = scrollOffset
48
+ self.position = position
49
+ }
50
+ }
51
+
52
+ public struct FloatingButton: View {
53
+ @State private var lastScrollOffset: CGFloat = 0
54
+ @State private var isExpanded: Bool = false
55
+ let scrollOffset: CGFloat
56
+ let bottomPadding: CGFloat
57
+ let onClick: () -> Void
58
+ let containerColor: Color
59
+ let contentColor: Color
60
+ let icon: String
61
+ let iconColor: Color?
62
+ let text: String?
63
+ let size: FABSize
64
+ let position: FABPosition
65
+ let keyboardOffset: CGFloat
66
+
67
+ public init(
68
+ scrollOffset: CGFloat = 0,
69
+ bottomPadding: CGFloat = 0,
70
+ onClick: @escaping () -> Void,
71
+ containerColor: Color = .blue,
72
+ contentColor: Color = .white,
73
+ icon: String,
74
+ iconColor: Color? = nil,
75
+ text: String? = nil,
76
+ size: FABSize = .default,
77
+ position: FABPosition = .end,
78
+ keyboardOffset: CGFloat = 0
79
+ ) {
80
+ self.scrollOffset = scrollOffset
81
+ self.bottomPadding = bottomPadding
82
+ self.onClick = onClick
83
+ self.containerColor = containerColor
84
+ self.contentColor = contentColor
85
+ self.icon = icon
86
+ self.iconColor = iconColor
87
+ self.text = text
88
+ self.size = size
89
+ self.position = position
90
+ self.keyboardOffset = keyboardOffset
91
+ }
92
+
93
+ public var body: some View {
94
+ VStack {
95
+ Spacer()
96
+ HStack {
97
+ if position == .center {
98
+ Spacer()
99
+ }
100
+ FloatingButtonContent()
101
+ .padding(.trailing, position == .end ? 12 : 0)
102
+ .padding(.bottom, bottomPadding)
103
+ .offset(y: -keyboardOffset)
104
+ if position == .end {
105
+ Spacer()
106
+ }
107
+ }
108
+ }
109
+ .onReceive(Just(scrollOffset)) { newValue in
110
+ withAnimation(.easeInOut(duration: 0.3)) {
111
+ isExpanded = newValue > lastScrollOffset
112
+ lastScrollOffset = newValue
113
+ }
114
+ }
115
+ }
116
+
117
+ @ViewBuilder
118
+ private func FloatingButtonContent() -> some View {
119
+ SwiftUI.Button(action: onClick) {
120
+ HStack {
121
+ Image(systemName: icon)
122
+ .foregroundColor(iconColor ?? .white)
123
+ .font(size == .small ? .system(size: 12) : .system(size: 24))
124
+
125
+ if isExpanded, let text = text {
126
+ Text(text)
127
+ .foregroundColor(.white)
128
+ .transition(.asymmetric(insertion: expandAnimation, removal: collapseAnimation))
129
+ }
130
+ }
131
+ .padding(.horizontal, isExpanded ? 12 : 0)
132
+ .frame(width: isExpanded ? 80 : (size == .small ? 36 : 48), height: size == .small ? 36 : 48)
133
+ .background(containerColor)
134
+ .cornerRadius(24)
135
+ .shadow(radius: 4)
136
+ }
137
+ }
138
+
139
+ private var expandAnimation: AnyTransition {
140
+ AnyTransition.opacity.animation(.easeIn(duration: 0.2))
141
+ .combined(with: .move(edge: .leading))
142
+ }
143
+
144
+ private var collapseAnimation: AnyTransition {
145
+ AnyTransition.opacity.animation(.easeOut(duration: 0.5))
146
+ .combined(with: .move(edge: .trailing))
147
+ }
148
+ }
@@ -0,0 +1,231 @@
1
+ import SwiftUI
2
+
3
+ public struct HeaderRightData {
4
+ var useShortcut: Bool
5
+ var useMore: Bool
6
+ var tools: [ToolGroup]
7
+ var toolCallback: ((String) -> Void)?
8
+
9
+ public init(
10
+ useShortcut: Bool = false,
11
+ useMore: Bool = false,
12
+ tools: [ToolGroup] = [],
13
+ toolCallback: ((String) -> Void)? = nil
14
+ ) {
15
+ self.useShortcut = useShortcut
16
+ self.useMore = useMore
17
+ self.tools = tools
18
+ self.toolCallback = toolCallback
19
+ }
20
+ }
21
+
22
+ public struct Tool {
23
+ var key: String
24
+ var icon: String
25
+ var showBadge: Bool
26
+ var name: [String: String]
27
+
28
+ public init(
29
+ key: String,
30
+ icon: String,
31
+ showBadge: Bool = false,
32
+ name: [String: String] = [:]
33
+ ) {
34
+ self.key = key
35
+ self.icon = icon
36
+ self.showBadge = showBadge
37
+ self.name = name
38
+ }
39
+
40
+ func toMap() -> [String: Any] {
41
+ return [
42
+ "key": key,
43
+ "icon": icon,
44
+ "showBadge": showBadge,
45
+ "name": name
46
+ ]
47
+ }
48
+ }
49
+
50
+ public struct ToolGroup {
51
+ var title: [String: String]
52
+ var items: [Tool]
53
+
54
+ public init(
55
+ title: [String: String] = [:],
56
+ items: [Tool]
57
+ ) {
58
+ self.title = title
59
+ self.items = items
60
+ }
61
+
62
+ func toMap() -> [String: Any] {
63
+ return [
64
+ "title": title,
65
+ "items": items.map { $0.toMap() }
66
+ ]
67
+ }
68
+ }
69
+
70
+ struct NavigationButtonConfig {
71
+ let icon: String
72
+ let onPress: () -> Void
73
+ }
74
+
75
+ // MARK: - Components
76
+ struct HeaderRight: View {
77
+ var headerRight: HeaderRightData?
78
+
79
+ var body: some View {
80
+ HStack(alignment: .center) {
81
+ ToolkitHeaderRight(headerRight: headerRight)
82
+ }
83
+ }
84
+ }
85
+
86
+ struct ToolkitHeaderRight: View {
87
+ var headerRight: HeaderRightData?
88
+ @State private var isFavorite: Bool = false
89
+ @State private var isLoading: Bool = false
90
+
91
+ // MARK: - Actions
92
+ private func onPressShortcut() {
93
+ ComposeApi.shared.request(
94
+ funcName: "onToolAction",
95
+ params: ["item": ["key": "onFavorite"]]
96
+ ) { _ in
97
+ isFavorite.toggle()
98
+ }
99
+ }
100
+
101
+ private func onPressHelpCenter() {
102
+ let context = ApplicationContext.shared
103
+ let paramMap: [String: Any] = [
104
+ "appId": context.appId,
105
+ "code": context.appCode,
106
+ "name": context.appName,
107
+ "icon": context.appIcon,
108
+ "description": context.description
109
+ ]
110
+ ComposeApi.shared.request(
111
+ funcName: "showHelpCenter",
112
+ params: paramMap
113
+ ) { _ in }
114
+ }
115
+
116
+ private func onPressClose() {
117
+ ComposeApi.shared.request(funcName: "dismiss", params: "")
118
+ }
119
+
120
+ private func onPressMore() {
121
+ let context = ApplicationContext.shared
122
+ let params: [String: Any] = [
123
+ "tools": headerRight?.tools.map { $0.toMap() } ?? [],
124
+ "context": [
125
+ "appId": context.appId,
126
+ "code": context.appCode,
127
+ "name": context.appName,
128
+ "icon": context.appIcon,
129
+ "description": context.description
130
+ ]
131
+ ]
132
+ ComposeApi.shared.request(
133
+ funcName: "showTools",
134
+ params: params
135
+ ) { _ in }
136
+ }
137
+
138
+ private func getNavigationButtonConfig() -> NavigationButtonConfig {
139
+ let totalTools = headerRight?.tools.reduce(0) { $0 + $1.items.count } ?? 0
140
+ var icon = isFavorite ? "pin_star_checked" : "pin_star"
141
+ var onClickHandler: () -> Void = onPressShortcut
142
+
143
+ if totalTools > 1 || headerRight?.useMore == true {
144
+ icon = "navigation_more_icon"
145
+ onClickHandler = onPressMore
146
+ } else if totalTools == 1, let singleTool = headerRight?.tools.first?.items.first {
147
+ icon = singleTool.icon
148
+ onClickHandler = {
149
+ headerRight?.toolCallback?(singleTool.key)
150
+ }
151
+ }
152
+
153
+ return NavigationButtonConfig(icon: icon, onPress: onClickHandler)
154
+ }
155
+
156
+ var body: some View {
157
+ let navButtonConfig = getNavigationButtonConfig()
158
+ let showBadge = headerRight?.tools.contains { group in
159
+ group.items.contains { $0.showBadge }
160
+ } ?? false
161
+
162
+ HStack(alignment: .center) {
163
+ if headerRight?.useShortcut == true {
164
+ NavigationButton(
165
+ disabled: isLoading,
166
+ icon: navButtonConfig.icon,
167
+ showBadge: showBadge,
168
+ onClick: navButtonConfig.onPress
169
+ )
170
+ }
171
+
172
+ HStack(alignment: .center, spacing: 0) {
173
+ Icon(source: "help_center", size: 20)
174
+ .padding(4)
175
+ .onTapGesture {
176
+ onPressHelpCenter()
177
+ }
178
+
179
+ Rectangle()
180
+ .fill(Colors.black20)
181
+ .frame(width: 0.5, height: 12)
182
+
183
+ Icon(source: "16_navigation_close_circle", size: 20)
184
+ .padding(4)
185
+ .onTapGesture {
186
+ onPressClose()
187
+ }
188
+ }
189
+ .frame(width: 65, height: 28)
190
+ .background(Color.white.opacity(0.6))
191
+ .cornerRadius(14)
192
+ .overlay(
193
+ RoundedRectangle(cornerRadius: 14)
194
+ .stroke(Color.black.opacity(0.2), lineWidth: 0.2)
195
+ )
196
+ .padding(.leading, 8)
197
+ }
198
+ }
199
+ }
200
+
201
+ struct NavigationButton: View {
202
+ var disabled: Bool
203
+ var icon: String
204
+ var showBadge: Bool
205
+ var onClick: () -> Void
206
+
207
+ var body: some View {
208
+ ZStack {
209
+ Circle()
210
+ .fill(Color.white.opacity(0.6))
211
+ .overlay(
212
+ Circle()
213
+ .stroke(Color.black.opacity(0.2), lineWidth: 0.2)
214
+ )
215
+
216
+ Icon(source: icon, size: 16)
217
+
218
+ if showBadge {
219
+ BadgeDot(size: .small)
220
+ .offset(x: -2, y: -2)
221
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
222
+ }
223
+ }
224
+ .frame(width: 28, height: 28)
225
+ .onTapGesture {
226
+ if !disabled {
227
+ onClick()
228
+ }
229
+ }
230
+ }
231
+ }
@@ -0,0 +1,228 @@
1
+ //
2
+ // Screen.swift
3
+ // Pods
4
+ //
5
+ // Created by sophia on 5/2/25.
6
+ //
7
+
8
+ import SwiftUI
9
+
10
+ public struct Screen<Content: View>: View {
11
+ var backgroundColor: Color?
12
+ var isBack: Bool
13
+ var headerType: HeaderType
14
+ var verticalAlignment: VerticalAlignment
15
+ var horizontalAlignment: HorizontalAlignment
16
+ var title: String
17
+ var titlePosition: TitlePosition
18
+ var goBack: (() -> Void)?
19
+ var scrollable: Bool
20
+ var onContentLayout: ((CGRect) -> Void)?
21
+ var useAvoidKeyboard: Bool
22
+ var footer: (() -> AnyView)?
23
+ var headerRight: HeaderRightData?
24
+ var animatedHeader: AnimatedHeader? = nil
25
+ var layoutOffset: CGFloat
26
+ var inputSearchProps: InputSearchProps? = nil
27
+ var fabProps: FabProps? = nil
28
+ var content: () -> Content
29
+
30
+ public init(
31
+ backgroundColor: Color? = nil,
32
+ isBack: Bool = true,
33
+ headerType: HeaderType = .default,
34
+ verticalAlignment: VerticalAlignment = .top,
35
+ horizontalAlignment: HorizontalAlignment = .leading,
36
+ title: String = "Stack",
37
+ titlePosition: TitlePosition = .center,
38
+ goBack: (() -> Void)? = nil,
39
+ scrollable: Bool = true,
40
+ onContentLayout: ((CGRect) -> Void)? = nil,
41
+ useAvoidKeyboard: Bool = true,
42
+ footer: (() -> AnyView)? = nil,
43
+ headerRight: HeaderRightData? = nil,
44
+ animatedHeader: AnimatedHeader? = nil,
45
+ layoutOffset: CGFloat = 56,
46
+ inputSearchProps: InputSearchProps? = nil,
47
+ fabProps: FabProps? = nil,
48
+ @ViewBuilder content: @escaping () -> Content
49
+ ) {
50
+ self.backgroundColor = backgroundColor
51
+ self.isBack = isBack
52
+ self.headerType = headerType
53
+ self.verticalAlignment = verticalAlignment
54
+ self.horizontalAlignment = horizontalAlignment
55
+ self.title = title
56
+ self.titlePosition = titlePosition
57
+ self.goBack = goBack
58
+ self.scrollable = scrollable
59
+ self.onContentLayout = onContentLayout
60
+ self.useAvoidKeyboard = useAvoidKeyboard
61
+ self.footer = footer
62
+ self.headerRight = headerRight
63
+ self.animatedHeader = animatedHeader
64
+ self.layoutOffset = layoutOffset
65
+ self.inputSearchProps = inputSearchProps
66
+ self.fabProps = fabProps
67
+ self.content = content
68
+ }
69
+
70
+ @State private var scrollOffset: CGFloat = 0
71
+
72
+ public var body: some View {
73
+ GeometryReader { geometry in
74
+ let safeAreaInsets = geometry.safeAreaInsets
75
+ let statusBarHeight = safeAreaInsets.top
76
+ let keyboardHeight: CGFloat = 0
77
+ let keyboardSize: CGFloat = useAvoidKeyboard ? max(keyboardHeight, 0) : 0
78
+
79
+ ZStack {
80
+ (backgroundColor ?? Color.white).edgesIgnoringSafeArea(.all)
81
+
82
+ VStack(spacing: 0) {
83
+ ZStack {
84
+ if animatedHeader == nil {
85
+ HeaderBackground(headerType: headerType, scrollState: scrollOffset)
86
+ } else {
87
+ Color.white.opacity(Double(min(scrollOffset / 114, 1)))
88
+ .overlay(HeaderBackground(headerType: headerType, scrollState: scrollOffset))
89
+
90
+ }
91
+
92
+ Header(
93
+ headerType: headerType,
94
+ titlePosition: titlePosition,
95
+ title: title,
96
+ headerRight: headerRight,
97
+ goBack: goBack,
98
+ opacity: min(scrollOffset / 114, 1),
99
+ animatedHeader: animatedHeader,
100
+ scrollState: scrollOffset,
101
+ inputSearchProps: inputSearchProps
102
+ ).padding(.bottom, headerType == .extended ? 0 : -safeAreaTopInset())
103
+ }.padding(.top, -safeAreaTopInset())
104
+
105
+
106
+ VStack(alignment: horizontalAlignment, spacing: 0) {
107
+ if animatedHeader != nil {
108
+ Spacer().frame(height: statusBarHeight + 52 + layoutOffset)
109
+ }
110
+
111
+ if scrollable {
112
+ ScrollView(.vertical, showsIndicators: false) {
113
+ VStack {
114
+ content()
115
+ }
116
+ .background(GeometryReader { geo -> Color in
117
+ DispatchQueue.main.async {
118
+ self.scrollOffset = -geo.frame(in: .global).origin.y
119
+ }
120
+ return Color.clear
121
+ })
122
+ }
123
+ } else {
124
+ content()
125
+ }
126
+ }
127
+ .padding(.bottom, keyboardSize)
128
+
129
+
130
+ if let footerView = footer?() {
131
+ ZStack(alignment: .top) {
132
+ Rectangle()
133
+ .fill(Colors.black20)
134
+ .frame(height: 4)
135
+ .shadow(color: Colors.black20.opacity(0.2), radius: 10, x: 0, y: -2)
136
+
137
+ Footer(footerView: footerView, keyboardSize: keyboardSize)
138
+ }
139
+ }
140
+
141
+ if let fabProps = fabProps {
142
+ FloatingButton(
143
+ scrollOffset: scrollOffset,
144
+ bottomPadding: fabProps.bottom ?? 16,
145
+ onClick: fabProps.onClick,
146
+ containerColor: .blue,
147
+ contentColor: .white,
148
+ icon: fabProps.icon,
149
+ iconColor: fabProps.iconColor,
150
+ text: fabProps.label,
151
+ size: fabProps.size,
152
+ position: fabProps.position ?? .end,
153
+ keyboardOffset: keyboardSize
154
+ )
155
+ }
156
+ }
157
+
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ struct Footer: View {
164
+ var footerView: AnyView
165
+ var keyboardSize: CGFloat
166
+
167
+ var body: some View {
168
+ VStack {
169
+ footerView
170
+ }
171
+ .padding(.bottom, keyboardSize)
172
+ .background(Colors.black01)
173
+ }
174
+ }
175
+
176
+ public struct Header: View {
177
+ var headerType: HeaderType = .default
178
+ var titlePosition: TitlePosition
179
+ var title: String
180
+ var headerRight: HeaderRightData?
181
+ var goBack: (() -> Void)?
182
+ var opacity: CGFloat? = nil
183
+ var animatedHeader: AnimatedHeader? = nil
184
+ var scrollState: CGFloat = 0
185
+ var inputSearchProps: InputSearchProps? = nil
186
+
187
+ @State private var colorFraction: CGFloat = 0
188
+
189
+ public init(
190
+ headerType: HeaderType = .default,
191
+ titlePosition: TitlePosition,
192
+ title: String,
193
+ headerRight: HeaderRightData? = nil,
194
+ goBack: (() -> Void)? = nil,
195
+ opacity: CGFloat? = nil,
196
+ animatedHeader: AnimatedHeader? = nil,
197
+ scrollState: CGFloat = 0,
198
+ inputSearchProps: InputSearchProps? = nil
199
+ ) {
200
+ self.headerType = headerType
201
+ self.titlePosition = titlePosition
202
+ self.title = title
203
+ self.headerRight = headerRight
204
+ self.goBack = goBack
205
+ self.opacity = opacity
206
+ self.animatedHeader = animatedHeader
207
+ self.scrollState = scrollState
208
+ self.inputSearchProps = inputSearchProps
209
+ }
210
+
211
+ public var body: some View {
212
+ HStack {
213
+ if let goBack = goBack {
214
+ Button(action: goBack) {
215
+ Text("Back")
216
+ }
217
+ }
218
+
219
+ Spacer()
220
+
221
+ if headerRight != nil {
222
+ HeaderRight(headerRight: headerRight)
223
+ .opacity(animatedHeader != nil ? opacity ?? 1 : 1)
224
+ }
225
+ }
226
+ }
227
+ }
228
+
@@ -37,6 +37,62 @@ struct RenderRightIconSearch: View {
37
37
  }
38
38
  }
39
39
 
40
+ public struct InputSearchProps {
41
+ @Binding var text: String
42
+ var buttonText: String = ""
43
+ var showButtonText: Bool = false
44
+ var showBorder: Bool = true
45
+ var placeholder: String = ""
46
+ var onChangeText: (String) -> Void = { _ in }
47
+ var onPressButtonText: () -> Void = {}
48
+ var error: String = ""
49
+ var disabled: Bool = false
50
+ var icon: String = ""
51
+ var iconColor: Color = Colors.text
52
+ var onRightIconPressed: () -> Void = {}
53
+ var onFocus: () -> Void = {}
54
+ var onBlur: () -> Void = {}
55
+ var loading: Bool = false
56
+ var fontWeight: Font.Weight = .regular
57
+ var keyboardType: UIKeyboardType = .default
58
+
59
+ public init(text: Binding<String>,
60
+ buttonText: String = "",
61
+ showButtonText: Bool = false,
62
+ showBorder: Bool = true,
63
+ placeholder: String = "",
64
+ onChangeText: @escaping (String) -> Void = { _ in },
65
+ onPressButtonText: @escaping () -> Void = {},
66
+ error: String = "",
67
+ disabled: Bool = false,
68
+ icon: String = "",
69
+ iconColor: Color = Colors.text,
70
+ onRightIconPressed: @escaping () -> Void = {},
71
+ onFocus: @escaping () -> Void = {},
72
+ onBlur: @escaping () -> Void = {},
73
+ loading: Bool = false,
74
+ fontWeight: Font.Weight = .regular,
75
+ keyboardType: UIKeyboardType = .default) {
76
+ self._text = text
77
+ self.buttonText = buttonText
78
+ self.showButtonText = showButtonText
79
+ self.showBorder = showBorder
80
+ self.placeholder = placeholder
81
+ self.onChangeText = onChangeText
82
+ self.onPressButtonText = onPressButtonText
83
+ self.error = error
84
+ self.disabled = disabled
85
+ self.icon = icon
86
+ self.iconColor = iconColor
87
+ self.onRightIconPressed = onRightIconPressed
88
+ self.onFocus = onFocus
89
+ self.onBlur = onBlur
90
+ self.loading = loading
91
+ self.fontWeight = fontWeight
92
+ self.keyboardType = keyboardType
93
+ }
94
+ }
95
+
40
96
  // MARK: - InputSearch
41
97
 
42
98
  public struct InputSearch: View {
package/local.properties CHANGED
@@ -4,5 +4,5 @@
4
4
  # Location of the SDK. This is only used by Gradle.
5
5
  # For customization when using a Version Control System, please read the
6
6
  # header note.
7
- #Wed Aug 21 14:20:12 ICT 2024
8
- sdk.dir=/Users/huynhdung/Library/Android/sdk
7
+ #Tue Apr 23 16:40:15 ICT 2024
8
+ sdk.dir=/Users/hung.dao1/Library/Android/sdk
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.111.1-rc.3",
3
+ "version": "0.112.1-beta.10",
4
4
  "private": false,
5
5
  "dependencies": {},
6
6
  "devDependencies": {},