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

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 (128) hide show
  1. package/Application/BottomSheet.hooks.ts +8 -8
  2. package/Application/BottomSheet.parts.tsx +44 -34
  3. package/Application/BottomSheet.tsx +35 -26
  4. package/Application/BottomTab/ActiveIndicator.tsx +15 -14
  5. package/Application/BottomTab/BottomTabBar.styles.ts +1 -18
  6. package/Application/BottomTab/BottomTabBar.tsx +58 -57
  7. package/Application/BottomTab/BottomTabInset.tsx +8 -4
  8. package/Application/BottomTab/CurvedContainer.tsx +16 -20
  9. package/Application/BottomTab/CustomBottomTabItem.tsx +14 -15
  10. package/Application/BottomTab/DuoBarCompression.tsx +13 -19
  11. package/Application/BottomTab/DuoRailTabBar.tsx +20 -20
  12. package/Application/BottomTab/DuoTabBar.tsx +11 -24
  13. package/Application/BottomTab/TabBarIcon.tsx +15 -29
  14. package/Application/BottomTab/TabScreen.tsx +1 -5
  15. package/Application/BottomTab/index.tsx +33 -30
  16. package/Application/BottomTab/metrics.ts +25 -48
  17. package/Application/BottomTab/tabItems.ts +1 -1
  18. package/Application/BottomTab/usePressLift.ts +2 -2
  19. package/Application/Components/BackgroundImageView.tsx +4 -0
  20. package/Application/Components/DuoGlassBackground.tsx +5 -4
  21. package/Application/Components/HeaderAnimated.tsx +4 -0
  22. package/Application/Components/HeaderBackground.tsx +4 -0
  23. package/Application/Components/HeaderExtendHeader.tsx +11 -10
  24. package/Application/Components/HeaderLeft.tsx +2 -1
  25. package/Application/Components/HeaderRight.tsx +2 -1
  26. package/Application/Components/HeaderTitle.shared.ts +11 -9
  27. package/Application/Components/HeaderTitle.tsx +4 -3
  28. package/Application/Components/HeaderToolkitAction.tsx +17 -20
  29. package/Application/Components/NavigationButton.tsx +2 -1
  30. package/Application/Components/SearchHeader.tsx +17 -10
  31. package/Application/Components/TitleJourney.tsx +2 -2
  32. package/Application/Components/TitleLocation.tsx +2 -2
  33. package/Application/Components/TitleUser.tsx +2 -2
  34. package/Application/Components/useToolkitActions.tsx +17 -32
  35. package/Application/Localize.ts +7 -0
  36. package/Application/ModalScreen.tsx +9 -3
  37. package/Application/Navigation.ts +14 -16
  38. package/Application/NavigationContainer.tsx +19 -16
  39. package/Application/Navigator.ts +20 -48
  40. package/Application/ScaleSizeProvider.tsx +13 -6
  41. package/Application/StackScreen.tracking.ts +11 -26
  42. package/Application/StackScreen.tsx +8 -35
  43. package/Application/TooltipPortal.tsx +12 -5
  44. package/Application/WidgetContainer.tsx +10 -4
  45. package/Application/modalLayout.ts +13 -14
  46. package/Application/navigationOptions.tsx +21 -17
  47. package/Application/types.tabs.ts +1 -0
  48. package/Badge/Badge.tsx +5 -0
  49. package/Badge/BadgeDot.tsx +1 -0
  50. package/Badge/BadgeDotAnimation.tsx +15 -7
  51. package/Badge/BadgeRibbon.tsx +46 -38
  52. package/Badge/Shape.tsx +2 -0
  53. package/Badge/types.ts +2 -2
  54. package/Button/index.tsx +5 -16
  55. package/Button/utils.ts +9 -15
  56. package/CheckBox/index.tsx +4 -0
  57. package/Consts/theme.ts +10 -10
  58. package/Context/index.ts +5 -1
  59. package/Divider/DashDivider.tsx +1 -0
  60. package/Divider/index.tsx +4 -0
  61. package/FoundationList/index.tsx +4 -0
  62. package/Icon/index.tsx +6 -6
  63. package/Icon/types.ts +1 -1
  64. package/IconButton/index.tsx +4 -12
  65. package/Image/index.tsx +9 -7
  66. package/Input/Input.tsx +8 -9
  67. package/Input/InputDropDown.tsx +10 -12
  68. package/Input/InputMoney.tsx +12 -8
  69. package/Input/InputMoneyView.tsx +14 -7
  70. package/Input/InputOTP.tsx +35 -33
  71. package/Input/InputPhoneNumber.tsx +8 -9
  72. package/Input/InputSearch.tsx +4 -6
  73. package/Input/InputTextArea.tsx +4 -0
  74. package/Input/SearchTextTyping.tsx +4 -0
  75. package/Input/SystemTextInput.tsx +7 -2
  76. package/Input/TextTyping.tsx +4 -0
  77. package/Input/common.tsx +26 -12
  78. package/Input/index.tsx +17 -20
  79. package/Input/moneyUtils.ts +10 -4
  80. package/Input/styles.ts +0 -5
  81. package/Input/utils.ts +14 -12
  82. package/Layout/Card.tsx +7 -4
  83. package/Layout/FloatingButton.tsx +4 -0
  84. package/Layout/Item.tsx +6 -2
  85. package/Layout/ItemList.tsx +9 -7
  86. package/Layout/ItemSectionList.tsx +2 -1
  87. package/Layout/ReservedRegions.tsx +25 -25
  88. package/Layout/Screen.tsx +56 -58
  89. package/Layout/Section.tsx +8 -5
  90. package/Layout/TrackingScope.tsx +1 -0
  91. package/Layout/screenContent.tsx +4 -2
  92. package/Layout/screenFooter.tsx +28 -18
  93. package/Layout/screenHeader.tsx +34 -31
  94. package/Layout/screenQuickReturn.tsx +17 -20
  95. package/Layout/screenScroll.ts +4 -7
  96. package/Layout/screenTypes.ts +3 -2
  97. package/Layout/types.ts +2 -4
  98. package/Layout/utils.ts +31 -37
  99. package/Loader/DotLoader.tsx +4 -0
  100. package/Loader/Loader.tsx +4 -0
  101. package/Loader/ProgressBar.tsx +1 -0
  102. package/Loader/Spinner.tsx +4 -0
  103. package/Loader/utils.ts +11 -7
  104. package/Pagination/Dot.tsx +4 -0
  105. package/Pagination/PaginationDot.tsx +1 -0
  106. package/Pagination/PaginationNumber.tsx +5 -3
  107. package/Pagination/PaginationScroll.tsx +5 -0
  108. package/Pagination/PaginationWhiteDot.tsx +1 -0
  109. package/Pagination/index.tsx +4 -0
  110. package/Pagination/types.ts +2 -2
  111. package/Popup/PopupNotify.tsx +44 -48
  112. package/Popup/PopupPromotion.tsx +4 -7
  113. package/Popup/types.ts +4 -4
  114. package/Popup/utils.ts +4 -0
  115. package/Radio/index.tsx +4 -0
  116. package/Skeleton/index.tsx +8 -0
  117. package/Switch/index.tsx +1 -0
  118. package/Tag/index.tsx +5 -1
  119. package/Tag/types.ts +3 -3
  120. package/Text/index.tsx +14 -3
  121. package/Text/styles.ts +1 -1
  122. package/Text/types.ts +1 -6
  123. package/Text/utils.ts +20 -4
  124. package/Title/index.tsx +41 -32
  125. package/ViewBoundary/index.tsx +4 -2
  126. package/index.ts +0 -6
  127. package/package.json +1 -1
  128. package/Application/__tests__/modalLayout.test.ts +0 -128
