@momo-kits/foundation 0.89.6 → 0.92.1-beta.16

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.
@@ -1,29 +1,54 @@
1
- import React, {useContext, useEffect, useLayoutEffect} from 'react';
1
+ import React, {useContext, useEffect, useLayoutEffect, useRef} from 'react';
2
2
  import {ScreenParams} from './types';
3
3
  import Navigation from './Navigation';
4
- import {ApplicationContext, MiniAppContext} from './index';
4
+ import {ApplicationContext} from './index';
5
5
  import {GridSystem} from '../Layout';
6
6
  import {useHeaderHeight} from '@react-navigation/stack';
7
+ import {Alert, InteractionManager} from 'react-native';
8
+
9
+ const {
10
+ ScreenContext,
11
+ MiniAppContext,
12
+ } = require('react-native/Libraries/Components/ScreenContext');
7
13
 
8
14
  const StackScreen: React.FC<ScreenParams> = props => {
9
15
  const {theme, showGrid, navigator} = useContext(ApplicationContext);
10
- const context = useContext(MiniAppContext);
16
+ const startTime = useRef(Date.now());
17
+ const endTime = useRef(Date.now());
18
+ const timeLoad = useRef(0);
19
+ const timeout = useRef<any>();
20
+
21
+ const context = useContext<any>(MiniAppContext);
11
22
 
12
- const {screen: Component, options}: ScreenParams = props.route.params;
23
+ const {screen: Component, options, initialParams} = props.route.params;
13
24
  const navigation = new Navigation(props.navigation, theme);
14
25
  const heightHeader = useHeaderHeight();
15
26
 
16
- const params = {
27
+ const data = {
17
28
  ...props.route.params,
29
+ ...initialParams,
18
30
  navigation,
19
31
  };
20
32
 
21
- delete params.screen;
33
+ delete data.screen;
34
+ delete data.initialParams;
35
+
36
+ const screenName = Component?.name || Component?.type?.name || 'Invalid';
22
37
 
23
38
  /**
24
39
  * set options for screen
25
40
  */
26
41
  useLayoutEffect(() => {
42
+ const host = (global as any).miniAppApi?.currentHost || {};
43
+ host.screenName = screenName;
44
+ InteractionManager.runAfterInteractions(() => {
45
+ navigator?.maxApi?.showToastDebug?.({
46
+ appId: `auto - ${context.appId}`,
47
+ message: `${screenName} screen_interaction_time ${
48
+ Date.now() - startTime.current
49
+ }`,
50
+ });
51
+ });
27
52
  if (options) {
28
53
  navigation.setOptions(options);
29
54
  }
@@ -33,9 +58,18 @@ const StackScreen: React.FC<ScreenParams> = props => {
33
58
  * tracking for screen
34
59
  */
35
60
  useEffect(() => {
61
+ if (__DEV__) {
62
+ if (!screenName || screenName === 'screen') {
63
+ Alert.alert(
64
+ 'Design system',
65
+ `Unvalidated screen name "${screenName}" !`,
66
+ );
67
+ }
68
+ }
69
+
36
70
  const tracking = {
37
71
  ...context,
38
- screen_name: Component.name,
72
+ screen_name: screenName,
39
73
  };
40
74
 
41
75
  const routes = props.navigation.getState()?.routes || [];
@@ -52,8 +86,8 @@ const StackScreen: React.FC<ScreenParams> = props => {
52
86
  };
53
87
  navigator?.maxApi?.trackEvent?.('service_frame_displayed', params);
54
88
  navigator?.maxApi?.showToastDebug?.({
55
- appId: context.appId,
56
- message: `${Component.name} service_frame_displayed`,
89
+ appId: `auto - ${context.appId}`,
90
+ message: `${screenName} service_frame_displayed`,
57
91
  params,
58
92
  });
59
93
 
@@ -65,18 +99,43 @@ const StackScreen: React.FC<ScreenParams> = props => {
65
99
  };
66
100
  navigator?.maxApi?.trackEvent?.('service_icon_clicked', params);
67
101
  navigator?.maxApi?.showToastDebug?.({
68
- appId: context.appId,
69
- message: `${Component.name} service_icon_clicked`,
102
+ appId: `auto - ${context.appId}`,
103
+ message: `${screenName} service_icon_clicked`,
70
104
  params,
71
105
  });
72
106
  });
73
107
  }, []);
74
108
 
75
109
  return (
76
- <>
77
- <Component heightHeader={heightHeader} {...params} />
110
+ <ScreenContext.Provider
111
+ value={{
112
+ appId: context.appId,
113
+ code: context.code,
114
+ screenName,
115
+ autoTracking: (data: any) => {
116
+ navigator?.maxApi?.dispatchFunction?.('autoTracking', data);
117
+ },
118
+ onElementLoad: () => {
119
+ endTime.current = Date.now();
120
+ clearTimeout(timeout.current);
121
+ timeout.current = setTimeout(() => {
122
+ if (timeLoad.current === 0) {
123
+ timeLoad.current = endTime.current - startTime.current;
124
+ navigator?.maxApi?.showToastDebug?.({
125
+ appId: `auto - ${context.appId}`,
126
+ message: `${screenName} screen_load_time ${timeLoad.current}`,
127
+ });
128
+ }
129
+ }, 2000);
130
+ },
131
+ }}>
132
+ <Component
133
+ heightHeader={heightHeader}
134
+ {...data}
135
+ onScreenLoad={() => {}}
136
+ />
78
137
  {showGrid && <GridSystem />}
79
- </>
138
+ </ScreenContext.Provider>
80
139
  );
81
140
  };
82
141
 
@@ -1,9 +1,9 @@
1
- import {Animated, FlatListProps, ViewProps} from 'react-native';
2
1
  import {StackNavigationOptions} from '@react-navigation/stack';
3
2
  import React from 'react';
4
- import Navigator from './Navigator';
3
+ import {Animated, FlatListProps, ViewProps} from 'react-native';
5
4
  import {PopupNotifyProps} from '../Popup/types';
6
5
  import Navigation from './Navigation';
6
+ import Navigator from './Navigator';
7
7
 
8
8
  type Screen = React.ComponentType<NavigationScreenProps>;
9
9
 
@@ -95,6 +95,7 @@ export type NavigationContainerProps = {
95
95
  theme: Theme;
96
96
  maxApi: any;
97
97
  localization?: LocalizationObject;
98
+ initialParams?: any;
98
99
  };
99
100
 
100
101
  export type NavigationScreenProps = {
@@ -129,6 +130,7 @@ export type NavigationButtonProps = {
129
130
  onPress: () => void;
130
131
  badgeType?: 'dot' | 'number';
131
132
  badgeValue?: number;
133
+ accessibilityID?: string;
132
134
  };
133
135
 
134
136
  export type PinnedToolType = {
@@ -208,6 +210,14 @@ export type BottomTabProps = {
208
210
  tabPress?: any;
209
211
  focus?: any;
210
212
  };
213
+ floatingButton?: FloatingButtonProps;
214
+ };
215
+
216
+ export type FloatingButtonProps = {
217
+ container?: React.ReactNode;
218
+ icon: string | React.ReactNode;
219
+ label: string;
220
+ onPress: () => void;
211
221
  };
212
222
 
213
223
  export type AnimatedHeader = {
@@ -12,7 +12,8 @@ import {
12
12
  } from './Components';
13
13
  import {NavigationOptions, Theme} from './types';
14
14
  import {Colors} from '../Consts';
15
-
15
+ import {Platform} from 'react-native';
16
+ const isIOS = Platform.OS === 'ios';
16
17
  /**
17
18
  * default options for stack screen
18
19
  */
@@ -141,4 +142,71 @@ const getOptions = (params: NavigationOptions, theme: Theme) => {
141
142
  };
142
143
  };
143
144
 
144
- export {getStackOptions, getDialogOptions, getModalOptions, getOptions};
145
+ const getAccessibilityID = (
146
+ key = '',
147
+ override: string | undefined = undefined,
148
+ ) => {
149
+ if (isIOS) {
150
+ return {
151
+ accessible: true,
152
+ testID: override || key,
153
+ };
154
+ } else {
155
+ return {
156
+ accessibilityLabel: override || key,
157
+ };
158
+ }
159
+ };
160
+
161
+ const cleanNameForTracking = (word = ''): string => {
162
+ return word
163
+ .normalize('NFD')
164
+ .replace(/[\u0300-\u036f |\s]/g, '')
165
+ .toLowerCase();
166
+ };
167
+
168
+ const formatComponentName = (word = '') => {
169
+ const accentsMap = {
170
+ a: 'á|à|ả|ã|ạ|â|ấ|ầ|ẩ|ẫ|ậ|ă|ắ|ằ|ẳ|ẵ|ặ',
171
+ A: 'Á|À|Ả|Ã|Ạ|Â|Ấ|Ầ|Ẩ|Ẫ|Ậ|Ă|Ắ|Ằ|Ẳ|Ẵ|Ặ',
172
+ e: 'é|è|ẻ|ẽ|ẹ|ê|ế|ề|ể|ễ|ệ',
173
+ E: 'É|È|Ẻ|Ẽ|Ẹ|Ê|Ế|Ề|Ể|Ễ|Ệ',
174
+ i: 'í|ì|ỉ|ĩ|ị',
175
+ I: 'Í|Ì|Ỉ|Ĩ|Ị',
176
+ o: 'ó|ò|ỏ|õ|ọ|ô|ố|ồ|ổ|ỗ|ộ|ơ|ớ|ờ|ở|ỡ|ợ',
177
+ O: 'Ó|Ò|Ỏ|Õ|Ọ|Ô|Ố|Ồ|Ổ|Ỗ|Ộ|Ơ|Ớ|Ờ|Ở|Ỡ|Ợ',
178
+ u: 'ú|ù|ủ|ũ|ụ|ư|ứ|ừ|ử|ữ|ự',
179
+ U: 'Ú|Ù|Ủ|Ũ|Ụ|Ư|Ứ|Ừ|Ử|Ữ|Ự',
180
+ y: 'ý|ỳ|ỷ|ỹ|ỵ',
181
+ Y: 'Ý|Ỳ|Ỷ|Ỹ|Ỵ',
182
+ d: 'đ',
183
+ D: 'Đ',
184
+ };
185
+
186
+ for (const [nonAccent, accentGroup] of Object.entries(accentsMap)) {
187
+ const accents = new RegExp(accentGroup, 'g');
188
+ word = word.replace(accents, nonAccent);
189
+ }
190
+
191
+ word = word.replace(/\s+/g, '_').toLowerCase();
192
+
193
+ return word;
194
+ };
195
+
196
+ const formatNameLength = (name = '') => {
197
+ let formattedName = name;
198
+ if (formattedName.length > 100) {
199
+ formattedName = formattedName.substring(0, 100);
200
+ }
201
+ return formattedName;
202
+ };
203
+ export {
204
+ getStackOptions,
205
+ getDialogOptions,
206
+ getModalOptions,
207
+ getOptions,
208
+ formatComponentName,
209
+ formatNameLength,
210
+ getAccessibilityID,
211
+ cleanNameForTracking,
212
+ };
package/Badge/Badge.tsx CHANGED
@@ -46,7 +46,7 @@ const Badge: FC<BadgeProps> = ({label = 'Label', style, backgroundColor}) => {
46
46
 
47
47
  return (
48
48
  <View style={[style, styles.badge, {backgroundColor: badgeColor}]}>
49
- <Text color={Colors.black_01} typography={'action_xs_bold'}>
49
+ <Text color={Colors.black_01} typography={'action_xxs_bold'}>
50
50
  {formatTitle()}
51
51
  </Text>
52
52
  </View>
package/Button/index.tsx CHANGED
@@ -6,7 +6,6 @@ import {
6
6
  TouchableOpacityProps,
7
7
  View,
8
8
  } from 'react-native';
9
-
10
9
  import LinearGradient from 'react-native-linear-gradient';
11
10
  import {ApplicationContext} from '../Application';
12
11
  import {Text} from '../Text';
@@ -15,6 +14,16 @@ import {Colors, Spacing} from '../Consts';
15
14
  import styles from './styles';
16
15
  import {Icon} from '../Icon';
17
16
  import {Loader} from '../Loader';
17
+ import {
18
+ cleanNameForTracking,
19
+ formatComponentName,
20
+ getAccessibilityID,
21
+ } from '../Application/utils';
22
+
23
+ const {
24
+ ScreenContext,
25
+ MiniAppContext,
26
+ } = require('react-native/Libraries/Components/ScreenContext');
18
27
 
19
28
  const AnimationLinear = Animated.createAnimatedComponent(LinearGradient);
20
29
 
@@ -70,6 +79,10 @@ export interface ButtonProps extends TouchableOpacityProps {
70
79
  * A callback function that is called when the button is pressed.
71
80
  */
72
81
  onPress: () => void;
82
+
83
+ params?: any;
84
+
85
+ componentId?: string;
73
86
  }
74
87
 
75
88
  const Button: FC<ButtonProps> = ({
@@ -82,9 +95,13 @@ const Button: FC<ButtonProps> = ({
82
95
  loading = false,
83
96
  title = 'Button',
84
97
  onPress,
98
+ params,
99
+ componentId,
100
+ accessibilityLabel,
85
101
  ...rest
86
102
  }) => {
87
103
  const {theme, config} = useContext(ApplicationContext);
104
+ const {appId, code} = useContext<any>(MiniAppContext);
88
105
  const {gradient, color} = config?.navigationBar?.buttonColors ?? {};
89
106
  let gradientPros;
90
107
  if (gradient?.length > 0 && type == 'primary') {
@@ -94,6 +111,8 @@ const Button: FC<ButtonProps> = ({
94
111
  end: {x: 0, y: 0.5},
95
112
  };
96
113
  }
114
+
115
+ const screenInfo = useContext<any>(ScreenContext);
97
116
  const getSizeStyle = () => {
98
117
  const styleSheet: {[key: string]: any} = styles;
99
118
  return styleSheet[size ?? 'small'];
@@ -291,19 +310,40 @@ const Button: FC<ButtonProps> = ({
291
310
  const activeOpacity = type === 'disabled' ? 0.75 : 0.5;
292
311
 
293
312
  return (
294
- <TouchableOpacity
295
- {...rest}
296
- activeOpacity={activeOpacity}
297
- onPress={onPressButton}
298
- style={buttonStyle}>
299
- {renderLeading()}
300
- {renderTitle()}
301
- {renderTrailing()}
302
- {gradientPros && (
303
- <AnimationLinear {...gradientPros} style={styles.gradientView} />
304
- )}
305
- {gradientPros && <View style={styles.strokeView} />}
306
- </TouchableOpacity>
313
+ <ScreenContext.Provider
314
+ value={{
315
+ ...screenInfo,
316
+ componentName: 'Button',
317
+ params,
318
+ state: loading
319
+ ? 'loading'
320
+ : type === 'disabled'
321
+ ? 'disabled'
322
+ : 'enabled',
323
+ componentId,
324
+ componentValue: formatComponentName(title),
325
+ action: 'click',
326
+ }}>
327
+ <TouchableOpacity
328
+ {...rest}
329
+ {...getAccessibilityID(
330
+ `${appId}/${code}/${screenInfo.screenName}/btn_${cleanNameForTracking(
331
+ title,
332
+ )}`,
333
+ accessibilityLabel,
334
+ )}
335
+ activeOpacity={activeOpacity}
336
+ onPress={onPressButton}
337
+ style={buttonStyle}>
338
+ {renderLeading()}
339
+ {renderTitle()}
340
+ {renderTrailing()}
341
+ {gradientPros && (
342
+ <AnimationLinear {...gradientPros} style={styles.gradientView} />
343
+ )}
344
+ {gradientPros && <View style={styles.strokeView} />}
345
+ </TouchableOpacity>
346
+ </ScreenContext.Provider>
307
347
  );
308
348
  };
309
349
 
@@ -5,6 +5,16 @@ import styles from './styles';
5
5
  import {ApplicationContext} from '../Application';
6
6
  import {Text} from '../Text';
7
7
  import {Icon} from '../Icon';
8
+ import {
9
+ cleanNameForTracking,
10
+ formatNameLength,
11
+ getAccessibilityID,
12
+ } from '../Application/utils';
13
+
14
+ const {
15
+ ScreenContext,
16
+ MiniAppContext,
17
+ } = require('react-native/Libraries/Components/ScreenContext');
8
18
 
9
19
  const CheckBox: FC<CheckBoxProps> = ({
10
20
  value,
@@ -13,8 +23,12 @@ const CheckBox: FC<CheckBoxProps> = ({
13
23
  style,
14
24
  label,
15
25
  indeterminate,
26
+ params,
27
+ componentId,
16
28
  }) => {
29
+ const {appId, code} = useContext<any>(MiniAppContext);
17
30
  const {theme} = useContext(ApplicationContext);
31
+ const screenInfo = useContext<any>(ScreenContext);
18
32
  const haveValue = value || indeterminate;
19
33
  const iconSource = indeterminate ? 'ic_minus' : 'ic_checked';
20
34
  let borderColor = theme.colors.text.default;
@@ -34,27 +48,46 @@ const CheckBox: FC<CheckBoxProps> = ({
34
48
  }
35
49
  }
36
50
 
51
+ const accessibilityID = getAccessibilityID(
52
+ `${appId}/${code}/${screenInfo.screenName}/cbx_${cleanNameForTracking(
53
+ label,
54
+ )}_${indeterminate ? 'na' : value}`,
55
+ );
56
+
37
57
  return (
38
- <TouchableOpacity
39
- activeOpacity={0.8}
40
- onPress={() => onChange?.(!value)}
41
- disabled={disabled}
42
- style={[style, styles.container]}>
43
- <View
44
- style={[
45
- {borderColor, backgroundColor, borderWidth: 1},
46
- styles.checkbox,
47
- ]}>
48
- {haveValue && (
49
- <Icon
50
- color={theme.colors.background.surface}
51
- size={20}
52
- source={iconSource}
53
- />
54
- )}
55
- </View>
56
- {!!label && <Text typography={'body_default_regular'}>{label}</Text>}
57
- </TouchableOpacity>
58
+ <ScreenContext.Provider
59
+ value={{
60
+ ...screenInfo,
61
+ componentName: 'Button',
62
+ params,
63
+ state: disabled ? 'disabled' : 'filled',
64
+ componentId,
65
+ action: 'click',
66
+ componentLabel: formatNameLength(label || ''),
67
+ componentValue: indeterminate ? 'indetermine' : value,
68
+ }}>
69
+ <TouchableOpacity
70
+ {...accessibilityID}
71
+ activeOpacity={0.8}
72
+ onPress={() => onChange?.(!value)}
73
+ disabled={disabled}
74
+ style={[style, styles.container]}>
75
+ <View
76
+ style={[
77
+ {borderColor, backgroundColor, borderWidth: 1},
78
+ styles.checkbox,
79
+ ]}>
80
+ {haveValue && (
81
+ <Icon
82
+ color={theme.colors.background.surface}
83
+ size={20}
84
+ source={iconSource}
85
+ />
86
+ )}
87
+ </View>
88
+ {!!label && <Text typography={'body_default_regular'}>{label}</Text>}
89
+ </TouchableOpacity>
90
+ </ScreenContext.Provider>
58
91
  );
59
92
  };
60
93
 
package/CheckBox/types.ts CHANGED
@@ -35,4 +35,8 @@ export type CheckBoxProps = {
35
35
  * Accepts an object of style properties.
36
36
  */
37
37
  style?: ViewStyle;
38
+
39
+ params?: any
40
+
41
+ componentId?: string
38
42
  };
@@ -213,6 +213,20 @@ const Shadow = {
213
213
  elevation: 6,
214
214
  },
215
215
  }),
216
+
217
+ Pink: Platform.select({
218
+ ios: {
219
+ shadowColor: '#e27eb5',
220
+ shadowOffset: {width: 0, height: 6},
221
+ shadowOpacity: 0.25,
222
+ shadowRadius: 8,
223
+ },
224
+ android: {
225
+ elevation: 16,
226
+ shadowColor: '#e27eb5',
227
+ shadowOpacity: 0.25,
228
+ },
229
+ }),
216
230
  };
217
231
 
218
232
  export {Colors, Spacing, Radius, Shadow};
@@ -9,6 +9,11 @@ import {ApplicationContext} from '../Application';
9
9
  import {Colors} from '../Consts';
10
10
  import {Icon} from '../Icon';
11
11
  import styles from './styles';
12
+ import {formatComponentName} from '../Application/utils';
13
+
14
+ const {
15
+ ScreenContext,
16
+ } = require('react-native/Libraries/Components/ScreenContext');
12
17
 
13
18
  export interface IconButtonProps extends TouchableOpacityProps {
14
19
  /**
@@ -26,6 +31,10 @@ export interface IconButtonProps extends TouchableOpacityProps {
26
31
  * Optional. Defines the size of the IconButton.
27
32
  */
28
33
  size?: 'large' | 'small';
34
+
35
+ params?: any;
36
+
37
+ componentId?: string;
29
38
  }
30
39
 
31
40
  const IconButton: React.FC<IconButtonProps> = ({
@@ -33,9 +42,12 @@ const IconButton: React.FC<IconButtonProps> = ({
33
42
  icon,
34
43
  size,
35
44
  onPress,
45
+ params,
46
+ componentId,
36
47
  ...rest
37
48
  }) => {
38
49
  const {theme} = useContext(ApplicationContext);
50
+ const screenInfo = useContext<any>(ScreenContext);
39
51
 
40
52
  /**
41
53
  * get size icon button
@@ -121,13 +133,24 @@ const IconButton: React.FC<IconButtonProps> = ({
121
133
  const iconSize = size === 'small' ? 16 : 24;
122
134
 
123
135
  return (
124
- <TouchableOpacity
125
- {...rest}
126
- activeOpacity={activeOpacity}
127
- onPress={onPressButton}
128
- style={buttonStyle}>
129
- <Icon size={iconSize} source={icon} color={getIconColor()} />
130
- </TouchableOpacity>
136
+ <ScreenContext.Provider
137
+ value={{
138
+ ...screenInfo,
139
+ componentName: 'IconButton',
140
+ params,
141
+ state: type === 'disabled' ? 'disabled' : 'enabled',
142
+ componentId,
143
+ componentValue: formatComponentName(icon),
144
+ action: 'click',
145
+ }}>
146
+ <TouchableOpacity
147
+ {...rest}
148
+ activeOpacity={activeOpacity}
149
+ onPress={onPressButton}
150
+ style={buttonStyle}>
151
+ <Icon size={iconSize} source={icon} color={getIconColor()} />
152
+ </TouchableOpacity>
153
+ </ScreenContext.Provider>
131
154
  );
132
155
  };
133
156
 
package/Image/index.tsx CHANGED
@@ -1,12 +1,13 @@
1
1
  import React, {useContext, useRef, useState} from 'react';
2
2
  import {StyleSheet, View} from 'react-native';
3
- import FastImage from 'react-native-fast-image';
3
+ import FastImage, {Source} from 'react-native-fast-image';
4
4
  import styles from './styles';
5
5
  import {ApplicationContext} from '../Application';
6
6
  import {Skeleton} from '../Skeleton';
7
7
  import {Icon} from '../Icon';
8
8
  import {Styles} from '../Consts';
9
9
  import {ImageProps} from './types';
10
+ import {getAccessibilityID} from '../Application/utils';
10
11
 
11
12
  type Status = 'success' | 'loading' | 'error';
12
13
 
@@ -44,6 +45,7 @@ const Image: React.FC<ImageProps> = ({
44
45
  return (
45
46
  <FastImage
46
47
  {...rest}
48
+ {...getAccessibilityID(`img|${(source as Source).uri}`)}
47
49
  source={source}
48
50
  style={[styles.container, style]}
49
51
  onProgress={() => {