@momo-kits/native-kits 0.168.1-beta.4-debug → 0.168.1-beta.5-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 (31) hide show
  1. package/CLAUDE.md +4 -4
  2. package/README.md +2 -10
  3. package/build.gradle.kts +1 -3
  4. package/compose/build.gradle.kts +20 -19
  5. package/compose/build.gradle.kts.backup +19 -18
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +14 -23
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +5 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +14 -24
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +1 -6
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +1 -6
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +1 -6
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +2 -3
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +124 -59
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +30 -44
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +32 -40
  17. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +2 -6
  18. package/gradle/libs.versions.toml +4 -11
  19. package/gradle/wrapper/gradle-wrapper.properties +1 -3
  20. package/gradle.properties +3 -3
  21. package/ios/Layout/ScreenLayout.swift +5 -0
  22. package/ios/Navigation/BottomTab/DuoBarCompression.swift +2 -3
  23. package/ios/Navigation/BottomTab/DuoTabBar.swift +94 -69
  24. package/ios/Navigation/Component/HeaderBackground.swift +22 -1
  25. package/ios/Navigation/StackScreen.swift +3 -7
  26. package/package.json +1 -1
  27. package/publish.sh +2 -33
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +0 -37
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +0 -41
  30. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +0 -44
  31. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +0 -62
package/gradle.properties CHANGED
@@ -18,9 +18,9 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.300.4
21
+ version=0.167.1-duo.15
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
25
- gitlab.user=
26
- gitlab.password=
25
+ gitlab.user=upload_packages
26
+ gitlab.password=gldt-CtTSGmzz57sUL9cnjdaQ
@@ -68,6 +68,11 @@ enum DuoMetrics {
68
68
  // (x 550–643 of 669, clock digits centered on y 47); its reserved region is taller than the glyphs.
69
69
  static let innerStatusBarCenterY: CGFloat = 47
70
70
  static let innerStatusBarWidth: CGFloat = 120
71
+ // Host app's iOS 26 tab bar on the inner display in portrait: a 62pt glass pill sized to its tabs,
72
+ // QR as a 62pt platter beside it, ≥ 20pt from the sides, 13pt into the home indicator inset.
73
+ static let floatingTabBarHeight: CGFloat = 62
74
+ static let floatingTabBarMargin: CGFloat = 20
75
+ static let floatingTabBarInsetOverlap: CGFloat = 13
71
76
  // Widest short side of any single-display iPhone.
72
77
  static let iPhoneMaxShortSide: CGFloat = 440
73
78
  }
