@momo-kits/foundation 0.169.5-beta.1 → 0.169.5

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 (138) hide show
  1. package/Application/BottomSheet.hooks.ts +8 -8
  2. package/Application/BottomSheet.parts.tsx +44 -34
  3. package/Application/BottomSheet.tsx +40 -37
  4. package/Application/BottomTab/ActiveIndicator.tsx +15 -14
  5. package/Application/BottomTab/BottomTabBar.tsx +43 -34
  6. package/Application/BottomTab/BottomTabInset.tsx +8 -4
  7. package/Application/BottomTab/CurvedContainer.tsx +14 -12
  8. package/Application/BottomTab/CustomBottomTabItem.tsx +10 -6
  9. package/Application/BottomTab/DuoBarCompression.tsx +13 -19
  10. package/Application/BottomTab/DuoRailTabBar.tsx +13 -13
  11. package/Application/BottomTab/DuoTabBar.tsx +8 -9
  12. package/Application/BottomTab/TabBarIcon.tsx +13 -8
  13. package/Application/BottomTab/TabScreen.tsx +1 -5
  14. package/Application/BottomTab/index.tsx +38 -30
  15. package/Application/BottomTab/metrics.ts +21 -20
  16. package/Application/BottomTab/tabItems.ts +1 -1
  17. package/Application/BottomTab/usePressLift.ts +2 -2
  18. package/Application/Components/BackgroundImageView.tsx +4 -0
  19. package/Application/Components/DuoGlassBackground.tsx +5 -4
  20. package/Application/Components/HeaderAnimated.tsx +4 -0
  21. package/Application/Components/HeaderBackground.tsx +4 -0
  22. package/Application/Components/HeaderExtendHeader.tsx +11 -10
  23. package/Application/Components/HeaderLeft.tsx +2 -1
  24. package/Application/Components/HeaderRight.tsx +2 -1
  25. package/Application/Components/HeaderTitle.shared.ts +11 -9
  26. package/Application/Components/HeaderTitle.tsx +4 -3
  27. package/Application/Components/HeaderToolkitAction.tsx +17 -20
  28. package/Application/Components/NavigationButton.tsx +2 -1
  29. package/Application/Components/SearchHeader.tsx +17 -10
  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 +17 -32
  34. package/Application/Localize.ts +7 -0
  35. package/Application/ModalScreen.tsx +10 -10
  36. package/Application/Navigation.ts +14 -16
  37. package/Application/NavigationContainer.tsx +19 -18
  38. package/Application/Navigator.ts +20 -48
  39. package/Application/StackScreen.tracking.ts +11 -26
  40. package/Application/StackScreen.tsx +9 -39
  41. package/Application/TooltipPortal.tsx +12 -5
  42. package/Application/WidgetContainer.tsx +10 -4
  43. package/Application/modalLayout.ts +13 -14
  44. package/Application/navigationOptions.tsx +21 -17
  45. package/Badge/Badge.tsx +5 -0
  46. package/Badge/BadgeDot.tsx +1 -0
  47. package/Badge/BadgeDotAnimation.tsx +15 -7
  48. package/Badge/BadgeRibbon.tsx +46 -38
  49. package/Badge/Shape.tsx +2 -0
  50. package/Badge/types.ts +2 -2
  51. package/Button/index.tsx +5 -16
  52. package/Button/utils.ts +9 -15
  53. package/CheckBox/index.tsx +4 -0
  54. package/Consts/theme.ts +10 -10
  55. package/Context/index.ts +1 -0
  56. package/Divider/DashDivider.tsx +1 -0
  57. package/Divider/index.tsx +4 -0
  58. package/FoundationList/index.tsx +4 -0
  59. package/Icon/index.tsx +6 -6
  60. package/Icon/types.ts +1 -1
  61. package/IconButton/index.tsx +4 -12
  62. package/Image/index.tsx +9 -7
  63. package/Input/Input.tsx +8 -9
  64. package/Input/InputDropDown.tsx +10 -12
  65. package/Input/InputMoney.tsx +12 -8
  66. package/Input/InputMoneyView.tsx +14 -7
  67. package/Input/InputOTP.tsx +35 -33
  68. package/Input/InputPhoneNumber.tsx +8 -9
  69. package/Input/InputSearch.tsx +4 -6
  70. package/Input/InputTextArea.tsx +4 -0
  71. package/Input/SearchTextTyping.tsx +4 -0
  72. package/Input/SystemTextInput.tsx +7 -2
  73. package/Input/TextTyping.tsx +4 -0
  74. package/Input/common.tsx +26 -12
  75. package/Input/index.tsx +17 -20
  76. package/Input/moneyUtils.ts +10 -4
  77. package/Input/styles.ts +0 -5
  78. package/Input/utils.ts +14 -12
  79. package/Layout/Card.tsx +7 -4
  80. package/Layout/FloatingButton.tsx +4 -0
  81. package/Layout/Item.tsx +6 -2
  82. package/Layout/ItemList.tsx +9 -7
  83. package/Layout/ItemSectionList.tsx +2 -1
  84. package/Layout/ReservedRegions.tsx +25 -25
  85. package/Layout/Screen.tsx +56 -58
  86. package/Layout/Section.tsx +8 -5
  87. package/Layout/TrackingScope.tsx +1 -0
  88. package/Layout/screenContent.tsx +4 -2
  89. package/Layout/screenFooter.tsx +28 -18
  90. package/Layout/screenHeader.tsx +34 -31
  91. package/Layout/screenQuickReturn.tsx +17 -20
  92. package/Layout/screenScroll.ts +4 -7
  93. package/Layout/screenTypes.ts +3 -2
  94. package/Layout/types.ts +2 -4
  95. package/Layout/utils.ts +31 -37
  96. package/Loader/DotLoader.tsx +4 -0
  97. package/Loader/Loader.tsx +4 -0
  98. package/Loader/ProgressBar.tsx +1 -0
  99. package/Loader/Spinner.tsx +4 -0
  100. package/Loader/utils.ts +11 -7
  101. package/Pagination/Dot.tsx +4 -0
  102. package/Pagination/PaginationDot.tsx +1 -0
  103. package/Pagination/PaginationNumber.tsx +5 -3
  104. package/Pagination/PaginationScroll.tsx +5 -0
  105. package/Pagination/PaginationWhiteDot.tsx +1 -0
  106. package/Pagination/index.tsx +4 -0
  107. package/Pagination/types.ts +2 -2
  108. package/Popup/PopupNotify.tsx +44 -48
  109. package/Popup/PopupPromotion.tsx +4 -7
  110. package/Popup/types.ts +4 -4
  111. package/Popup/utils.ts +4 -0
  112. package/Radio/index.tsx +4 -0
  113. package/Skeleton/index.tsx +8 -0
  114. package/Switch/index.tsx +1 -0
  115. package/Tag/index.tsx +5 -1
  116. package/Tag/types.ts +3 -3
  117. package/Text/index.tsx +14 -3
  118. package/Text/styles.ts +1 -1
  119. package/Text/types.ts +1 -6
  120. package/Text/utils.ts +9 -0
  121. package/Title/index.tsx +41 -32
  122. package/ViewBoundary/index.tsx +4 -2
  123. package/index.ts +0 -6
  124. package/package.json +1 -1
  125. package/Application/KitAdoption/AdoptionBadge.tsx +0 -119
  126. package/Application/KitAdoption/AdoptionProbe.tsx +0 -145
  127. package/Application/KitAdoption/AdoptionStore.ts +0 -80
  128. package/Application/KitAdoption/Authorship/utils.ts +0 -137
  129. package/Application/KitAdoption/constants.ts +0 -118
  130. package/Application/KitAdoption/index.ts +0 -5
  131. package/Application/KitAdoption/styles.ts +0 -55
  132. package/Application/KitAdoption/types.ts +0 -48
  133. package/Application/KitAdoption/utils.ts +0 -375
  134. package/Application/__tests__/KitAdoption.integration.test.tsx +0 -235
  135. package/Application/__tests__/KitAdoption.test.ts +0 -283
  136. package/Application/__tests__/KitAdoptionAuthorship.test.ts +0 -132
  137. package/Application/__tests__/KitAdoptionStore.test.ts +0 -110
  138. package/Application/__tests__/modalLayout.test.ts +0 -128
