@momo-kits/foundation 0.169.6 → 0.169.7-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.
Files changed (143) hide show
  1. package/Application/BottomSheet.hooks.ts +8 -8
  2. package/Application/BottomSheet.parts.tsx +34 -44
  3. package/Application/BottomSheet.tsx +37 -40
  4. package/Application/BottomTab/ActiveIndicator.tsx +14 -15
  5. package/Application/BottomTab/BottomTabBar.tsx +62 -49
  6. package/Application/BottomTab/BottomTabInset.tsx +4 -8
  7. package/Application/BottomTab/CurvedContainer.tsx +12 -14
  8. package/Application/BottomTab/CustomBottomTabItem.tsx +13 -13
  9. package/Application/BottomTab/DuoBarCompression.tsx +19 -13
  10. package/Application/BottomTab/DuoRailTabBar.tsx +24 -20
  11. package/Application/BottomTab/DuoTabBar.tsx +10 -8
  12. package/Application/BottomTab/TabBarIcon.tsx +29 -15
  13. package/Application/BottomTab/TabScreen.tsx +5 -1
  14. package/Application/BottomTab/index.tsx +30 -33
  15. package/Application/BottomTab/metrics.ts +23 -23
  16. package/Application/BottomTab/tabItems.ts +1 -1
  17. package/Application/BottomTab/usePressLift.ts +2 -2
  18. package/Application/Components/BackgroundImageView.tsx +0 -4
  19. package/Application/Components/DuoGlassBackground.tsx +4 -5
  20. package/Application/Components/HeaderAnimated.tsx +0 -4
  21. package/Application/Components/HeaderBackground.tsx +0 -4
  22. package/Application/Components/HeaderExtendHeader.tsx +10 -11
  23. package/Application/Components/HeaderLeft.tsx +1 -2
  24. package/Application/Components/HeaderRight.tsx +1 -2
  25. package/Application/Components/HeaderTitle.shared.ts +9 -11
  26. package/Application/Components/HeaderTitle.tsx +3 -4
  27. package/Application/Components/HeaderToolkitAction.tsx +20 -17
  28. package/Application/Components/NavigationButton.tsx +1 -2
  29. package/Application/Components/SearchHeader.tsx +10 -17
  30. package/Application/Components/TitleJourney.tsx +2 -2
  31. package/Application/Components/TitleLocation.tsx +2 -2
  32. package/Application/Components/TitleUser.tsx +2 -2
  33. package/Application/Components/useToolkitActions.tsx +32 -17
  34. package/Application/KitAdoption/AdoptionBadge.tsx +119 -0
  35. package/Application/KitAdoption/AdoptionProbe.tsx +166 -0
  36. package/Application/KitAdoption/AdoptionStore.ts +80 -0
  37. package/Application/KitAdoption/Authorship/utils.ts +137 -0
  38. package/Application/KitAdoption/Slots/utils.ts +68 -0
  39. package/Application/KitAdoption/constants.ts +120 -0
  40. package/Application/KitAdoption/index.ts +12 -0
  41. package/Application/KitAdoption/styles.ts +55 -0
  42. package/Application/KitAdoption/types.ts +61 -0
  43. package/Application/KitAdoption/utils.ts +376 -0
  44. package/Application/Localize.ts +0 -7
  45. package/Application/ModalScreen.tsx +10 -10
  46. package/Application/Navigation.ts +16 -14
  47. package/Application/NavigationContainer.tsx +18 -19
  48. package/Application/Navigator.ts +48 -20
  49. package/Application/ScaleSizeProvider.tsx +6 -13
  50. package/Application/StackScreen.tracking.ts +33 -11
  51. package/Application/StackScreen.tsx +54 -10
  52. package/Application/TooltipPortal.tsx +5 -12
  53. package/Application/WidgetContainer.tsx +4 -10
  54. package/Application/__tests__/KitAdoption.integration.test.tsx +253 -0
  55. package/Application/__tests__/KitAdoption.test.ts +283 -0
  56. package/Application/__tests__/KitAdoptionAuthorship.test.ts +132 -0
  57. package/Application/__tests__/KitAdoptionStore.test.ts +110 -0
  58. package/Application/__tests__/KitAdoptionStructural.test.ts +193 -0
  59. package/Application/__tests__/modalLayout.test.ts +128 -0
  60. package/Application/modalLayout.ts +14 -13
  61. package/Application/navigationOptions.tsx +17 -21
  62. package/Badge/Badge.tsx +0 -5
  63. package/Badge/BadgeDot.tsx +0 -1
  64. package/Badge/BadgeDotAnimation.tsx +7 -15
  65. package/Badge/BadgeRibbon.tsx +38 -46
  66. package/Badge/Shape.tsx +0 -2
  67. package/Badge/types.ts +2 -2
  68. package/Button/index.tsx +16 -5
  69. package/Button/utils.ts +15 -9
  70. package/CheckBox/index.tsx +0 -4
  71. package/Consts/theme.ts +10 -10
  72. package/Context/index.ts +1 -5
  73. package/Divider/DashDivider.tsx +0 -1
  74. package/Divider/index.tsx +0 -4
  75. package/FoundationList/index.tsx +0 -4
  76. package/Icon/index.tsx +6 -6
  77. package/Icon/types.ts +1 -1
  78. package/IconButton/index.tsx +12 -4
  79. package/Image/index.tsx +7 -9
  80. package/Input/Input.tsx +9 -8
  81. package/Input/InputDropDown.tsx +12 -10
  82. package/Input/InputMoney.tsx +8 -12
  83. package/Input/InputMoneyView.tsx +7 -14
  84. package/Input/InputOTP.tsx +33 -35
  85. package/Input/InputPhoneNumber.tsx +9 -8
  86. package/Input/InputSearch.tsx +6 -4
  87. package/Input/InputTextArea.tsx +0 -4
  88. package/Input/SearchTextTyping.tsx +0 -4
  89. package/Input/SystemTextInput.tsx +2 -7
  90. package/Input/TextTyping.tsx +0 -4
  91. package/Input/common.tsx +12 -26
  92. package/Input/index.tsx +20 -17
  93. package/Input/moneyUtils.ts +4 -10
  94. package/Input/styles.ts +5 -0
  95. package/Input/utils.ts +12 -14
  96. package/Layout/Card.tsx +13 -16
  97. package/Layout/FloatingButton.tsx +0 -4
  98. package/Layout/Item.tsx +2 -6
  99. package/Layout/ItemList.tsx +7 -9
  100. package/Layout/ItemSectionList.tsx +1 -2
  101. package/Layout/ReservedRegions.tsx +25 -25
  102. package/Layout/Screen.tsx +58 -56
  103. package/Layout/Section.tsx +13 -17
  104. package/Layout/TrackingScope.tsx +0 -1
  105. package/Layout/constants.ts +19 -0
  106. package/Layout/screenContent.tsx +2 -4
  107. package/Layout/screenFooter.tsx +18 -28
  108. package/Layout/screenHeader.tsx +31 -34
  109. package/Layout/screenQuickReturn.tsx +20 -17
  110. package/Layout/screenScroll.ts +7 -4
  111. package/Layout/screenTypes.ts +2 -3
  112. package/Layout/types.ts +41 -3
  113. package/Layout/useGridSystem.ts +49 -0
  114. package/Layout/utils.ts +37 -38
  115. package/Loader/DotLoader.tsx +0 -4
  116. package/Loader/Loader.tsx +0 -4
  117. package/Loader/ProgressBar.tsx +0 -1
  118. package/Loader/Spinner.tsx +0 -4
  119. package/Loader/utils.ts +7 -11
  120. package/Pagination/Dot.tsx +0 -4
  121. package/Pagination/PaginationDot.tsx +0 -1
  122. package/Pagination/PaginationNumber.tsx +3 -5
  123. package/Pagination/PaginationScroll.tsx +0 -5
  124. package/Pagination/PaginationWhiteDot.tsx +0 -1
  125. package/Pagination/index.tsx +0 -4
  126. package/Pagination/types.ts +2 -2
  127. package/Popup/PopupNotify.tsx +48 -44
  128. package/Popup/PopupPromotion.tsx +7 -4
  129. package/Popup/types.ts +4 -4
  130. package/Popup/utils.ts +0 -4
  131. package/Radio/index.tsx +0 -4
  132. package/Skeleton/index.tsx +0 -8
  133. package/Switch/index.tsx +0 -1
  134. package/Tag/index.tsx +1 -5
  135. package/Tag/types.ts +3 -3
  136. package/Text/index.tsx +3 -14
  137. package/Text/styles.ts +1 -1
  138. package/Text/types.ts +6 -1
  139. package/Text/utils.ts +4 -20
  140. package/Title/index.tsx +32 -41
  141. package/ViewBoundary/index.tsx +2 -4
  142. package/index.ts +6 -0
  143. package/package.json +1 -1