@@ -2,6 +2,10 @@ import { NativeModules } from 'react-native';
2
2
  import { LocalizationObject } from './types';
3
3
  import defaultLanguage from '../Assets/language.json';
4
4
 
5
+ /**
6
+ * String table for kit and feature copy. The language is read once from the
7
+ * native `RNResource` module at construction; Vietnamese is the default.
8
+ */
5
9
  class Localize {
6
10
  private assets: LocalizationObject;
7
11
  private readonly currentLanguage: 'en' | 'vi';
@@ -25,14 +29,17 @@ class Localize {
25
29
  return this.assets;
26
30
  }
27
31
 
32
+ /** Text for `key` in the current language, or the key itself if missing. */
28
33
  translate(key: string) {
29
34
  return this.assets[this.currentLanguage]?.[key] || key;
30
35
  }
31
36
 
37
+ /** Picks the current-language text of a `{ vi, en }` pair, else its JSON. */
32
38
  translateData(data: { vi: string; en: string }) {
33
39
  return data[this.currentLanguage] ?? JSON.stringify(data);
34
40
  }
35
41
 
42
+ /** Merges extra translations; keys that already exist keep their text. */
36
43
  addTranslations(translations: LocalizationObject) {
37
44
  this.assets = {
38
45
  vi: { ...translations?.vi, ...this.assets.vi },
@@ -27,6 +27,10 @@ import { isIPhoneDuo } from './utils';
27
27
  import { useModalLayout } from './modalLayout';
28
28
  import { scheduleOnRN } from 'react-native-worklets';
29
29
 
30
+ /**
31
+ * Screen of the `Modal` route: a bottom sheet when pushed by
32
+ * `showBottomSheet`, otherwise a centered popup.
33
+ */
30
34
  const ModalScreen: React.FC<any> = props => {
31
35
  if (props.route?.params?.isBottomSheet) {
32
36
  return <BottomSheet {...props} />;
@@ -34,6 +38,10 @@ const ModalScreen: React.FC<any> = props => {
34
38
  return <Modal {...props} />;
35
39
  };
36
40
 
41
+ /**
42
+ * Centered popup that fades and scales in over a scrim. On foldables it is
43
+ * centered in the modal placement area, at most a popup wide.
44
+ */
37
45
  const Modal: React.FC<ModalParams> = props => {
38
46
  const { navigator } = useContext(ApplicationContext);
39
47
  const context = useContext<any>(MiniAppContext);
@@ -50,9 +58,7 @@ const Modal: React.FC<ModalParams> = props => {
50
58
  const isDuo = isIPhoneDuo();
51
59
  const { isFoldable, placement, popup, scrimOpacity } = useModalLayout();
52
60
 
53
- /**
54
- * Fire auto_popup_displayed tracking event on mount
55
- */
61
+ // Auto-tracks the popup as displayed on the current screen.
56
62
  useEffect(() => {
57
63
  navigator?.maxApi?.getDataObserver?.('current_screen', (curScreen: any) => {
58
64
  context.autoTracking({
@@ -11,6 +11,10 @@ export const WHITE_LIST = [
11
11
  'vn.momo.ekyc',
12
12
  ];
13
13
 
14
+ /**
15
+ * Screen-level navigation handle given to features; `setOptions` enforces the
16
+ * design system's header rules before applying React Navigation options.
17
+ */
14
18
  class Navigation {
15
19
  instance: NavigationProp<any>;
16
20
  context: any;
@@ -21,39 +25,33 @@ class Navigation {
21
25
  }
22
26
 
23
27
  /**
24
- * navigation options filter by design system rule
25
- * @param params
28
+ * Drops header overrides the design system forbids: any `headerLeft`, and
29
+ * render-function `headerRight`/`headerTitle` outside `WHITE_LIST` apps.
26
30
  */
27
31
  private filterParams = (params: NavigationOptions) => {
32
+ const isWhiteListed = WHITE_LIST.includes(this.context?.appId);
33
+ const rightType = typeof params.headerRight;
34
+ const titleType = typeof params.headerTitle;
35
+ const isTitleRenderer = titleType !== 'string' && titleType !== 'object';
36
+
28
37
  if (params.headerLeft) {
29
38
  console.warn('headerLeft not allow override render by design system!');
30
39
  delete params.headerLeft;
31
40
  }
32
41
 
33
- if (
34
- params.headerRight &&
35
- typeof params.headerRight !== 'object' &&
36
- !WHITE_LIST.includes(this.context?.appId)
37
- ) {
42
+ if (params.headerRight && rightType !== 'object' && !isWhiteListed) {
38
43
  console.warn('headerRight not allow type func render by design system!');
39
44
  delete params.headerRight;
40
45
  }
41
46
 
42
- if (
43
- params.headerTitle &&
44
- !['string', 'object'].includes(typeof params.headerTitle) &&
45
- !WHITE_LIST.includes(this.context?.appId)
46
- ) {
47
+ if (params.headerTitle && isTitleRenderer && !isWhiteListed) {
47
48
  console.warn('headerTitle not allow type func render by design system!');
48
49
  delete params.headerTitle;
49
50
  }
50
51
  return params;
51
52
  };
52
53
 
53
- /**
54
- * navigation setOptions support props NavigationOptions base on react-navigation options
55
- * @param params
56
- */
54
+ /** Applies kit `NavigationOptions` to the screen after `filterParams`. */
57
55
  setOptions = (params: NavigationOptions) => {
58
56
  const options = getOptions(this.filterParams(params));
59
57
  this.instance.setOptions(options as StackNavigationOptions);
@@ -33,6 +33,10 @@ import { ReservedRegionsProvider } from '../Layout/ReservedRegions';
33
33
 
34
34
  const Stack = createStackNavigator();
35
35
 
36
+ /**
37
+ * Root of a mini app: safe area, reserved regions and the merged mini app
38
+ * context around the kit's navigator.
39
+ */
36
40
  const NavigationContainer: React.FC<NavigationContainerProps> = ({
37
41
  screen,
38
42
  theme = defaultTheme,
@@ -92,6 +96,10 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
92
96
  );
93
97
  };
94
98
 
99
+ /**
100
+ * Stack navigator with its `Stack`, `Dialog` and `Modal` routes, under an
101
+ * `ApplicationContext` carrying the navigator, themed fonts and `translate`.
102
+ */
95
103
  const Navigation: React.FC<any> = ({
96
104
  screen,
97
105
  theme = defaultTheme,
@@ -109,9 +117,10 @@ const Navigation: React.FC<any> = ({
109
117
  const syncCanGoBack = useNativeCanGoBack();
110
118
  const isWhiteList = isWhiteListDesignSystem(context);
111
119
  const fontFamily = isWhiteList ? 'MomoTrustSans' : 'SFProText';
112
- const navigationFontFamily = isWhiteList
113
- ? 'MomoTrustSans-Regular'
114
- : 'SFProText';
120
+ let navigationFontFamily = 'SFProText';
121
+ if (isWhiteList) {
122
+ navigationFontFamily = 'MomoTrustSans-Regular';
123
+ }
115
124
  let headerBackground = context?.designSystemConfig?.config?.headerBar;
116
125
  let headerGradient = theme.colors?.gradient;
117
126
 
@@ -122,15 +131,9 @@ const Navigation: React.FC<any> = ({
122
131
  headerGradient = context?.designSystemConfig?.config?.headerGradient;
123
132
  }
124
133
 
125
- /**
126
- * inject data for navigator
127
- */
128
134
  navigator.current.maxApi = maxApi;
129
135
 
130
- /**
131
- * translate context method
132
- * @param data
133
- */
136
+ /** Text for a localization key, or the current-language side of `{ vi, en }`. */
134
137
  const translate = (data?: string | { vi: string; en: string }) => {
135
138
  if (data && typeof data !== 'string') {
136
139
  return localize?.translateData(data);
@@ -228,12 +231,12 @@ const Navigation: React.FC<any> = ({
228
231
  backgroundColor: theme.colors.background.default,
229
232
  },
230
233
  headerLeft: (props: any) => <HeaderLeft {...props} floating />,
231
- headerBackground: (props: any) => (
232
- <HeaderBackground {...props} />
233
- ),
234
- headerRight: (props: any) => (
235
- <HeaderRight {...props} floating />
236
- ),
234
+ headerBackground: (props: any) => {
235
+ return <HeaderBackground {...props} />;
236
+ },
237
+ headerRight: (props: any) => {
238
+ return <HeaderRight {...props} floating />;
239
+ },
237
240
  headerTintColor: Colors.black_17,
238
241
  ...TransitionPresets.ModalTransition,
239
242
  }}
@@ -1,6 +1,10 @@
1
1
  import {CommonActions, StackActions} from '@react-navigation/native';
2
2
  import {BottomSheetParams, ModalParams, ScreenParams} from './types';
3
3
 
4
+ /**
5
+ * Imperative navigation API exposed to features through `ApplicationContext`.
6
+ * Every action is a no-op until the container reports ready.
7
+ */
4
8
  class Navigator {
5
9
  ref?: any;
6
10
  isReady?: any;
@@ -14,40 +18,28 @@ class Navigator {
14
18
  this.isWidget = isWidget;
15
19
  }
16
20
 
17
- /**
18
- * push new stack screen
19
- * @param params
20
- */
21
+ /** Pushes a new stack screen. */
21
22
  push = (params: ScreenParams) => {
22
23
  if (this.isReady.current) {
23
24
  this.ref.current?.dispatch?.(StackActions.push('Stack', params));
24
25
  }
25
26
  };
26
27
 
27
- /**
28
- * replace current screen with new screen
29
- * @param params
30
- */
28
+ /** Replaces the current screen with a new stack screen. */
31
29
  replace = (params: ScreenParams) => {
32
30
  if (this.isReady.current) {
33
31
  this.ref.current?.dispatch?.(StackActions.replace('Stack', params));
34
32
  }
35
33
  };
36
34
 
37
- /**
38
- * pop to dismiss a screen
39
- * @param count
40
- */
35
+ /** Pops `count` screens, 1 by default. */
41
36
  pop = (count?: number) => {
42
37
  if (this.isReady.current) {
43
38
  this.ref.current?.dispatch?.(StackActions.pop(count ?? 1));
44
39
  }
45
40
  };
46
41
 
47
- /**
48
- * present a new screen, show from bottom iOS
49
- * @param params
50
- */
42
+ /** Presents a screen as a dialog (slides up from the bottom on iOS). */
51
43
  present = (params: ScreenParams) => {
52
44
  if (this.isReady.current) {
53
45
  this.ref.current?.dispatch?.(StackActions.push('Dialog', params));
@@ -55,9 +47,8 @@ class Navigator {
55
47
  };
56
48
 
57
49
  /**
58
- * show a modal popup
59
- * @param params
60
- * @param onError
50
+ * Shows a modal popup; inside a widget it always uses the native modal.
51
+ * Failures are reported through `onError` instead of throwing.
61
52
  */
62
53
  showModal = (params: ModalParams, onError?: (error: string) => void) => {
63
54
  try {
@@ -76,11 +67,7 @@ class Navigator {
76
67
  }
77
68
  };
78
69
 
79
- /**
80
- * show a bottom sheet
81
- * @param params
82
- * @param onError
83
- */
70
+ /** Shows a bottom sheet through the modal route; failures go to `onError`. */
84
71
  showBottomSheet = (
85
72
  params: BottomSheetParams,
86
73
  onError?: (error: string) => void
@@ -102,31 +89,21 @@ class Navigator {
102
89
  }
103
90
  };
104
91
 
105
- /**
106
- * pop all screen route
107
- */
92
+ /** Pops back to the first screen of the stack. */
108
93
  popToTop = () => {
109
94
  if (this.isReady.current) {
110
95
  this.ref.current?.dispatch?.(StackActions.popToTop());
111
96
  }
112
97
  };
113
98
 
114
- /**
115
- * navigate route name bottom tab only
116
- * @param name
117
- */
99
+ /** Navigates to a route by name; meant for bottom tab routes only. */
118
100
  navigate = (name: string) => {
119
101
  if (this.isReady.current) {
120
- this.ref.current?.dispatch?.(
121
- CommonActions.navigate(name)
122
- );
102
+ this.ref.current?.dispatch?.(CommonActions.navigate(name));
123
103
  }
124
104
  };
125
105
 
126
- /**
127
- * reset a navigation flow with new screen
128
- * @param params
129
- */
106
+ /** Resets the flow to a single new stack screen. */
130
107
  reset = (params: ScreenParams) => {
131
108
  if (this.isReady.current) {
132
109
  this.ref.current?.dispatch?.(
@@ -144,26 +121,21 @@ class Navigator {
144
121
  }
145
122
  };
146
123
 
147
- /**
148
- * dismiss a feature data
149
- * @param data
150
- */
124
+ /** Sets the payload the header passes to `maxApi.dismiss` on close. */
151
125
  setDismissData = (data: any) => {
152
126
  this.dismissData = data;
153
127
  };
154
128
 
155
129
  /**
156
- * set app context using for features of kit
157
- * @param context
130
+ * No-op placeholder; `NavigationContainer` and `WidgetContainer` replace it
131
+ * with their context state setter.
158
132
  */
159
- setCurrentContext = (context: {
133
+ setCurrentContext = (_context: {
160
134
  code?: string;
161
135
  name?: {vi: string; en: string};
162
136
  description?: {vi: string; en: string};
163
137
  icon?: string;
164
- }) => {
165
- console.log(context);
166
- };
138
+ }) => {};
167
139
  }
168
140
 
169
141
  export default Navigator;
@@ -1,16 +1,23 @@
1
- import React, { FC } from 'react';
2
- import { ScaleSizeContext } from '../Context';
1
+ import React, { FC, useContext, useMemo } from 'react';
2
+ import { MiniAppContext } from '../Context';
3
3
  import { ScaleSizeProviderProps } from './types';
4
4
 
5
- /** @deprecated Max font scale is fixed at 1.5; this provider no longer affects scaling. */
5
+ /**
6
+ * Caps the font scale of its subtree at `scaleSizeMaxRate` × the design size
7
+ * (`useScaleSize`), passed down in MiniAppContext. The nearest provider wins;
8
+ * an undefined rate lifts an outer cap.
9
+ */
6
10
  const ScaleSizeProvider: FC<ScaleSizeProviderProps> = ({
7
11
  scaleSizeMaxRate,
8
12
  children,
9
13
  }) => {
14
+ const context = useContext<object | undefined>(MiniAppContext);
15
+ const value = useMemo(() => {
16
+ return { ...context, scaleSizeMaxRate };
17
+ }, [context, scaleSizeMaxRate]);
18
+
10
19
  return (
11
- <ScaleSizeContext.Provider value={{ scaleSizeMaxRate }}>
12
- {children}
13
- </ScaleSizeContext.Provider>
20
+ <MiniAppContext.Provider value={value}>{children}</MiniAppContext.Provider>
14
21
  );
15
22
  };
16
23
 
@@ -8,15 +8,17 @@ type ScreenTrackingDeps = {
8
8
  screenName: string;
9
9
  };
10
10
 
11
+ /**
12
+ * Auto-tracking reporters of a stack screen. Each event fires at most once per
13
+ * screen, guarded by the `release*` flags on `tracking.current`.
14
+ */
11
15
  export const createScreenTracking = ({
12
16
  tracking,
13
17
  context,
14
18
  navigator,
15
19
  screenName,
16
20
  }: ScreenTrackingDeps) => {
17
- /**
18
- * tracking for screen load
19
- */
21
+ /** Reports the screen load time. */
20
22
  const onScreenLoad = () => {
21
23
  if (!tracking.current?.releaseLoad) {
22
24
  let timeLoad = tracking.current.timeLoad;
@@ -42,29 +44,22 @@ export const createScreenTracking = ({
42
44
  );
43
45
  tracking.current.releaseLoad = true;
44
46
 
45
- /**
46
- * debug
47
- */
48
47
  navigator?.maxApi?.showToastDebug?.({
49
48
  appId: context.appId,
50
49
  message: `${screenName} screen_load_time ${timeLoad}`,
51
50
  type: 'ERROR',
52
51
  });
53
- if (
54
- __DEV__ &&
55
- tracking.current.lastElement?.children?.current?.length > 0
56
- ) {
52
+ const lastChildren = tracking.current.lastElement?.children?.current;
53
+ if (__DEV__ && lastChildren?.length > 0) {
57
54
  Alert.alert(
58
55
  `${screenName}- load ${timeLoad}ms`,
59
- JSON.stringify(tracking.current.lastElement?.children?.current),
56
+ JSON.stringify(lastChildren),
60
57
  );
61
58
  }
62
59
  }
63
60
  };
64
61
 
65
- /**
66
- * tracking for screen load
67
- */
62
+ /** Reports the time until the screen became interactive. */
68
63
  const onScreenInteraction = () => {
69
64
  if (!tracking.current?.releaseInteraction) {
70
65
  let timeLoad = tracking.current.timeLoad;
@@ -92,9 +87,6 @@ export const createScreenTracking = ({
92
87
  );
93
88
  tracking.current.releaseInteraction = true;
94
89
 
95
- /**
96
- * debug toast
97
- */
98
90
  navigator?.maxApi?.showToastDebug?.({
99
91
  appId: context.appId,
100
92
  message: `${screenName} screen_interaction_time ${tracking.current.timeInteraction}`,
@@ -103,9 +95,7 @@ export const createScreenTracking = ({
103
95
  }
104
96
  };
105
97
 
106
- /**
107
- * tracking for first interaction by user
108
- */
98
+ /** Reports the user's first interaction with the screen. */
109
99
  const onFirstInteraction = (action: string) => {
110
100
  if (!tracking.current?.releaseUserInteraction) {
111
101
  const timeLoad = tracking.current.endTime - tracking.current.startTime;
@@ -119,9 +109,6 @@ export const createScreenTracking = ({
119
109
  });
120
110
  tracking.current.releaseUserInteraction = true;
121
111
 
122
- /**
123
- * debug
124
- */
125
112
  navigator?.maxApi?.showToastDebug?.({
126
113
  appId: context.appId,
127
114
  message: `${screenName} user_interaction ${timeLoad}`,
@@ -130,9 +117,7 @@ export const createScreenTracking = ({
130
117
  }
131
118
  };
132
119
 
133
- /**
134
- * tracking for loading screen
135
- */
120
+ /** Reports how long the skeleton loading state lasted. */
136
121
  const onScreenLoading = () => {
137
122
  const start = tracking.current.timeStartLoading;
138
123
  const end = tracking.current.timeEndLoading;
@@ -12,9 +12,8 @@ import { TooltipPortalHost, TooltipPortalProvider } from './TooltipPortal';
12
12
  const runAfterInteractions = InteractionManager.runAfterInteractions;
13
13
 
14
14
  /**
15
- * container for stack screen
16
- * @param props
17
- * @constructor
15
+ * Hosts a feature screen in the stack: applies its header options, provides
16
+ * `ScreenContext` and reports navigation, load and interaction tracking.
18
17
  */
19
18
  const StackScreen: React.FC<any> = props => {
20
19
  const { navigator } = useContext(ApplicationContext);
@@ -64,18 +63,12 @@ const StackScreen: React.FC<any> = props => {
64
63
 
65
64
  const screenName = Component?.name || Component?.type?.name || 'Invalid';
66
65
 
67
- /**
68
- * set options for screen
69
- */
70
66
  useLayoutEffect(() => {
71
67
  if (options) {
72
68
  navigation.setOptions(options);
73
69
  }
74
70
  }, [navigation, options]);
75
71
 
76
- /**
77
- * tracking for screen
78
- */
79
72
  useEffect(() => {
80
73
  let focusScreen: any;
81
74
  let onFocusApp: any;
@@ -149,6 +142,7 @@ const StackScreen: React.FC<any> = props => {
149
142
  // eslint-disable-next-line react-hooks/exhaustive-deps
150
143
  }, []);
151
144
 
145
+ /** Reports a navigation to this screen: `back` if it was shown before. */
152
146
  const onScreenNavigated = (pre: string, current: string) => {
153
147
  if (!isBackgroundToForeground) {
154
148
  const item: any = {
@@ -166,9 +160,6 @@ const StackScreen: React.FC<any> = props => {
166
160
 
167
161
  tracking.current.mounted = true;
168
162
 
169
- /**
170
- * debug toast
171
- */
172
163
  navigator?.maxApi?.showToastDebug?.({
173
164
  appId: context.appId,
174
165
  message: `${screenName} screen_navigated`,
@@ -190,9 +181,7 @@ const StackScreen: React.FC<any> = props => {
190
181
  screenName,
191
182
  bottomTab: !!bottomTab,
192
183
  onElementLoad: (item: any) => {
193
- /**
194
- * widget handle
195
- */
184
+ // Widgets record their own load span and don't end the screen load.
196
185
  if (item?.componentName === 'Widget') {
197
186
  const index = widgets.current.findIndex(
198
187
  (element: any) => element.componentId === item.componentId,
@@ -216,35 +205,25 @@ const StackScreen: React.FC<any> = props => {
216
205
  return;
217
206
  }
218
207
 
219
- /**
220
- * tracking for element screen load
221
- */
222
208
  clearTimeout(tracking.current.timeoutLoad);
223
209
  tracking.current.endTime = Date.now();
224
210
  tracking.current.timeoutInteraction?.cancel?.();
225
211
  tracking.current.timeoutInteraction = runAfterInteractions(() => {
226
- tracking.current.timeInteraction =
227
- Date.now() - tracking.current.startTime;
212
+ const elapsed = Date.now() - tracking.current.startTime;
213
+ tracking.current.timeInteraction = elapsed;
228
214
  });
229
215
 
230
- /**
231
- * support for debug last element
232
- */
233
216
  if (item?.componentName) {
234
217
  tracking.current.lastElement = item;
235
218
  }
236
219
 
237
- /**
238
- * for stop tracking when user interaction
239
- */
220
+ // A user interaction settles load and interaction tracking at once.
240
221
  if (item?.interaction) {
241
222
  onScreenLoad();
242
223
  onScreenInteraction();
243
224
  onFirstInteraction(item?.action);
244
225
  }
245
- /**
246
- * timeout for handle tracking screen
247
- */
226
+ // Reports once no element has loaded for 2s.
248
227
  tracking.current.timeoutLoad = setTimeout(() => {
249
228
  const time = tracking.current.endTime - tracking.current.startTime;
250
229
  tracking.current.widgets = widgets.current;
@@ -262,16 +241,10 @@ const StackScreen: React.FC<any> = props => {
262
241
  tracking.current.timeStartLoading = Date.now();
263
242
  }
264
243
 
265
- /**
266
- * tracking for loading screen
267
- */
268
244
  if (!loading) {
269
245
  tracking.current.timeEndLoading = Date.now();
270
246
  }
271
247
 
272
- /**
273
- * timeout for handle tracking screen
274
- */
275
248
  clearTimeout(tracking.current.timeoutLoading);
276
249
  tracking.current.timeoutLoading = setTimeout(() => {
277
250
  onScreenLoading();
@@ -21,10 +21,14 @@ const TooltipPortalContext = createContext<TooltipPortalContextValue | null>(
21
21
  null,
22
22
  );
23
23
 
24
+ /**
25
+ * Registry of tooltip contents that `TooltipPortalHost` renders in one layer
26
+ * above the screen, so tooltips aren't clipped by their anchor's parents.
27
+ */
24
28
  export const TooltipPortalProvider: React.FC<{ children: ReactNode }> = ({
25
29
  children,
26
30
  }) => {
27
- // Use ref for synchronous updates + reducer for manual re-renders
31
+ // The ref map updates synchronously; the reducer only triggers re-renders.
28
32
  const portalsRef = useRef<Map<string, ReactNode>>(new Map());
29
33
  const hostRef = useRef<View>(null);
30
34
  const [updateCounter, forceUpdate] = useReducer((x: number) => x + 1, 0);
@@ -32,16 +36,15 @@ export const TooltipPortalProvider: React.FC<{ children: ReactNode }> = ({
32
36
 
33
37
  const register = useCallback((id: string, content: ReactNode) => {
34
38
  portalsRef.current.set(id, content);
35
- forceUpdate(); // Trigger re-render to show new content
39
+ forceUpdate();
36
40
  }, []);
37
41
 
38
42
  const unregister = useCallback((id: string, immediate = false) => {
39
43
  const hasItem = portalsRef.current.has(id);
40
- if (!hasItem) return; // Already removed
44
+ if (!hasItem) return;
41
45
 
42
46
  portalsRef.current.delete(id);
43
47
 
44
- // Clear any pending updates
45
48
  if (pendingUpdateRef.current) {
46
49
  clearTimeout(pendingUpdateRef.current);
47
50
  pendingUpdateRef.current = null;
@@ -72,6 +75,7 @@ export const TooltipPortalProvider: React.FC<{ children: ReactNode }> = ({
72
75
  );
73
76
  };
74
77
 
78
+ /** Full-screen, touch-through layer that renders every registered tooltip. */
75
79
  export const TooltipPortalHost: React.FC = () => {
76
80
  const context = useContext(TooltipPortalContext);
77
81
 
@@ -95,6 +99,10 @@ export const TooltipPortalHost: React.FC = () => {
95
99
  );
96
100
  };
97
101
 
102
+ /**
103
+ * Tooltip portal API. Outside a provider it returns no-ops (and warns in dev)
104
+ * so a tooltip degrades instead of crashing.
105
+ */
98
106
  export const useTooltipPortal = () => {
99
107
  const context = useContext(TooltipPortalContext);
100
108
 
@@ -104,7 +112,6 @@ export const useTooltipPortal = () => {
104
112
  'useTooltipPortal must be used within TooltipPortalProvider. Tooltips may not appear correctly.',
105
113
  );
106
114
  }
107
- // Return no-op functions if context is missing
108
115
  return {
109
116
  register: () => { },
110
117
  unregister: () => { },
@@ -18,6 +18,7 @@ import { StyleSheet, View } from 'react-native';
18
18
 
19
19
  const Stack = createStackNavigator();
20
20
 
21
+ /** Widget screen that reports its laid-out height to size the container. */
21
22
  const WidgetContent: React.FC = ({ route }: any) => {
22
23
  const Component = route?.params?.screen;
23
24
  return (
@@ -31,6 +32,10 @@ const WidgetContent: React.FC = ({ route }: any) => {
31
32
  );
32
33
  };
33
34
 
35
+ /**
36
+ * Root of an embedded widget: its own navigator with the widget screen and the
37
+ * `Modal` route, sized to the widget's content height.
38
+ */
34
39
  const WidgetContainer: React.FC<WidgetContainerProps> = ({
35
40
  widget,
36
41
  theme = defaultTheme,
@@ -61,11 +66,9 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
61
66
  headerGradient = context?.designConfig?.headerGradient;
62
67
  }
63
68
 
64
- /**
65
- * inject data for navigator
66
- */
67
69
  navigator.current.maxApi = maxApi;
68
70
 
71
+ /** Text for a localization key, or the current-language side of `{ vi, en }`. */
69
72
  const translate = (data?: string | { vi: string; en: string }) => {
70
73
  if (data && typeof data !== 'string') {
71
74
  return localize?.translateData(data);
@@ -81,7 +84,10 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
81
84
  };
82
85
  const isWhiteList = isWhiteListDesignSystem(mergedContext);
83
86
  const fontFamily = isWhiteList ? 'MomoTrustSans' : 'SFProText';
84
- const navigationFontFamily = isWhiteList ? 'MomoTrustSans-Regular' : 'SFProText';
87
+ let navigationFontFamily = 'SFProText';
88
+ if (isWhiteList) {
89
+ navigationFontFamily = 'MomoTrustSans-Regular';
90
+ }
85
91
 
86
92
  return (
87
93
  <View style={[styles.container, { height: height ?? '100%' }]}>