package/Text/index.tsx CHANGED
@@ -111,6 +111,10 @@ const deprecatedValues: Typography[] = [
111
111
  'action_s',
112
112
  ];
113
113
 
114
+ /**
115
+ * Text style of a typography token with sizes scaled by `useScaleSize` and the
116
+ * font file name for its weight. Calls hooks, so it only runs during render.
117
+ */
114
118
  const TypoStyles = (typo: Typography, newFontFamily?: string) => {
115
119
  const { theme } = useContext(ApplicationContext);
116
120
  const styleSheet: {
@@ -158,9 +162,7 @@ const TypoStyles = (typo: Typography, newFontFamily?: string) => {
158
162
  fontSize: useScaleSize(fontSize),
159
163
  lineHeight: useScaleSize(lineHeight),
160
164
  fontStyle,
161
- // fontFamily da mang san weight. Tren Android, ReactFontManager doi
162
- // `fonts/<family>_bold.ttf` khi fontWeight >= 700; file do khong ton tai nen
163
- // font se roi ve font he thong. Bo fontWeight de Android dung dung file.
165
+ // Weight is in fontFamily; fontWeight >= 700 makes Android seek a missing <family>_bold.ttf.
164
166
  fontWeight: undefined,
165
167
  };
166
168
  };
@@ -169,6 +171,11 @@ const skeletonStyles = StyleSheet.create({
169
171
  overlay: { position: 'absolute' },
170
172
  });
171
173
 
174
+ /**
175
+ * Design-system text: renders a `typography` token in the theme font with
176
+ * scaled sizes and OS font scaling off. Warns on deprecated tokens and shows a
177
+ * skeleton overlay while a SkeletonContext is loading.
178
+ */
172
179
  const Text: React.FC<TextProps> = ({
173
180
  typography = 'body_default_regular',
174
181
  color,
@@ -228,6 +235,10 @@ const Text: React.FC<TextProps> = ({
228
235
  );
229
236
  };
230
237
 
238
+ /**
239
+ * Theme font family for a loose weight name ('bold', 'semibold', 'regular').
240
+ * The `typography` argument is ignored; it calls hooks, so use it during render.
241
+ */
231
242
  const exportFontFamily = (
232
243
  fontWeight: string,
233
244
  typography?: Typography,
package/Text/styles.ts CHANGED
@@ -39,7 +39,7 @@ export default {
39
39
  action_s_bold: { fontSize: 14, lineHeight: 20, fontWeight: '700' },
40
40
  action_xs_bold: { fontSize: 12, lineHeight: 18, fontWeight: '700' },
41
41
  action_xxs_bold: { fontSize: 10, lineHeight: 14, fontWeight: '700' },
42
- /**non-support**/
42
+ // Legacy tokens kept for old callers; not in the current type scale.
43
43
  headline_default: {
44
44
  fontSize: 28,
45
45
  lineHeight: 34,
package/Text/types.ts CHANGED
@@ -14,9 +14,7 @@ export type TypographyWeight = {
14
14
  };
15
15
 
16
16
  export type Typography =
17
- /**
18
- * @deprecated
19
- **/
17
+ // Deprecated up to 'action_s': Text warns at runtime when one is used.
20
18
  | 'headline_default'
21
19
  | 'headline_l'
22
20
  | 'headline_xl'
@@ -29,9 +27,6 @@ export type Typography =
29
27
  | 'paragraph_italic'
30
28
  | 'label_xxs'
31
29
  | 'action_s'
32
- /**
33
- * end
34
- **/
35
30
  | 'headline_l_bold'
36
31
  | 'headline_default_bold'
37
32
  | 'header_m_bold'
package/Text/utils.ts CHANGED
@@ -6,6 +6,11 @@ const DEFAULT_SCREEN_SIZE = 375;
6
6
  const MAX_FONT_SCALE = 1.5;
7
7
  const MAX_DEVICE_SCALE = 5;
8
8
 
9
+ /**
10
+ * Scales `size` by screen width (at most +5) and OS font scale (at most
11
+ * `scaleRate`, default 1.5), taking the larger; a mini app can opt out of the
12
+ * OS rate and use its own `fontScale.userScaleRate` instead.
13
+ */
9
14
  const useScaleSize = (size: number, scaleRate?: number) => {
10
15
  const { width } = useWindowDimensions();
11
16
  const context = useContext<any>(MiniAppContext);
@@ -37,6 +42,10 @@ const useScaleSize = (size: number, scaleRate?: number) => {
37
42
  return Math.max(fontSizeDeviceScale, fontSizeOSScale);
38
43
  };
39
44
 
45
+ /**
46
+ * Non-hook `useScaleSize` with the default cap and no mini app rate, for use
47
+ * outside render. Reads the window width at call time, so it misses resizes.
48
+ */
40
49
  const scaleSize = (size: number) => {
41
50
  const fontScale = PixelRatio.getFontScale();
42
51
  const deviceScale = Dimensions.get('window').width / DEFAULT_SCREEN_SIZE;
package/Title/index.tsx CHANGED
@@ -15,6 +15,11 @@ import styles from './styles';
15
15
  import { TitleProps } from './types';
16
16
  import { Typography } from '../Text/types';
17
17
 
18
+ /**
19
+ * Section or card heading with optional icon, badge, description, trailing
20
+ * chevron and right action (chevron, or a text button when `buttonTitle` is
21
+ * set). `textOnly` renders the title text alone.
22
+ */
18
23
  const Title: FC<TitleProps> = ({
19
24
  accessibilityLabel,
20
25
  title = 'Title',
@@ -43,7 +48,6 @@ const Title: FC<TitleProps> = ({
43
48
 
44
49
  const size18 = useScaleSize(18);
45
50
  const size22 = useScaleSize(22);
46
- // Dynamic styles using useScaleSize hook
47
51
  const dynamicStyles = {
48
52
  card_small: {
49
53
  fontSize: useScaleSize(14),
@@ -204,39 +208,44 @@ const Title: FC<TitleProps> = ({
204
208
 
205
209
  const lineHeight = styleSheet[typography].lineHeight;
206
210
 
211
+ let action = (
212
+ <TouchableOpacity
213
+ onPress={onPressRightAction}
214
+ style={[
215
+ styles.iconRight,
216
+ {
217
+ backgroundColor: theme.colors.primary + '0F',
218
+ },
219
+ ]}
220
+ accessibilityLabel={componentId + '|label-right-action-touch'}
221
+ >
222
+ <Icon
223
+ source={'arrow_chevron_right_small'}
224
+ size={size22}
225
+ color={theme.colors.primary}
226
+ />
227
+ </TouchableOpacity>
228
+ );
229
+ if (buttonTitle) {
230
+ action = (
231
+ <TouchableOpacity
232
+ onPress={onPressRightAction}
233
+ accessibilityLabel={componentId + '|label-right-action-touch'}
234
+ >
235
+ <Text
236
+ color={theme.colors.primary}
237
+ typography={buttonTypo}
238
+ accessibilityLabel={componentId + '|label-right-action-text'}
239
+ >
240
+ {buttonTitle}
241
+ </Text>
242
+ </TouchableOpacity>
243
+ );
244
+ }
245
+
207
246
  return (
208
247
  <View style={[styles.actionRightView, { height: lineHeight }]}>
209
- {!buttonTitle ? (
210
- <TouchableOpacity
211
- onPress={onPressRightAction}
212
- style={[
213
- styles.iconRight,
214
- {
215
- backgroundColor: theme.colors.primary + '0F',
216
- },
217
- ]}
218
- accessibilityLabel={componentId + '|label-right-action-touch'}
219
- >
220
- <Icon
221
- source={'arrow_chevron_right_small'}
222
- size={size22}
223
- color={theme.colors.primary}
224
- />
225
- </TouchableOpacity>
226
- ) : (
227
- <TouchableOpacity
228
- onPress={onPressRightAction}
229
- accessibilityLabel={componentId + '|label-right-action-touch'}
230
- >
231
- <Text
232
- color={theme.colors.primary}
233
- typography={buttonTypo}
234
- accessibilityLabel={componentId + '|label-right-action-text'}
235
- >
236
- {buttonTitle}
237
- </Text>
238
- </TouchableOpacity>
239
- )}
248
+ {action}
240
249
  </View>
241
250
  );
242
251
  };
@@ -14,6 +14,10 @@ interface ViewBoundaryState {
14
14
  error: Error | null;
15
15
  }
16
16
 
17
+ /**
18
+ * Error boundary: renders `fallback` (or nothing) after a render error and
19
+ * reports it to native `MiniAppModule.onJSBoundaryCrash` when available.
20
+ */
17
21
  class ViewBoundary extends React.Component<
18
22
  ViewBoundaryProps,
19
23
  ViewBoundaryState
@@ -33,7 +37,6 @@ class ViewBoundary extends React.Component<
33
37
  this.props.onError?.(error, errorInfo);
34
38
 
35
39
  const context = (this as any).context ?? {};
36
- // check exist native module method onJSBoundaryCrash
37
40
  const nativeMethod = NativeModules?.MiniAppModule?.onJSBoundaryCrash;
38
41
  if (typeof nativeMethod === 'function') {
39
42
  try {
@@ -49,7 +52,6 @@ class ViewBoundary extends React.Component<
49
52
  componentStack: errorInfo?.componentStack?.substring(0, 500) ?? '',
50
53
  },
51
54
  });
52
- console.info('[ViewBoundary] onJSBoundaryCrash success!');
53
55
  } catch (_e) {
54
56
  console.warn('[ViewBoundary] onJSBoundaryCrash failed:', _e);
55
57
  }
package/index.ts CHANGED
@@ -1,14 +1,8 @@
1
- /**
2
- * native export
3
- */
4
1
  import { TouchableOpacity } from 'react-native';
5
2
 
6
3
  export * from 'react-native-linear-gradient';
7
4
  export * from 'react-native-safe-area-context';
8
5
 
9
- /**
10
- * foundation component
11
- */
12
6
  export * from './Consts';
13
7
  export * from './Context';
14
8
  export * from './Layout';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.169.5-beta.1",
3
+ "version": "0.169.5",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},
@@ -1,119 +0,0 @@
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 };
@@ -1,145 +0,0 @@
1
- import React from 'react';
2
- import { InteractionManager, Platform } from 'react-native';
3
-
4
- import { MiniAppContext } from '../../Context';
5
- import { adoptionStore } from './AdoptionStore';
6
- import { SETTLE_MS } from './constants';
7
- import type {
8
- FiberLike,
9
- KitRegistries,
10
- ScreenSnapshot,
11
- WalkResult,
12
- } from './types';
13
- import {
14
- createRegistries,
15
- getAppId,
16
- getCurrentFiber,
17
- walkScreen,
18
- } from './utils';
19
-
20
- type NavigationLike = {
21
- addListener?: (event: string, callback: () => void) => () => void;
22
- };
23
-
24
- type ProbeProps = {
25
- screenName: string;
26
- navigation?: NavigationLike;
27
- children: React.ReactNode;
28
- };
29
-
30
- let registriesCache: KitRegistries | undefined;
31
-
32
- /**
33
- * Kit and RN identity sets, built on first use. Lazy require breaks the cycle
34
- * between the foundation index and the Application screens that render this probe.
35
- */
36
- const getRegistries = (): KitRegistries => {
37
- if (!registriesCache) {
38
- const kitModule = require('../../index') as Record<string, unknown>;
39
- const rnModule = require('react-native') as Record<string, unknown>;
40
- registriesCache = createRegistries([kitModule], rnModule);
41
- }
42
- return registriesCache;
43
- };
44
-
45
- /** Foundation version from its package.json, or undefined if it cannot be read. */
46
- const readFoundationVersion = (): string | undefined => {
47
- try {
48
- const pkg = require('../../package.json') as { version?: string };
49
- return pkg.version;
50
- } catch {
51
- return undefined;
52
- }
53
- };
54
-
55
- const createErrorResult = (): WalkResult => ({
56
- counts: { K: 0, R: 0, O: 0 },
57
- kit: {},
58
- rn: {},
59
- other: {},
60
- top_transparent: [],
61
- fibers: 0,
62
- truncated: false,
63
- status: 'error',
64
- });
65
-
66
- /** Stops the walk at nested probes so each screen is measured on its own. */
67
- const isProbeFiber = (fiber: FiberLike): boolean =>
68
- fiber.type === AdoptionProbeDev;
69
-
70
- /** Passes children through with no measurement. Used in release builds. */
71
- const PassThrough = (props: ProbeProps): React.JSX.Element => (
72
- <>{props.children}</>
73
- );
74
-
75
- /**
76
- * Dev-only wrapper around a screen. Measures the screen's fiber tree after
77
- * the first interactions settle, and again on focus. Release builds use PassThrough.
78
- */
79
- class AdoptionProbeDev extends React.Component<ProbeProps> {
80
- static contextType = MiniAppContext;
81
-
82
- private timer: ReturnType<typeof setTimeout> | undefined;
83
- private interaction: { cancel: () => void } | undefined;
84
- private unsubscribeFocus: (() => void) | undefined;
85
-
86
- componentDidMount(): void {
87
- adoptionStore.setActive(this.measure);
88
- this.unsubscribeFocus = this.props.navigation?.addListener?.(
89
- 'focus',
90
- this.handleFocus,
91
- );
92
- this.interaction = InteractionManager.runAfterInteractions(() => {
93
- this.timer = setTimeout(this.measure, SETTLE_MS);
94
- });
95
- }
96
-
97
- componentWillUnmount(): void {
98
- if (this.timer !== undefined) clearTimeout(this.timer);
99
- this.interaction?.cancel();
100
- this.unsubscribeFocus?.();
101
- adoptionStore.clearActive(this.measure);
102
- }
103
-
104
- render(): React.ReactNode {
105
- return this.props.children;
106
- }
107
-
108
- private handleFocus = (): void => {
109
- adoptionStore.setActive(this.measure);
110
- };
111
-
112
- private measure = (): void => {
113
- const startedAt = Date.now();
114
- let result: WalkResult;
115
- try {
116
- const self = getCurrentFiber(
117
- (this as unknown as { _reactInternals: FiberLike })._reactInternals,
118
- );
119
- const root = self.child;
120
- if (!root) return;
121
- result = walkScreen(root, getRegistries(), isProbeFiber);
122
- } catch {
123
- result = createErrorResult();
124
- }
125
- this.save(result, startedAt);
126
- };
127
-
128
- private save(result: WalkResult, startedAt: number): void {
129
- const snapshot: ScreenSnapshot = {
130
- ...result,
131
- app_id: getAppId(this.context),
132
- screen_name: this.props.screenName,
133
- foundation_version: readFoundationVersion(),
134
- platform: Platform.OS,
135
- ts: Date.now(),
136
- walk_ms: Date.now() - startedAt,
137
- };
138
- adoptionStore.save(snapshot);
139
- }
140
- }
141
-
142
- const AdoptionProbe = __DEV__ ? AdoptionProbeDev : PassThrough;
143
-
144
- export type { NavigationLike, ProbeProps };
145
- export { AdoptionProbe, AdoptionProbeDev };
@@ -1,80 +0,0 @@
1
- import type { ScreenSnapshot } from './types';
2
-
3
- type Listener = () => void;
4
- type Measure = () => void;
5
-
6
- /** Total classified nodes in a snapshot, used to keep the richer measurement. */
7
- const getTotal = (snapshot: ScreenSnapshot): number =>
8
- snapshot.counts.K + snapshot.counts.R + snapshot.counts.O;
9
-
10
- const getKey = (snapshot: ScreenSnapshot): string =>
11
- `${snapshot.app_id}/${snapshot.screen_name}`;
12
-
13
- /**
14
- * In-memory, dev-only store of per-screen snapshots. Keeps one snapshot per
15
- * app and screen (the one with the most nodes) and holds the active measurer.
16
- */
17
- class AdoptionStore {
18
- private snapshots = new Map<string, ScreenSnapshot>();
19
- private listeners = new Set<Listener>();
20
- private latestKey: string | undefined;
21
- private activeMeasure: Measure | undefined;
22
-
23
- /** Stores a snapshot unless the stored one for the same screen is larger. */
24
- save(snapshot: ScreenSnapshot): void {
25
- const key = getKey(snapshot);
26
- const existing = this.snapshots.get(key);
27
- if (!existing || getTotal(existing) <= getTotal(snapshot)) {
28
- this.snapshots.set(key, snapshot);
29
- }
30
- this.latestKey = key;
31
- this.notify();
32
- }
33
-
34
- getAll(): ScreenSnapshot[] {
35
- return Array.from(this.snapshots.values());
36
- }
37
-
38
- /** The screen saved most recently, or undefined when nothing is saved. */
39
- getLatest(): ScreenSnapshot | undefined {
40
- return this.latestKey === undefined
41
- ? undefined
42
- : this.snapshots.get(this.latestKey);
43
- }
44
-
45
- clear(): void {
46
- this.snapshots.clear();
47
- this.latestKey = undefined;
48
- this.notify();
49
- }
50
-
51
- /** Registers a change listener and returns its unsubscribe function. */
52
- subscribe(listener: Listener): () => void {
53
- this.listeners.add(listener);
54
- return () => {
55
- this.listeners.delete(listener);
56
- };
57
- }
58
-
59
- setActive(measure: Measure): void {
60
- this.activeMeasure = measure;
61
- }
62
-
63
- /** Clears the active measurer only if it is still the given one. */
64
- clearActive(measure: Measure): void {
65
- if (this.activeMeasure === measure) this.activeMeasure = undefined;
66
- }
67
-
68
- remeasureActive(): void {
69
- this.activeMeasure?.();
70
- }
71
-
72
- private notify(): void {
73
- this.listeners.forEach(listener => listener());
74
- }
75
- }
76
-
77
- const adoptionStore = new AdoptionStore();
78
-
79
- export type { Listener, Measure };
80
- export { AdoptionStore, adoptionStore };