@@ -23,10 +23,6 @@ const SCREEN_PADDING = 12;
23
23
  const BACK_WIDTH = 28;
24
24
  const TOOLKIT_WIDTH = 73;
25
25
 
26
- /**
27
- * Extended header background: a gradient that collapses on scroll and, with
28
- * `inputSearchProps`, a full-width search that slides into the bar beside back/toolkit.
29
- */
30
26
  const HeaderExtendHeader: React.FC<{
31
27
  headerType?: HeaderType;
32
28
  animatedValue: AnimatedCompatValue;
@@ -68,15 +64,18 @@ const HeaderExtendHeader: React.FC<{
68
64
  const contentLeft = layout.insetLeft + SCREEN_PADDING;
69
65
  const contentRight = layout.insetLeft + layout.contentWidth;
70
66
  // collapsed on the status bar row, the search stays between its clusters
71
- const clearanceLeft = layout.statusBarClearance?.left ?? 0;
72
- const clearanceRight = layout.statusBarClearance?.right ?? 0;
73
- const collapsedLeft = layout.insetLeft + leftPosition + clearanceLeft;
74
- const collapsedRight = contentRight - clearanceRight - headerRightWidth;
67
+ const collapsedLeft =
68
+ layout.insetLeft + leftPosition + (layout.statusBarClearance?.left ?? 0);
69
+ const collapsedRight = contentRight - (layout.statusBarClearance?.right ?? 0);
75
70
  const searchWidth = layout.contentWidth - SCREEN_PADDING * 2;
76
- const collapsedSearchWidth = collapsedRight - collapsedLeft - SCREEN_PADDING;
71
+ const collapsedSearchWidth =
72
+ collapsedRight - collapsedLeft - SCREEN_PADDING - headerRightWidth;
77
73
  // a row taller than barHeight (iPhone Duo) centers the collapsed search on the title
78
- const titleRowHeight = layout.headerHeight - layout.headerStatusBarHeight;
79
- const collapsedRaise = (titleRowHeight - HeaderMetrics.barHeight) / 2;
74
+ const collapsedRaise =
75
+ (layout.headerHeight -
76
+ layout.headerStatusBarHeight -
77
+ HeaderMetrics.barHeight) /
78
+ 2;
80
79
 
81
80
  let useShadowHeader = useShadowHeaderProp;
82
81
  if (inputSearchProps && Platform.OS === 'android') {
@@ -10,8 +10,7 @@ import { useScreenLayout, useSideBarItemStyle } from '../../Layout/utils';
10
10
  import { useSideBarCounterStyle } from './DuoQuickReturnHeader';
11
11
 
12
12
  /**
13
- * Header back button, also bound to the Android hardware back. `preventBack`
14
- * confirms in a popup first; the iPhone Duo layouts use the 48pt side bar button.
13
+ * default header left used for nav
15
14
  */
16
15
  const HeaderLeft: React.FC<HeaderBackProps> = ({
17
16
  tintColor,
@@ -9,8 +9,7 @@ import { HeaderToolkitAction } from './HeaderToolkitAction';
9
9
  import { styles } from './HeaderRight.styles';
10
10
 
11
11
  /**
12
- * Header right slot: a custom `headerRight` render function, the onboarding skip
13
- * button, or the toolkit (stacked vertically in the floating iPhone Duo side bar).
12
+ * main component for header right
14
13
  */
15
14
  const HeaderRight: React.FC<any> = props => {
16
15
  const { translate } = useContext(ApplicationContext);
@@ -4,10 +4,6 @@ import { Spacing } from '../../Consts';
4
4
  import { isWhiteListDesignSystem } from '../utils';
5
5
  import { useScreenLayout } from '../../Layout';
6
6
 
7
- /**
8
- * Header title font: MoMoTrustDisplay for design-system whitelisted mini apps,
9
- * SFProText otherwise.
10
- */
11
7
  const useHeaderFontFamily = (): string => {
12
8
  const context = useContext<any>(MiniAppContext);
13
9
  return isWhiteListDesignSystem(context) ? 'MoMoTrustDisplay' : 'SFProText';
@@ -24,22 +20,24 @@ const HEADER_ACTIONS_WIDTH = 172;
24
20
  const STATUS_BAR_ROW_ACTIONS_EXTRA = 55;
25
21
 
26
22
  /**
27
- * Max header title width. The iPhone Duo side bar takes back and toolkit out of
28
- * the bar, so the title gets the content width; otherwise it shares the bar with them.
23
+ * max title width, the iPhone Duo side bar takes back and toolkit out of the bar
29
24
  */
30
25
  const useTitleMaxWidth = () => {
31
- const layout = useScreenLayout();
32
- const { width, contentWidth, sideBar, statusBarClearance } = layout;
26
+ const { width, contentWidth, sideBar, statusBarClearance } =
27
+ useScreenLayout();
33
28
  let clearance = 0;
34
- let rowActionsExtra = 0;
35
29
  if (statusBarClearance) {
36
30
  clearance = statusBarClearance.left + statusBarClearance.right;
37
- rowActionsExtra = STATUS_BAR_ROW_ACTIONS_EXTRA;
38
31
  }
39
32
  if (sideBar) {
40
33
  return contentWidth - Spacing.M * 2 - clearance;
41
34
  }
42
- return width - HEADER_ACTIONS_WIDTH - rowActionsExtra - clearance;
35
+ return (
36
+ width -
37
+ HEADER_ACTIONS_WIDTH -
38
+ (statusBarClearance ? STATUS_BAR_ROW_ACTIONS_EXTRA : 0) -
39
+ clearance
40
+ );
43
41
  };
44
42
 
45
43
  export { useHeaderFontFamily, HEADER_ACTIONS_WIDTH, useTitleMaxWidth };
@@ -33,8 +33,7 @@ type HeaderTitleExtraProps = {
33
33
  };
34
34
 
35
35
  /**
36
- * Default navigation header title. With `animatedValue` its opacity follows the
37
- * scroll through `interpolate` (0 to 1 over the first 200pt by default).
36
+ * default header title used for nav
38
37
  */
39
38
  const HeaderTitle: React.FC<
40
39
  HeaderTitleExtraProps & { [key: string]: any }
@@ -101,8 +100,8 @@ const HeaderTitle: React.FC<
101
100
  };
102
101
 
103
102
  /**
104
- * Header title picked by `type` (user, location or journey) with `data` as its
105
- * props; an empty view for an unknown type.
103
+ * Title custom for header
104
+ * @constructor
106
105
  */
107
106
  const TitleCustom: React.FC<any> = ({ type, data, ...props }) => {
108
107
  switch (type) {
@@ -14,8 +14,7 @@ const CLOSE_ICON = '16_navigation_close_circle';
14
14
  const HOME_ICON = '16_basic_home';
15
15
 
16
16
  /**
17
- * Header toolkit: optional shortcut plus the help center / close capsule. When
18
- * `vertical` (iPhone Duo side bar) it reports its height and may fold into one button.
17
+ * Header toolkit action
19
18
  */
20
19
  const HeaderToolkitAction: React.FC<any> = ({
21
20
  tintColor,
@@ -28,9 +27,8 @@ const HeaderToolkitAction: React.FC<any> = ({
28
27
  vertical = false,
29
28
  }) => {
30
29
  const context = useContext<any>(MiniAppContext);
31
- const compression = useDuoBarCompression();
32
- const { toolbarCompressed, toolbarHidden } = compression;
33
- const { expandToolbar, setToolbarHeight } = compression;
30
+ const { toolbarCompressed, toolbarHidden, expandToolbar, setToolbarHeight } =
31
+ useDuoBarCompression();
34
32
  const { statusBarClearance } = useScreenLayout();
35
33
  const {
36
34
  isLoading,
@@ -58,22 +56,18 @@ const HeaderToolkitAction: React.FC<any> = ({
58
56
 
59
57
  const useHelpCenter = context.appId !== 'vn.momo.helpcenter';
60
58
  const closeIcon = useCloseIcon ? CLOSE_ICON : HOME_ICON;
61
- let toolkitWidthStyle: ViewStyle = styles.toolkitNarrow;
62
- if (useHelpCenter) {
63
- toolkitWidthStyle = styles.toolkitWide;
64
- }
65
59
 
66
60
  const groupCount = useHelpCenter ? 2 : 1;
67
61
  let capsuleHeight = DuoMetrics.controlSize;
68
62
  if (groupCount > 1) {
69
63
  capsuleHeight = groupCount * DuoMetrics.itemPitch;
70
64
  }
71
- const backSlotHeight = DuoMetrics.controlSize + Spacing.M;
72
- let shortcutHeight = 0;
73
- if (useShortcut) {
74
- shortcutHeight = DuoMetrics.controlSize + Spacing.M;
75
- }
76
- const fullToolbarHeight = backSlotHeight + shortcutHeight + capsuleHeight;
65
+ // Back slot, the optional shortcut, then the help/home capsule
66
+ const fullToolbarHeight =
67
+ DuoMetrics.controlSize +
68
+ Spacing.M +
69
+ (useShortcut ? DuoMetrics.controlSize + Spacing.M : 0) +
70
+ capsuleHeight;
77
71
 
78
72
  useEffect(() => {
79
73
  if (vertical) {
@@ -86,7 +80,8 @@ const HeaderToolkitAction: React.FC<any> = ({
86
80
  return null;
87
81
  }
88
82
 
89
- // HIG "Toolbar compressed": a menu icon (not the "more" one) that expands the toolbar
83
+ // HIG "Toolbar compressed": tools fold into one button; tapping it expands the
84
+ // toolbar and compresses the tab bar instead. Not the useMore icon, to tell them apart
90
85
  if (vertical && toolbarCompressed) {
91
86
  return (
92
87
  <SideBarButton
@@ -175,7 +170,15 @@ const HeaderToolkitAction: React.FC<any> = ({
175
170
  showBadge={showBadge}
176
171
  />
177
172
  )}
178
- <View style={[styles.toolkitContainer, buttonStyle, toolkitWidthStyle]}>
173
+ <View
174
+ style={[
175
+ styles.toolkitContainer,
176
+ buttonStyle,
177
+ context.appId !== 'vn.momo.helpcenter'
178
+ ? styles.toolkitWide
179
+ : styles.toolkitNarrow,
180
+ ]}
181
+ >
179
182
  {useHelpCenter && (
180
183
  <>
181
184
  <TouchableOpacity
@@ -7,8 +7,7 @@ import { Icon } from '../../Icon';
7
7
  import { BadgeDot } from '../../Badge';
8
8
 
9
9
  /**
10
- * Round 28pt icon button of the header (back, shortcut) with an optional badge
11
- * dot; switches to a dark fill under a white tint so the icon stays readable.
10
+ * default navigation button used header nav
12
11
  */
13
12
  const NavigationButton: React.FC<NavigationButtonProps> = ({
14
13
  icon,
@@ -10,10 +10,6 @@ import { useNormalizedSharedValue } from '../hooks';
10
10
  import { useScreenLayout } from '../../Layout';
11
11
  import { DuoMetrics } from '../../Layout/utils';
12
12
 
13
- /**
14
- * Header search bar whose fill turns from surface to default once scrolled, with
15
- * a cancel button that goes back (or dismisses the mini app) unless `renderButtons`.
16
- */
17
13
  const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
18
14
  (
19
15
  {
@@ -40,9 +36,9 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
40
36
  if (layout.sideBar) {
41
37
  searchMaxWidth = layout.contentWidth - Spacing.M * 2 - headerRightWidth;
42
38
  }
43
- const clearance = layout.statusBarClearance;
44
- if (clearance) {
45
- searchMaxWidth -= clearance.left + clearance.right;
39
+ if (layout.statusBarClearance) {
40
+ searchMaxWidth -=
41
+ layout.statusBarClearance.left + layout.statusBarClearance.right;
46
42
  }
47
43
 
48
44
  const backgroundStyle = useAnimatedStyle(() => {
@@ -80,15 +76,6 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
80
76
 
81
77
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
82
78
 
83
- let buttons: React.ReactNode = (
84
- <TouchableOpacity onPress={goBackSafe} style={styles.searchAction}>
85
- <Text typography={'action_default_bold'}>{buttonText}</Text>
86
- </TouchableOpacity>
87
- );
88
- if (typeof renderButtons === 'function') {
89
- buttons = renderButtons();
90
- }
91
-
92
79
  return (
93
80
  <View
94
81
  style={[styles.container, showBaseLineDebug && styles.debugBaseLine]}
@@ -110,7 +97,13 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
110
97
  />
111
98
  </Animated.View>
112
99
  </View>
113
- {buttons}
100
+ {renderButtons && typeof renderButtons === 'function' ? (
101
+ renderButtons()
102
+ ) : (
103
+ <TouchableOpacity onPress={goBackSafe} style={styles.searchAction}>
104
+ <Text typography={'action_default_bold'}>{buttonText}</Text>
105
+ </TouchableOpacity>
106
+ )}
114
107
  </View>
115
108
  );
116
109
  },
@@ -14,8 +14,8 @@ import {
14
14
  } from './HeaderTitle.shared';
15
15
 
16
16
  /**
17
- * Header title for a journey: `start` and `end` around an icon, with an optional
18
- * description below; a shimmer while `isLoading`.
17
+ * Header user title
18
+ * @constructor
19
19
  */
20
20
  const TitleJourney: React.FC<TitleJourneyProps> = ({
21
21
  start,
@@ -9,8 +9,8 @@ import { styles } from './HeaderTitle.styles';
9
9
  import { useHeaderFontFamily, useTitleMaxWidth } from './HeaderTitle.shared';
10
10
 
11
11
  /**
12
- * Header title showing a location with a dropdown chevron and an optional
13
- * description above it; a shimmer while `isLoading`.
12
+ * Header user title
13
+ * @constructor
14
14
  */
15
15
  const TitleLocation: React.FC<TitleLocationProps> = ({
16
16
  tintColor,
@@ -10,8 +10,8 @@ import { styles } from './HeaderTitle.styles';
10
10
  import { useHeaderFontFamily, useTitleMaxWidth } from './HeaderTitle.shared';
11
11
 
12
12
  /**
13
- * Header title for a user or group: avatar (1 to 4 images, a `+N` stack, or name
14
- * initials), title with verified icons and subtitle; a shimmer while `isLoading`.
13
+ * Header user title
14
+ * @constructor
15
15
  */
16
16
  const TitleUser: React.FC<TitleUserProps> = ({
17
17
  title,
@@ -15,8 +15,7 @@ type ToolkitActionsParams = {
15
15
  };
16
16
 
17
17
  /**
18
- * State and handlers behind the header toolkit: the favorite shortcut (kept in sync
19
- * with the home), single tool or "more" shortcut, help center and close.
18
+ * state and handlers behind the header toolkit buttons
20
19
  */
21
20
  const useToolkitActions = ({
22
21
  preventClose,
@@ -32,15 +31,20 @@ const useToolkitActions = ({
32
31
  const [isFavorite, setIsFavorite] = useState<boolean>(false);
33
32
  const [isLoading, setIsLoading] = useState(false);
34
33
 
35
- const checkAppIsFavorite = useCallback(() => {
36
- return navigator?.maxApi?.dispatchFunction?.(
37
- 'isFavoriteApp',
38
- { code: context?.code },
39
- (result: boolean) => {
40
- setIsFavorite(result);
41
- },
42
- );
43
- }, [context?.code, navigator?.maxApi]);
34
+ /**
35
+ * check app is favorite
36
+ */
37
+ const checkAppIsFavorite = useCallback(
38
+ () =>
39
+ navigator?.maxApi?.dispatchFunction?.(
40
+ 'isFavoriteApp',
41
+ { code: context?.code },
42
+ (result: boolean) => {
43
+ setIsFavorite(result);
44
+ },
45
+ ),
46
+ [context?.code, navigator?.maxApi],
47
+ );
44
48
 
45
49
  useEffect(() => {
46
50
  if (useShortcut) {
@@ -71,6 +75,9 @@ const useToolkitActions = ({
71
75
  }
72
76
  };
73
77
 
78
+ /**
79
+ * close navigation container
80
+ */
74
81
  const onClose = () => {
75
82
  const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
76
83
  context?.autoTracking?.({
@@ -100,6 +107,9 @@ const useToolkitActions = ({
100
107
  }
101
108
  };
102
109
 
110
+ /**
111
+ * on press shortcut
112
+ */
103
113
  const onPressShortcut = () => {
104
114
  const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
105
115
  const expected = !isFavorite;
@@ -129,6 +139,9 @@ const useToolkitActions = ({
129
139
  }, 1000);
130
140
  };
131
141
 
142
+ /**
143
+ * on press help center
144
+ */
132
145
  const onPressHelpCenter = () => {
133
146
  const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
134
147
  context?.autoTracking?.({
@@ -141,6 +154,9 @@ const useToolkitActions = ({
141
154
  navigator?.maxApi?.dispatchFunction?.('showHelpCenter', context);
142
155
  };
143
156
 
157
+ /**
158
+ * on press icon more
159
+ */
144
160
  const onPressIconMore = () => {
145
161
  const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
146
162
  context?.autoTracking?.({
@@ -184,10 +200,9 @@ const useToolkitActions = ({
184
200
  iconShortcut = 'navigation_more_icon';
185
201
  shortcutOnPress = onPressIconMore;
186
202
  } else if (isHaveOneTool) {
187
- const singleToolGroup = tools.find(
203
+ const singleTool = tools.find(
188
204
  (group: ToolGroup) => group?.items?.length === 1,
189
- );
190
- const singleTool = singleToolGroup?.items[0];
205
+ )?.items[0];
191
206
  iconShortcut = singleTool?.icon;
192
207
  shortcutOnPress = () => {
193
208
  singleTool?.onPress?.();
@@ -201,9 +216,9 @@ const useToolkitActions = ({
201
216
  };
202
217
  }
203
218
 
204
- const showBadge = tools.some((group: ToolGroup) => {
205
- return group?.items?.some?.(tool => tool?.showBadge);
206
- });
219
+ const showBadge = tools.some((group: ToolGroup) =>
220
+ group?.items?.some?.(tool => tool?.showBadge),
221
+ );
207
222
 
208
223
  return {
209
224
  isLoading,
@@ -0,0 +1,119 @@
1
+ import React, { useState, useSyncExternalStore } from 'react';
2
+ import { Share, TouchableOpacity, View } from 'react-native';
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
+
5
+ import { Colors, Spacing } from '../../Consts';
6
+ import { Text } from '../../Text';
7
+ import { adoptionStore } from './AdoptionStore';
8
+ import { styles } from './styles';
9
+ import type { ScreenSnapshot } from './types';
10
+
11
+ /** Subscribes a React listener to the adoption store. Stable identity for useSyncExternalStore. */
12
+ const subscribeToStore = (listener: () => void): (() => void) =>
13
+ adoptionStore.subscribe(listener);
14
+
15
+ const getLatestSnapshot = (): ScreenSnapshot | undefined =>
16
+ adoptionStore.getLatest();
17
+
18
+ const getScreenCount = (): number => adoptionStore.getAll().length;
19
+
20
+ /** Share of classified nodes that are app-authored kit: K / (K + R + O). */
21
+ const getAdoptionRate = (snapshot: ScreenSnapshot): number => {
22
+ const { K, R, O } = snapshot.counts;
23
+ const total = K + R + O;
24
+ return total === 0 ? 0 : K / total;
25
+ };
26
+
27
+ const formatSummary = (snapshot: ScreenSnapshot | undefined): string => {
28
+ if (!snapshot) return 'Kit adoption: chưa có dữ liệu';
29
+ const { K, R, O } = snapshot.counts;
30
+ const percent = Math.round(getAdoptionRate(snapshot) * 100);
31
+ return `Kit ${percent}% · K${K} R${R} O${O}`;
32
+ };
33
+
34
+ const handleRemeasure = (): void => {
35
+ adoptionStore.remeasureActive();
36
+ };
37
+
38
+ const handleShare = (): void => {
39
+ Share.share({ message: JSON.stringify(adoptionStore.getAll()) }).catch(
40
+ () => undefined,
41
+ );
42
+ };
43
+
44
+ const handleClear = (): void => {
45
+ adoptionStore.clear();
46
+ };
47
+
48
+ /**
49
+ * Dev-only floating badge showing the latest screen's kit adoption. Tap to
50
+ * expand actions (re-measure, share JSON, clear). Renders nothing in release builds.
51
+ */
52
+ const AdoptionBadge: React.FC = () => {
53
+ const insets = useSafeAreaInsets();
54
+ const [isExpanded, setExpanded] = useState(false);
55
+ const latest = useSyncExternalStore(subscribeToStore, getLatestSnapshot);
56
+ const screenCount = useSyncExternalStore(subscribeToStore, getScreenCount);
57
+
58
+ if (!__DEV__) return null;
59
+
60
+ const handleToggle = (): void => setExpanded(prev => !prev);
61
+
62
+ return (
63
+ <View
64
+ pointerEvents="box-none"
65
+ style={[styles.wrapper, { top: insets.top + Spacing.S }]}
66
+ >
67
+ <TouchableOpacity
68
+ accessibilityRole="button"
69
+ onPress={handleToggle}
70
+ style={styles.badge}
71
+ >
72
+ <Text typography="label_xs_medium" color={Colors.black_01}>
73
+ {formatSummary(latest)}
74
+ </Text>
75
+ {latest ? (
76
+ <Text typography="description_xs_regular" color={Colors.black_01}>
77
+ {latest.screen_name}
78
+ </Text>
79
+ ) : null}
80
+ </TouchableOpacity>
81
+ {isExpanded ? (
82
+ <View style={styles.panel}>
83
+ <Text typography="label_xs_medium" color={Colors.black_17}>
84
+ {`Đã đo ${screenCount} màn hình`}
85
+ </Text>
86
+ <TouchableOpacity
87
+ accessibilityRole="button"
88
+ onPress={handleRemeasure}
89
+ style={styles.action}
90
+ >
91
+ <Text typography="label_xs_medium" color={Colors.blue_03}>
92
+ Đo lại
93
+ </Text>
94
+ </TouchableOpacity>
95
+ <TouchableOpacity
96
+ accessibilityRole="button"
97
+ onPress={handleShare}
98
+ style={styles.action}
99
+ >
100
+ <Text typography="label_xs_medium" color={Colors.blue_03}>
101
+ Share JSON
102
+ </Text>
103
+ </TouchableOpacity>
104
+ <TouchableOpacity
105
+ accessibilityRole="button"
106
+ onPress={handleClear}
107
+ style={styles.actionOutline}
108
+ >
109
+ <Text typography="label_xs_medium" color={Colors.black_17}>
110
+ Xoá
111
+ </Text>
112
+ </TouchableOpacity>
113
+ </View>
114
+ ) : null}
115
+ </View>
116
+ );
117
+ };
118
+
119
+ export { AdoptionBadge };