@@ -35,8 +35,6 @@ struct DuoBarCompression {
35
35
  static let defaultToolbarHeight = DuoMetrics.controlSize + Spacing.M + 2 * DuoMetrics.itemPitch
36
36
  /// Back, then the single button the toolbar folds into.
37
37
  private static let compressedToolbarHeight = 2 * DuoMetrics.controlSize + Spacing.M
38
- /// Back alone, while a rail opened from the compressed state takes the toolbar slot.
39
- private static let backOnlyHeight = DuoMetrics.controlSize
40
38
 
41
39
  /// - both fit: both stay whole
42
40
  /// - one fits whole beside the other compressed: one is compressed, tapping it swaps them
@@ -72,7 +70,8 @@ struct DuoBarCompression {
72
70
 
73
71
  let expandToolbar = { opened.wrappedValue = .toolbar }
74
72
  let expandTabBar = { opened.wrappedValue = .tabBar }
75
- if opened.wrappedValue == .tabBar && fits(backOnlyHeight, tabBar) {
73
+ // an opened rail always shows, even when only the group gap is short: it closes once a tab is picked
74
+ if opened.wrappedValue == .tabBar {
76
75
  return DuoBarCompression(
77
76
  toolbarCompressed: true,
78
77
  tabBarCompressed: false,
@@ -3,7 +3,7 @@
3
3
  // MoMoUIKits
4
4
  //
5
5
  // iPhone Duo tab bar, port of RN `BottomTab/DuoTabBar.tsx` (Figma Fold-Design-System MT1/MT2):
6
- // a glass rail in the side bar, or a floating glass bar when unfolded portrait.
6
+ // a rail in the side bar, or a floating bar when unfolded portrait.
7
7
  //
8
8
 
9
9
  import SwiftUI
@@ -16,12 +16,15 @@ private let railGap = Spacing.XXS
16
16
  // selection bubble overflows the 48 rail cell by 5 above and below (Figma MT2)
17
17
  private let railBubbleWidth: CGFloat = 44
18
18
  private let railBubbleHeight: CGFloat = 58
19
- let duoFloatingTabBarHeight: CGFloat = 62
19
+ private let floatingHeight = DuoMetrics.floatingTabBarHeight
20
20
  private let floatingPadding = Spacing.XS
21
- private let floatingQrSize: CGFloat = 44
22
- // floating bar spans 6 of the 8 portrait grid columns, like the content
23
- private let gridColumns: CGFloat = 8
24
- private let gridSpan: CGFloat = 6
21
+ // the system bar sizes its pill to the tabs instead of stretching it across the display
22
+ private let floatingItemWidth: CGFloat = 80
23
+ // while pressed the selection bubble grows into a lens past the pill's edges
24
+ private let lensScaleX: CGFloat = 1.2
25
+ private let lensScaleY: CGFloat = 1.3
26
+ // Figma Home-2026 tab bar "Selection": brand pink at 8%
27
+ private let selectionTintOpacity: Double = 0x14 / 255
25
28
  private let tabSpring = Animation.interpolatingSpring(mass: 0.8, stiffness: 220, damping: 18)
26
29
 
27
30
  /// Room tab screens leave for the Duo tab bar, provided by BottomTab to its tab StackScreens.
@@ -51,47 +54,34 @@ func duoRailHeight(itemCount: Int, hasQr: Bool, compressed: Bool = false) -> CGF
51
54
  }
52
55
 
53
56
  extension ScreenLayout {
57
+ /// Overlaps the home indicator inset like the system tab bar (21pt above the bottom with a 34pt inset).
58
+ var duoFloatingTabBarBottom: CGFloat {
59
+ max(insets.bottom - DuoMetrics.floatingTabBarInsetOverlap, Spacing.S)
60
+ }
61
+
54
62
  func duoTabBarInsets(itemCount: Int, hasFloatingButton: Bool, tabBarCompressed: Bool = false) -> DuoTabBarInsets {
55
63
  guard isIPhoneDuo else { return DuoTabBarInsets() }
56
64
  guard let sideBar else {
57
- return DuoTabBarInsets(overlay: duoFloatingTabBarHeight + Spacing.M)
65
+ // overlay is measured from the bottom safe area, the bar reaches into it
66
+ return DuoTabBarInsets(overlay: floatingHeight + duoFloatingTabBarBottom - insets.bottom)
58
67
  }
59
68
  let rail = duoRailHeight(itemCount: itemCount, hasQr: hasFloatingButton, compressed: tabBarCompressed)
60
69
  return DuoTabBarInsets(sideBarReserve: sideBar.bottom + rail + DuoBarCompression.groupGap)
61
70
  }
62
71
  }
63
72
 
64
- /// Glass on iOS 26, a system blur before it (RN GlassView's fallback).
65
- struct DuoBarMaterial<S: Shape>: ViewModifier {
73
+ /// RN `BarMaterial`: surface with a 1pt border.
74
+ struct DuoBarMaterial<S: InsettableShape>: ViewModifier {
66
75
  let shape: S
67
- var visible: Bool = true
68
-
69
- func body(content: Content) -> some View {
70
- content.background(material.allowsHitTesting(false))
71
- }
72
76
 
73
- @ViewBuilder
74
- private var material: some View {
75
- #if compiler(>=6.2)
76
- if #available(iOS 26.0, *) {
77
- // glass materializes by toggling its effect, it does not fade with opacity
78
- Color.clear
79
- .glassEffect(visible ? .regular : .identity, in: shape)
80
- .shadow(color: Color.black.opacity(visible ? 0.16 : 0), radius: 16, x: 0, y: 6)
81
- .animation(.easeInOut(duration: 0.25), value: visible)
82
- } else {
83
- blur
84
- }
85
- #else
86
- blur
87
- #endif
88
- }
77
+ @Environment(\.appTheme) private var theme
89
78
 
90
- private var blur: some View {
91
- shape.fill(.regularMaterial)
92
- .shadow(color: Color.black.opacity(0.16), radius: 16, x: 0, y: 6)
93
- .opacity(visible ? 1 : 0)
94
- .animation(.easeInOut(duration: 0.25), value: visible)
79
+ func body(content: Content) -> some View {
80
+ content.background(
81
+ shape.fill(theme.colors.background.surface)
82
+ .overlay(shape.strokeBorder(theme.colors.border.default, lineWidth: 1))
83
+ .allowsHitTesting(false)
84
+ )
95
85
  }
96
86
  }
97
87
 
@@ -126,7 +116,7 @@ private struct DuoRailTabBar: View {
126
116
  let onTabSelected: (Int) -> Void
127
117
  let compression: DuoBarCompression
128
118
 
129
- @Environment(\.appTheme) private var theme
119
+ @State private var pressedIndex: Int?
130
120
 
131
121
  private var isRight: Bool { sideBar.edge == .right }
132
122
 
@@ -157,15 +147,15 @@ private struct DuoRailTabBar: View {
157
147
  .ignoresSafeArea()
158
148
  }
159
149
 
160
- /// Glass column of tab items with the sliding selection bubble.
150
+ /// Column of icon-only tab items with the sliding selection bubble.
161
151
  private var rail: some View {
162
152
  ZStack(alignment: .top) {
163
- Capsule()
164
- .fill(theme.colors.background.selected)
153
+ DuoSelectionMaterial(lens: pressedIndex != nil)
165
154
  .frame(width: railBubbleWidth, height: railBubbleHeight)
166
155
  .offset(y: railPadding - (railBubbleHeight - itemSize) / 2
167
- + CGFloat(selectedIndex) * (itemSize + railGap))
156
+ + CGFloat(pressedIndex ?? selectedIndex) * (itemSize + railGap))
168
157
  .animation(tabSpring, value: selectedIndex)
158
+ .animation(tabSpring, value: pressedIndex)
169
159
 
170
160
  VStack(spacing: railGap) {
171
161
  ForEach(items.indices, id: \.self) { index in
@@ -177,19 +167,19 @@ private struct DuoRailTabBar: View {
177
167
  .frame(width: itemSize, height: itemSize)
178
168
  .contentShape(Rectangle())
179
169
  }
180
- .buttonStyle(.plain)
170
+ .buttonStyle(DuoLensPressStyle(index: index, pressedIndex: $pressedIndex))
181
171
  .accessibilityLabel(items[index].label)
182
172
  }
183
173
  }
184
174
  .padding(.vertical, railPadding)
185
175
  }
186
176
  .frame(width: itemSize)
187
- .modifier(DuoBarMaterial(shape: RoundedRectangle(cornerRadius: Radius.XL)))
177
+ .modifier(DuoBarMaterial(shape: Capsule()))
188
178
  .accessibilityIdentifier("bottom_tab_bar")
189
179
  }
190
180
  }
191
181
 
192
- /// Floating labelled bar for the unfolded portrait display, QR in the middle cell (Figma MT1-M).
182
+ /// Floating labelled bar for the unfolded portrait display, QR pinned in its own platter beside it.
193
183
  private struct DuoFloatingTabBar: View {
194
184
  let items: [BottomTabItem]
195
185
  let floatingButton: BottomTabFloatingButton?
@@ -198,45 +188,48 @@ private struct DuoFloatingTabBar: View {
198
188
 
199
189
  @Environment(\.screenLayout) private var layout
200
190
  @Environment(\.appTheme) private var theme
191
+ @State private var pressedIndex: Int?
201
192
 
202
193
  private var barWidth: CGFloat {
203
- let columnWidth = (layout.width - 2 * Spacing.M - (gridColumns - 1) * Spacing.M) / gridColumns
204
- return columnWidth * gridSpan + (gridSpan - 1) * Spacing.M
194
+ let margins = layout.insets.leading + layout.insets.trailing + 2 * DuoMetrics.floatingTabBarMargin
195
+ let qr = floatingButton == nil ? 0 : floatingHeight + Spacing.S
196
+ let preferred = CGFloat(items.count) * floatingItemWidth + 2 * floatingPadding
197
+ return max(min(preferred, layout.width - margins - qr), 0)
205
198
  }
206
199
 
207
- private var useQr: Bool { floatingButton != nil && items.count % 2 == 0 }
208
- private var middle: Int { items.count / 2 }
209
- private var cellCount: Int { items.count + (useQr ? 1 : 0) }
210
- private var cellWidth: CGFloat { (barWidth - floatingPadding * 2) / CGFloat(max(cellCount, 1)) }
211
- private var visualIndex: Int { useQr && selectedIndex >= middle ? selectedIndex + 1 : selectedIndex }
200
+ private var cellWidth: CGFloat { (barWidth - floatingPadding * 2) / CGFloat(max(items.count, 1)) }
212
201
 
213
202
  var body: some View {
203
+ HStack(spacing: Spacing.S) {
204
+ bar
205
+ if let floatingButton {
206
+ DuoQrButton(data: floatingButton, size: floatingHeight)
207
+ .shadow(color: Color.black.opacity(0.16), radius: 16, x: 0, y: 6)
208
+ }
209
+ }
210
+ .padding(.bottom, layout.duoFloatingTabBarBottom)
211
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
212
+ .ignoresSafeArea()
213
+ }
214
+
215
+ private var bar: some View {
214
216
  ZStack(alignment: .leading) {
215
- Capsule()
216
- .fill(theme.colors.background.selected)
217
+ DuoSelectionMaterial(lens: pressedIndex != nil)
217
218
  .frame(width: cellWidth)
218
- .offset(x: CGFloat(visualIndex) * cellWidth)
219
- .animation(tabSpring, value: visualIndex)
219
+ .offset(x: CGFloat(pressedIndex ?? selectedIndex) * cellWidth)
220
+ .animation(tabSpring, value: selectedIndex)
221
+ .animation(tabSpring, value: pressedIndex)
220
222
 
221
223
  HStack(spacing: 0) {
222
- ForEach(0..<cellCount, id: \.self) { cell in
223
- if useQr && cell == middle, let floatingButton {
224
- DuoQrButton(data: floatingButton, size: floatingQrSize)
225
- .frame(width: cellWidth)
226
- } else {
227
- let index = useQr && cell > middle ? cell - 1 : cell
228
- tabCell(index)
229
- }
224
+ ForEach(items.indices, id: \.self) { index in
225
+ tabCell(index)
230
226
  }
231
227
  }
232
228
  }
233
229
  .padding(floatingPadding)
234
- .frame(width: barWidth, height: duoFloatingTabBarHeight)
230
+ .frame(width: barWidth, height: floatingHeight)
235
231
  .modifier(DuoBarMaterial(shape: Capsule()))
236
232
  .accessibilityIdentifier("bottom_tab_bar")
237
- .padding(.bottom, layout.insets.bottom + Spacing.M)
238
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
239
- .ignoresSafeArea()
240
233
  }
241
234
 
242
235
  private func tabCell(_ index: Int) -> some View {
@@ -247,20 +240,52 @@ private struct DuoFloatingTabBar: View {
247
240
  MomoText(
248
241
  items[index].label,
249
242
  typography: .labelXsMedium,
250
- color: selected ? theme.colors.primary : theme.colors.text.default,
243
+ color: selected ? theme.colors.primary : theme.colors.text.hint,
251
244
  textAlign: .center,
252
245
  maxLines: 1
253
246
  )
247
+ // like the system tab bar the label keeps its design size; a constant snapshot, nothing to observe
248
+ .environment(\.momoFontScale, FontScale(userScaleRate: 1, useOSScaleRate: false))
254
249
  }
255
250
  .frame(width: cellWidth)
256
251
  .frame(maxHeight: .infinity)
257
252
  .contentShape(Rectangle())
258
253
  }
259
- .buttonStyle(.plain)
254
+ .buttonStyle(DuoLensPressStyle(index: index, pressedIndex: $pressedIndex))
260
255
  .accessibilityLabel(items[index].label)
261
256
  }
262
257
  }
263
258
 
259
+ /// Pink tinted capsule under the selected tab; while pressed it grows into a lens (80x54 to about 96x70).
260
+ private struct DuoSelectionMaterial: View {
261
+ let lens: Bool
262
+
263
+ @Environment(\.appTheme) private var theme
264
+
265
+ var body: some View {
266
+ Capsule()
267
+ .fill(theme.colors.primary.opacity(selectionTintOpacity))
268
+ .scaleEffect(x: lens ? lensScaleX : 1, y: lens ? lensScaleY : 1)
269
+ .allowsHitTesting(false)
270
+ }
271
+ }
272
+
273
+ /// Tracks the finger so the selection lens jumps to the pressed tab before it is selected.
274
+ private struct DuoLensPressStyle: ButtonStyle {
275
+ let index: Int
276
+ @Binding var pressedIndex: Int?
277
+
278
+ func makeBody(configuration: Configuration) -> some View {
279
+ configuration.label.onChange(of: configuration.isPressed) { pressed in
280
+ if pressed {
281
+ pressedIndex = index
282
+ } else if pressedIndex == index {
283
+ pressedIndex = nil
284
+ }
285
+ }
286
+ }
287
+ }
288
+
264
289
  private struct DuoTabIcon: View {
265
290
  let item: BottomTabItem
266
291
  let selected: Bool
@@ -274,7 +299,7 @@ private struct DuoTabIcon: View {
274
299
  }
275
300
 
276
301
  var body: some View {
277
- Icon(source: item.icon, size: iconSize, color: selected ? theme.colors.primary : theme.colors.text.default)
302
+ Icon(source: item.icon, size: iconSize, color: selected ? theme.colors.primary : theme.colors.text.hint)
278
303
  .overlay(alignment: .topTrailing) {
279
304
  if let badgeLabel, !badgeLabel.isEmpty {
280
305
  Badge(label: badgeLabel).offset(x: Spacing.S, y: -Spacing.XS)
@@ -90,6 +90,27 @@ private let duoGradientExtra: CGFloat = 52
90
90
  // scroll distance over which the extended gradient collapses to the header
91
91
  private let duoExtendedCollapseDistance: CGFloat = 100
92
92
 
93
+ // scroll distance over which the bar fades in
94
+ private let duoBarFadeDistance: CGFloat = 16
95
+
96
+ /// Port of RN `DuoGlassBackground`: translucent surface with a hairline, shown once the content
97
+ /// scrolls `fadeStart` under the header.
98
+ struct DuoHeaderBar: View {
99
+ let scrollOffset: CGFloat
100
+ var fadeStart: CGFloat = 0
101
+
102
+ @Environment(\.appTheme) private var theme
103
+
104
+ var body: some View {
105
+ theme.colors.background.surface.opacity(0.8)
106
+ .overlay(alignment: .bottom) {
107
+ theme.colors.border.default.frame(height: 0.5)
108
+ }
109
+ .opacity(min(max((scrollOffset - fadeStart) / duoBarFadeDistance, 0), 1))
110
+ .allowsHitTesting(false)
111
+ }
112
+ }
113
+
93
114
  /// Port of RN `DuoScrollGradient`: with the glass header the content scrolls under the bar, so the
94
115
  /// header gradient is drawn under it. Compose draws this inside `HeaderBackground`.
95
116
  struct DuoScrollGradient: View {
@@ -114,7 +135,7 @@ struct DuoScrollGradient: View {
114
135
  let progress = min(scroll / duoExtendedCollapseDistance, 1)
115
136
  gradient
116
137
  .frame(height: headerHeight + (extended ? duoGradientExtra * (1 - progress) : 0))
117
- .opacity(1 - min(scroll / duoGradientExtra, 1))
138
+ .opacity(Double(1 - min(scroll / duoGradientExtra, 1)))
118
139
  }
119
140
  }
120
141
  .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
@@ -403,11 +403,8 @@ public struct StackScreen<Content: View>: View {
403
403
  .background(alignment: .top) {
404
404
  Group {
405
405
  if layout.sideBar != nil {
406
- // glass shows once the banner has scrolled under the header
407
- Color.clear.modifier(DuoBarMaterial(
408
- shape: Rectangle(),
409
- visible: scrollOffset > heroGlassStart(animatedHeader)
410
- ))
406
+ // the bar shows once the banner has scrolled under the header
407
+ DuoHeaderBar(scrollOffset: scrollOffset, fadeStart: heroGlassStart(animatedHeader))
411
408
  } else {
412
409
  HeaderBarBackground(opacity: opacity)
413
410
  }
@@ -457,8 +454,7 @@ public struct StackScreen<Content: View>: View {
457
454
 
458
455
  header
459
456
  .background(
460
- Color.clear
461
- .modifier(DuoBarMaterial(shape: Rectangle(), visible: scrollOffset > 0))
457
+ DuoHeaderBar(scrollOffset: scrollOffset)
462
458
  .edgesIgnoringSafeArea([.top, .horizontal])
463
459
  )
464
460
  .offset(y: -headerHide)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.168.1-beta.4-debug",
3
+ "version": "0.168.1-beta.5-debug",
4
4
  "private": false,
5
5
  "dependeNavigationncies": {},
6
6
  "devDependencies": {},
package/publish.sh CHANGED
@@ -379,12 +379,6 @@ phase_publish_npm() {
379
379
  exit 1
380
380
  fi
381
381
 
382
- # AI-GENERATED START: preserve an existing credential backup owned by another run
383
- if [ -e gradle.properties.npm-backup ]; then
384
- echo "❌ Error: gradle.properties.npm-backup already exists; recover it before publishing."
385
- exit 1
386
- fi
387
- # AI-GENERATED END: preserve an existing credential backup owned by another run
388
382
  echo "🔧 Preparing debug version with compose kits..."
389
383
 
390
384
  # Create debug version
@@ -395,9 +389,6 @@ phase_publish_npm() {
395
389
  restore_npm_workspace() {
396
390
  [ -f .npmignore.backup ] && mv .npmignore.backup .npmignore
397
391
  [ -f compose/build.gradle.kts.backup ] && mv compose/build.gradle.kts.backup compose/build.gradle.kts
398
- # AI-GENERATED START: restore private publishing credentials after npm packaging
399
- [ -f gradle.properties.npm-backup ] && mv gradle.properties.npm-backup gradle.properties
400
- # AI-GENERATED END: restore private publishing credentials after npm packaging
401
392
  local pkg_version
402
393
  pkg_version=$(node -p "require('./package.json').version")
403
394
  if [ "$pkg_version" != "$VERSION" ]; then
@@ -409,16 +400,6 @@ phase_publish_npm() {
409
400
  fi
410
401
  }
411
402
  trap restore_npm_workspace EXIT
412
- # AI-GENERATED START: keep private Maven credentials out of the debug npm package
413
- cp gradle.properties gradle.properties.npm-backup
414
- node <<'NODE'
415
- const fs = require('fs');
416
- const path = 'gradle.properties';
417
- const sanitized = fs.readFileSync(path, 'utf8')
418
- .replace(/^(gitlab\.(?:user|password)\s*=).*$/gm, '$1');
419
- fs.writeFileSync(path, sanitized);
420
- NODE
421
- # AI-GENERATED END: keep private Maven credentials out of the debug npm package
422
403
  echo "📦 Debug version: $DEBUG_VERSION"
423
404
 
424
405
  # Get current version from package.json (in case it's different from $VERSION)
@@ -455,18 +436,12 @@ NODE
455
436
  echo "🔧 Modifying android configuration for debug version..."
456
437
  if [[ "$OSTYPE" == "darwin"* ]]; then
457
438
  sed -i '' 's/namespace = "\$gitlabGroup\.\$gitlabArtifactId"/namespace = "vn.momo.kits.kits"/' compose/build.gradle.kts
458
- # AI-GENERATED START: keep the debug package on the approved SDK 36 toolchain
459
- sed -i '' \
460
- 's/compileSdk = libs\.versions\.android\.compileSdk\.get()\.toInt()/compileSdk = 36/' compose/build.gradle.kts
461
- # AI-GENERATED END: keep the debug package on the approved SDK 36 toolchain
439
+ sed -i '' 's/compileSdk = libs\.versions\.android\.compileSdk\.get()\.toInt()/compileSdk = 35/' compose/build.gradle.kts
462
440
  sed -i '' 's/minSdk = libs\.versions\.android\.minSdk\.get()\.toInt()/minSdk = 24/' compose/build.gradle.kts
463
441
  sed -i '' "s/version = gitlabVersion/version = \"$DEBUG_VERSION\"/" compose/build.gradle.kts
464
442
  else
465
443
  sed -i 's/namespace = "\$gitlabGroup\.\$gitlabArtifactId"/namespace = "vn.momo.kits.kits"/' compose/build.gradle.kts
466
- # AI-GENERATED START: keep the debug package on the approved SDK 36 toolchain
467
- sed -i \
468
- 's/compileSdk = libs\.versions\.android\.compileSdk\.get()\.toInt()/compileSdk = 36/' compose/build.gradle.kts
469
- # AI-GENERATED END: keep the debug package on the approved SDK 36 toolchain
444
+ sed -i 's/compileSdk = libs\.versions\.android\.compileSdk\.get()\.toInt()/compileSdk = 35/' compose/build.gradle.kts
470
445
  sed -i 's/minSdk = libs\.versions\.android\.minSdk\.get()\.toInt()/minSdk = 24/' compose/build.gradle.kts
471
446
  sed -i "s/version = gitlabVersion/version = \"$DEBUG_VERSION\"/" compose/build.gradle.kts
472
447
  fi
@@ -488,9 +463,6 @@ build/
488
463
  .gitattributes
489
464
  .gitlab-ci.yml
490
465
  .npmignore.backup
491
- # AI-GENERATED START: exclude the private credential backup from debug npm packages
492
- gradle.properties.npm-backup
493
- # AI-GENERATED END: exclude the private credential backup from debug npm packages
494
466
  EOF
495
467
 
496
468
  echo "📤 Publishing debug version (with compose)..."
@@ -505,9 +477,6 @@ EOF
505
477
  # Restore compose/build.gradle.kts
506
478
  mv compose/build.gradle.kts.backup compose/build.gradle.kts
507
479
  echo "✅ compose/build.gradle.kts restored"
508
- # AI-GENERATED START: restore credentials before leaving the debug packaging phase
509
- mv gradle.properties.npm-backup gradle.properties
510
- # AI-GENERATED END: restore credentials before leaving the debug packaging phase
511
480
 
512
481
  # Restore package.json version to original
513
482
  # Verify current version is debug version before restoring
@@ -1,37 +0,0 @@
1
- // File: HelpCenterRequest.kt
2
- // Created At: 2026-10-19 +07:00
3
- // Created By: AI | AI Agent: Codex | Model: GPT-6
4
- // AI-GENERATED START: preserve the toolkit Help Center payload in the typed request
5
- package vn.momo.kits.navigation.component
6
-
7
- import kotlinx.serialization.json.JsonArray
8
- import kotlinx.serialization.json.JsonElement
9
- import kotlinx.serialization.json.JsonNull
10
- import kotlinx.serialization.json.JsonObject
11
- import kotlinx.serialization.json.JsonPrimitive
12
- import vn.momo.kits.application.MiniAppContext
13
- import vn.momo.maxapi.contract.navigation.model.BottomSheetRequest
14
- import vn.momo.maxapi.contract.navigation.model.HelpCenterContext
15
-
16
- internal fun MiniAppContext.helpCenterRequest() = BottomSheetRequest.HelpCenter(
17
- HelpCenterContext(
18
- appId = appId,
19
- code = appCode,
20
- name = appName?.helpCenterJson(),
21
- icon = appIcon,
22
- description = description?.helpCenterJson(),
23
- ),
24
- )
25
-
26
- private fun Any?.helpCenterJson(): JsonElement = when (this) {
27
- null -> JsonNull
28
- is JsonElement -> this
29
- is String -> JsonPrimitive(this)
30
- is Boolean -> JsonPrimitive(this)
31
- is Number -> JsonPrimitive(this)
32
- is Map<*, *> -> JsonObject(entries.associate { (key, value) -> key.toString() to value.helpCenterJson() })
33
- is Iterable<*> -> JsonArray(map { it.helpCenterJson() })
34
- is Array<*> -> JsonArray(map { it.helpCenterJson() })
35
- else -> JsonPrimitive(toString())
36
- }
37
- // AI-GENERATED END: preserve the toolkit Help Center payload in the typed request
@@ -1,41 +0,0 @@
1
- // File: ToolkitRequests.kt
2
- // Created At: 2026-10-12 +07:00 | Created By: AI | AI Agent: Codex | Model: GPT-6
3
- // AI-GENERATED START: cancel only toolkit requests owned by this composition
4
- package vn.momo.kits.navigation.component
5
-
6
- import vn.momo.maxapi.runtime.ApiCallback
7
- import vn.momo.maxapi.runtime.ApiRequestHandle
8
-
9
- internal class ToolkitRequests {
10
- private var active = true
11
- private val handles = mutableSetOf<ApiRequestHandle>()
12
-
13
- fun <T> launch(request: (ApiCallback<T>) -> ApiRequestHandle, onResult: ApiCallback<T>) {
14
- if (!active) return
15
- var completed = false
16
- var handle: ApiRequestHandle? = null
17
- val created = request { result ->
18
- completed = true
19
- handle?.let(handles::remove)
20
- if (active) onResult(result)
21
- }
22
- handle = created
23
- if (!active) created.cancel() else if (!completed) handles.add(created)
24
- }
25
-
26
- // AI-GENERATED START: retain subscriptions across nonterminal sheet events
27
- fun <T> observe(request: (ApiCallback<T>) -> ApiRequestHandle, onResult: ApiCallback<T>) {
28
- if (!active) return
29
- val handle = request { result -> if (active) onResult(result) }
30
- if (active) handles.add(handle) else handle.cancel()
31
- }
32
- // AI-GENERATED END: retain subscriptions across nonterminal sheet events
33
-
34
- fun close() {
35
- active = false
36
- val pending = handles.toList()
37
- handles.clear()
38
- pending.forEach { it.cancel() }
39
- }
40
- }
41
- // AI-GENERATED END: cancel only toolkit requests owned by this composition
@@ -1,44 +0,0 @@
1
- // File: HelpCenterRequestTest.kt
2
- // Created At: 2026-10-19 +07:00
3
- // Created By: AI | AI Agent: Codex | Model: GPT-6
4
- // AI-GENERATED START: verify Help Center identity and localized payload preservation
5
- package vn.momo.kits.navigation.component
6
-
7
- import kotlinx.serialization.json.JsonObject
8
- import kotlinx.serialization.json.JsonPrimitive
9
- import vn.momo.kits.application.MiniAppContext
10
- import kotlin.test.Test
11
- import kotlin.test.assertEquals
12
- import kotlin.test.assertNull
13
-
14
- class HelpCenterRequestTest {
15
- @Test
16
- fun preservesApplicationIdentityAndLocalizedObjects() {
17
- val context = MiniAppContext(
18
- appId = "app-id",
19
- appCode = "app-code",
20
- appIcon = "app-icon",
21
- appName = mapOf("vi" to "Tên ứng dụng", "en" to "App name"),
22
- description = mapOf("vi" to "Mô tả", "en" to "Description"),
23
- )
24
- val request = context.helpCenterRequest().context
25
- assertEquals(context.appId, request.appId)
26
- assertEquals(context.appCode, request.code)
27
- assertEquals(context.appIcon, request.icon)
28
- assertEquals(JsonObject(mapOf("vi" to JsonPrimitive("Tên ứng dụng"),
29
- "en" to JsonPrimitive("App name"))), request.name)
30
- assertEquals(JsonObject(mapOf("vi" to JsonPrimitive("Mô tả"),
31
- "en" to JsonPrimitive("Description"))), request.description)
32
- assertNull(request.support)
33
- assertNull(request.toolkitConfig)
34
- assertNull(request.permissions)
35
- }
36
-
37
- @Test
38
- fun preservesPlainStringsAndAbsentValues() {
39
- val request = MiniAppContext(appName = "App name").helpCenterRequest().context
40
- assertEquals(JsonPrimitive("App name"), request.name)
41
- assertNull(request.description)
42
- }
43
- }
44
- // AI-GENERATED END: verify Help Center identity and localized payload preservation