@momo-kits/foundation 0.102.3-beta.1 → 0.102.3-optimize.1
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.
- package/Application/Components.tsx +12 -1
- package/Application/Localize.ts +37 -0
- package/Application/NavigationContainer.tsx +9 -20
- package/Application/StackScreen.tsx +175 -130
- package/Application/index.ts +3 -1
- package/Application/types.ts +5 -4
- package/Badge/BadgeDotAnimation.tsx +95 -0
- package/Badge/index.tsx +2 -1
- package/Badge/styles.ts +50 -6
- package/Input/styles.ts +1 -2
- package/Layout/Screen.tsx +3 -4
- package/package.json +1 -1
- package/publish.sh +2 -2
|
@@ -35,6 +35,7 @@ import {Badge, BadgeDot} from '../Badge';
|
|
|
35
35
|
import {HeaderType} from '../Layout/types';
|
|
36
36
|
import Navigation from './Navigation';
|
|
37
37
|
import {InputRef, InputSearch, InputSearchProps} from '../Input';
|
|
38
|
+
import {BadgeDotAnimation} from '../Badge';
|
|
38
39
|
|
|
39
40
|
const SCREEN_PADDING = 12;
|
|
40
41
|
const BACK_WIDTH = 28;
|
|
@@ -71,6 +72,11 @@ const NavigationButton: React.FC<NavigationButtonProps> = ({
|
|
|
71
72
|
if (badgeType === 'dot') {
|
|
72
73
|
return <BadgeDot size="small" style={styles.badgeDot} />;
|
|
73
74
|
}
|
|
75
|
+
if (badgeType === 'dot-animation') {
|
|
76
|
+
return (
|
|
77
|
+
<BadgeDotAnimation size="small" style={styles.badgeDotAnimation} />
|
|
78
|
+
);
|
|
79
|
+
}
|
|
74
80
|
|
|
75
81
|
if (isNumber(badgeValue)) {
|
|
76
82
|
return <Badge label={badgeValue} style={styles.badge} />;
|
|
@@ -666,7 +672,7 @@ const HeaderRightAction: React.FC<any> = ({children, ...restProps}) => {
|
|
|
666
672
|
if (__DEV__) {
|
|
667
673
|
Alert.alert(
|
|
668
674
|
'HeaderRightAction',
|
|
669
|
-
'This component is deprecated, please use
|
|
675
|
+
'This component is deprecated and will removed 10/2024, please use new type headerRight specs instead v0.92.34'
|
|
670
676
|
);
|
|
671
677
|
}
|
|
672
678
|
const validateType = (child: React.ReactElement) => {
|
|
@@ -763,6 +769,11 @@ const styles = StyleSheet.create({
|
|
|
763
769
|
top: -Spacing.XS,
|
|
764
770
|
right: -Spacing.XS,
|
|
765
771
|
},
|
|
772
|
+
badgeDotAnimation: {
|
|
773
|
+
position: 'absolute',
|
|
774
|
+
top: -Spacing.XS,
|
|
775
|
+
right: -Spacing.XS,
|
|
776
|
+
},
|
|
766
777
|
extendedHeader: {
|
|
767
778
|
aspectRatio: 1.75,
|
|
768
779
|
position: 'absolute',
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {NativeModules} from 'react-native';
|
|
2
|
+
import {LocalizationObject} from './types';
|
|
3
|
+
import defaultLanguage from '../Assets/language.json';
|
|
4
|
+
|
|
5
|
+
class Localize {
|
|
6
|
+
private assets: LocalizationObject;
|
|
7
|
+
private currentLanguage: 'en' | 'vi';
|
|
8
|
+
|
|
9
|
+
constructor(assets: LocalizationObject) {
|
|
10
|
+
this.assets = {
|
|
11
|
+
vi: {...assets?.vi, ...defaultLanguage.vi},
|
|
12
|
+
en: {...assets?.en, ...defaultLanguage.en},
|
|
13
|
+
};
|
|
14
|
+
this.currentLanguage = 'vi';
|
|
15
|
+
if (NativeModules?.RNResource?.language === 'en') {
|
|
16
|
+
this.currentLanguage = 'en';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
translate(key: string) {
|
|
21
|
+
return this.assets[this.currentLanguage]?.[key] || key;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
translateData(data: {vi: string; en: string}) {
|
|
25
|
+
return data[this.currentLanguage] || data.vi || JSON.stringify(data);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
addTranslations(translations: LocalizationObject) {
|
|
29
|
+
this.assets = translations;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setLanguage(language: 'en' | 'vi') {
|
|
33
|
+
this.currentLanguage = language;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default Localize;
|
|
@@ -10,9 +10,9 @@ import StackScreen from './StackScreen';
|
|
|
10
10
|
import ModalScreen from './ModalScreen';
|
|
11
11
|
import Navigator from './Navigator';
|
|
12
12
|
import {getDialogOptions, getModalOptions, getStackOptions} from './utils';
|
|
13
|
-
import {
|
|
13
|
+
import {NavigationContainerProps} from './types';
|
|
14
14
|
import {ApplicationContext} from './index';
|
|
15
|
-
import
|
|
15
|
+
import Localize from './Localize';
|
|
16
16
|
|
|
17
17
|
const Stack = createStackNavigator();
|
|
18
18
|
|
|
@@ -21,15 +21,18 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
|
|
|
21
21
|
theme,
|
|
22
22
|
options,
|
|
23
23
|
maxApi,
|
|
24
|
-
localization,
|
|
25
24
|
initialParams,
|
|
25
|
+
localize = new Localize({vi: {}, en: {}}),
|
|
26
26
|
}) => {
|
|
27
27
|
const navigationRef = useRef<NavigationContainerRef>(null);
|
|
28
28
|
const isReady = useRef(false);
|
|
29
29
|
const navigator = useRef(new Navigator(navigationRef, isReady));
|
|
30
30
|
const [showGrid, setShowGrid] = useState(false);
|
|
31
31
|
const config = useRef<any>();
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* inject data for navigator
|
|
34
|
+
*/
|
|
35
|
+
navigator.current.maxApi = maxApi;
|
|
33
36
|
/**
|
|
34
37
|
* handle mini language & listen change
|
|
35
38
|
* engine only shake to enable grid view
|
|
@@ -39,7 +42,7 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
|
|
|
39
42
|
'onChangeGrid',
|
|
40
43
|
enable => {
|
|
41
44
|
setShowGrid(!!enable);
|
|
42
|
-
}
|
|
45
|
+
}
|
|
43
46
|
);
|
|
44
47
|
|
|
45
48
|
return () => {
|
|
@@ -47,22 +50,8 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
|
|
|
47
50
|
};
|
|
48
51
|
}, []);
|
|
49
52
|
|
|
50
|
-
/**
|
|
51
|
-
* inject data for navigator
|
|
52
|
-
*/
|
|
53
|
-
navigator.current.maxApi = maxApi;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* translate
|
|
57
|
-
* @param key
|
|
58
|
-
*/
|
|
59
53
|
const translate = (key: string) => {
|
|
60
|
-
|
|
61
|
-
const mergedLocalization: LocalizationObject = {
|
|
62
|
-
vi: {...localization?.vi, ...kitLocalization.vi},
|
|
63
|
-
en: {...localization?.en, ...kitLocalization.en},
|
|
64
|
-
};
|
|
65
|
-
return mergedLocalization?.[language]?.[key] || key;
|
|
54
|
+
return localize?.translate(key);
|
|
66
55
|
};
|
|
67
56
|
|
|
68
57
|
return (
|
|
@@ -8,80 +8,6 @@ import {GridSystem} from '../Layout';
|
|
|
8
8
|
import {Text} from '../Text';
|
|
9
9
|
import {Colors, Radius, Spacing} from '../Consts';
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Empty screen for unvalidated screen name
|
|
13
|
-
* @constructor
|
|
14
|
-
*/
|
|
15
|
-
const EmptyScreen: React.FC = () => {
|
|
16
|
-
return (
|
|
17
|
-
<View
|
|
18
|
-
style={{
|
|
19
|
-
backgroundColor: Colors.black_01,
|
|
20
|
-
padding: Spacing.M,
|
|
21
|
-
borderRadius: Radius.M,
|
|
22
|
-
}}>
|
|
23
|
-
<Text typography={'header_m_bold'}>Unvalidated screen name</Text>
|
|
24
|
-
<Text
|
|
25
|
-
typography={'description_default_regular'}
|
|
26
|
-
style={{paddingVertical: Spacing.S}}>
|
|
27
|
-
Your screen has not been rendered because Platform has not detected the
|
|
28
|
-
screen name. Please migrate to support this feature.
|
|
29
|
-
</Text>
|
|
30
|
-
<Text typography={'header_default'} style={{width: '100%'}}>
|
|
31
|
-
Possible fixes:
|
|
32
|
-
</Text>
|
|
33
|
-
<View style={{marginLeft: Spacing.S}}>
|
|
34
|
-
<Text typography={'body_default_regular'}>1. Off webpack config</Text>
|
|
35
|
-
<Text typography={'body_default_regular'}>
|
|
36
|
-
2. Screen name refactoring
|
|
37
|
-
</Text>
|
|
38
|
-
<View style={{marginLeft: Spacing.S}}>
|
|
39
|
-
<Text
|
|
40
|
-
typography={'description_default_regular'}
|
|
41
|
-
style={{marginBottom: Spacing.S}}>
|
|
42
|
-
- Với các screen push/import dạng arrow function ={'>'} đổi thành
|
|
43
|
-
named function
|
|
44
|
-
</Text>
|
|
45
|
-
<Text
|
|
46
|
-
typography={'description_default_regular'}
|
|
47
|
-
style={{marginBottom: Spacing.S}}>
|
|
48
|
-
- Với các screen import dạng High Order Component ={'>'} rename
|
|
49
|
-
generic function thành named function
|
|
50
|
-
</Text>
|
|
51
|
-
<Text
|
|
52
|
-
typography={'description_default_regular'}
|
|
53
|
-
style={{marginBottom: Spacing.S}}>
|
|
54
|
-
- Với Tab Navigator, Platform sẽ lấy Tab Container làm screen name.
|
|
55
|
-
Trong trường hợp miniapp muốn track riêng action cho từng tab, tham
|
|
56
|
-
khảo theo docs
|
|
57
|
-
</Text>
|
|
58
|
-
</View>
|
|
59
|
-
</View>
|
|
60
|
-
<Text typography={'header_s_semibold'}>More information: </Text>
|
|
61
|
-
<Text
|
|
62
|
-
typography={'body_default_regular'}
|
|
63
|
-
color={Colors.blue_01}
|
|
64
|
-
onPress={() => {
|
|
65
|
-
Linking.openURL(
|
|
66
|
-
'https://gitlab.mservice.com.vn/momo-platform/mini-app/-/tree/vn.momo.uikits/dev/app'
|
|
67
|
-
);
|
|
68
|
-
}}>
|
|
69
|
-
Mini App: vn.momo.uikits/dev
|
|
70
|
-
</Text>
|
|
71
|
-
<Text
|
|
72
|
-
typography={'body_default_regular'}
|
|
73
|
-
color={Colors.blue_01}
|
|
74
|
-
onPress={() => {
|
|
75
|
-
Linking.openURL(
|
|
76
|
-
'https://docs.google.com/presentation/d/1bIFoh8gRXb6hsnGJz4O1Kxg4_oNikB1KuuEt2wULCDM/edit?usp=sharing'
|
|
77
|
-
);
|
|
78
|
-
}}>
|
|
79
|
-
https://docs.google.com/presentation/d/1bIFoh8gRXb6hsnGJz4O1Kxg4_oNikB1KuuEt2wULCDM/edit?usp=sharing
|
|
80
|
-
</Text>
|
|
81
|
-
</View>
|
|
82
|
-
);
|
|
83
|
-
};
|
|
84
|
-
|
|
85
11
|
/**
|
|
86
12
|
* container for stack screen
|
|
87
13
|
* @param props
|
|
@@ -89,20 +15,20 @@ const EmptyScreen: React.FC = () => {
|
|
|
89
15
|
*/
|
|
90
16
|
const StackScreen: React.FC<ScreenParams> = props => {
|
|
91
17
|
const {showGrid, navigator} = useContext(ApplicationContext);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
18
|
+
const tracking = useRef<any>({
|
|
19
|
+
timeoutLoad: undefined,
|
|
20
|
+
timeoutInteraction: undefined,
|
|
21
|
+
timeoutTracking: undefined,
|
|
22
|
+
startTime: Date.now(),
|
|
23
|
+
endTime: Date.now(),
|
|
98
24
|
traceIdLoad: undefined,
|
|
99
25
|
traceIdInteraction: undefined,
|
|
100
26
|
releaseLoad: undefined,
|
|
101
27
|
releaseInteraction: undefined,
|
|
102
|
-
|
|
103
|
-
|
|
28
|
+
timeLoad: 0,
|
|
29
|
+
timeInteraction: 0,
|
|
104
30
|
});
|
|
105
|
-
|
|
31
|
+
|
|
106
32
|
const context = useContext<any>(MiniAppContext);
|
|
107
33
|
const {screen: Component, options, initialParams} = props.route.params;
|
|
108
34
|
const navigation = new Navigation(props.navigation);
|
|
@@ -118,6 +44,7 @@ const StackScreen: React.FC<ScreenParams> = props => {
|
|
|
118
44
|
delete data.initialParams;
|
|
119
45
|
|
|
120
46
|
const screenName = Component?.name || Component?.type?.name || 'Invalid';
|
|
47
|
+
const routes = props.navigation.getState()?.routes || [];
|
|
121
48
|
|
|
122
49
|
/**
|
|
123
50
|
* set options for screen
|
|
@@ -135,15 +62,6 @@ const StackScreen: React.FC<ScreenParams> = props => {
|
|
|
135
62
|
};
|
|
136
63
|
}
|
|
137
64
|
navigation.setOptions(defaultOptions);
|
|
138
|
-
navigator?.maxApi?.startTraceScreenLoad?.(screenName, (data: any) => {
|
|
139
|
-
tracked.current.traceIdLoad = data?.traceId;
|
|
140
|
-
});
|
|
141
|
-
navigator?.maxApi?.startTraceScreenInteraction?.(
|
|
142
|
-
screenName,
|
|
143
|
-
(data: any) => {
|
|
144
|
-
tracked.current.traceIdInteraction = data?.traceId;
|
|
145
|
-
}
|
|
146
|
-
);
|
|
147
65
|
}, [options]);
|
|
148
66
|
|
|
149
67
|
/**
|
|
@@ -155,19 +73,39 @@ const StackScreen: React.FC<ScreenParams> = props => {
|
|
|
155
73
|
navigator?.showModal({screen: EmptyScreen});
|
|
156
74
|
}, 300);
|
|
157
75
|
}
|
|
158
|
-
|
|
159
|
-
|
|
76
|
+
|
|
77
|
+
const subscription = props.navigation?.addListener?.('focus', () => {
|
|
78
|
+
navigator?.maxApi?.of?.({screenName});
|
|
79
|
+
navigator?.maxApi?.getDataObserver('CURRENT_SCREEN', (data: any) => {
|
|
80
|
+
let preScreenName = data?.screenName;
|
|
81
|
+
if (routes?.length > 1) {
|
|
82
|
+
const screen = routes?.[routes?.length - 2]?.params?.screen;
|
|
83
|
+
preScreenName = screen?.name || screen?.type?.name || 'Invalid';
|
|
84
|
+
}
|
|
85
|
+
onScreenNavigated(preScreenName);
|
|
86
|
+
navigator?.maxApi?.setObserver('CURRENT_SCREEN', {screenName});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
navigator?.maxApi?.startTraceScreenLoad?.(screenName, (data: any) => {
|
|
90
|
+
tracking.current.traceIdLoad = data?.traceId;
|
|
91
|
+
});
|
|
92
|
+
navigator?.maxApi?.startTraceScreenInteraction?.(
|
|
93
|
+
screenName,
|
|
94
|
+
(data: any) => {
|
|
95
|
+
tracking.current.traceIdInteraction = data?.traceId;
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
tracking.current.timeoutTracking = setTimeout(() => {
|
|
160
100
|
onScreenLoad();
|
|
161
|
-
}, 5000);
|
|
162
|
-
tracked.current.timeoutInteraction = setTimeout(() => {
|
|
163
101
|
onScreenInteraction();
|
|
164
102
|
}, 5000);
|
|
165
103
|
|
|
166
104
|
return () => {
|
|
167
|
-
clearTimeout(tracked.current.timeoutLoad);
|
|
168
|
-
clearTimeout(tracked.current.timeoutInteraction);
|
|
169
105
|
onScreenLoad();
|
|
170
106
|
onScreenInteraction();
|
|
107
|
+
clearTimeout(tracking.current.timeoutTracking);
|
|
108
|
+
subscription?.();
|
|
171
109
|
};
|
|
172
110
|
}, []);
|
|
173
111
|
|
|
@@ -175,9 +113,10 @@ const StackScreen: React.FC<ScreenParams> = props => {
|
|
|
175
113
|
* tracking for screen load
|
|
176
114
|
*/
|
|
177
115
|
const onScreenLoad = () => {
|
|
178
|
-
if (!
|
|
179
|
-
if (
|
|
180
|
-
|
|
116
|
+
if (!tracking.current?.releaseLoad) {
|
|
117
|
+
if (tracking.current.timeLoad === 0) {
|
|
118
|
+
tracking.current.timeLoad =
|
|
119
|
+
tracking.current.endTime - tracking.current.startTime;
|
|
181
120
|
}
|
|
182
121
|
|
|
183
122
|
context.autoTracking?.({
|
|
@@ -185,26 +124,26 @@ const StackScreen: React.FC<ScreenParams> = props => {
|
|
|
185
124
|
code: context.code,
|
|
186
125
|
buildNumber: context.buildNumber,
|
|
187
126
|
screenName,
|
|
188
|
-
action: 'push',
|
|
189
127
|
componentName: 'Screen',
|
|
190
128
|
state: 'load',
|
|
191
|
-
duration:
|
|
129
|
+
duration: tracking.current.timeLoad,
|
|
192
130
|
});
|
|
193
131
|
navigator?.maxApi?.stopTrace?.(
|
|
194
|
-
|
|
195
|
-
{value:
|
|
132
|
+
tracking.current.traceIdLoad,
|
|
133
|
+
{value: tracking.current.timeLoad / 1000},
|
|
196
134
|
null
|
|
197
135
|
);
|
|
198
|
-
|
|
136
|
+
tracking.current.releaseLoad = true;
|
|
199
137
|
|
|
200
138
|
/**
|
|
201
139
|
* debug
|
|
202
140
|
*/
|
|
203
141
|
navigator?.maxApi?.showToastDebug?.({
|
|
204
|
-
appId:
|
|
205
|
-
message: `${screenName} screen_load_time ${
|
|
142
|
+
appId: context.appId,
|
|
143
|
+
message: `${screenName} screen_load_time ${tracking.current.timeLoad}`,
|
|
144
|
+
type: 'ERROR',
|
|
206
145
|
});
|
|
207
|
-
if (
|
|
146
|
+
if (tracking.current.timeLoad <= 0 && context.enableAutoId) {
|
|
208
147
|
Alert.alert(screenName, "Can't get screen load time");
|
|
209
148
|
}
|
|
210
149
|
}
|
|
@@ -214,12 +153,13 @@ const StackScreen: React.FC<ScreenParams> = props => {
|
|
|
214
153
|
* tracking for screen load
|
|
215
154
|
*/
|
|
216
155
|
const onScreenInteraction = () => {
|
|
217
|
-
if (!
|
|
218
|
-
if (
|
|
219
|
-
|
|
156
|
+
if (!tracking.current?.releaseInteraction) {
|
|
157
|
+
if (tracking.current.timeLoad === 0) {
|
|
158
|
+
tracking.current.timeLoad =
|
|
159
|
+
tracking.current.endTime - tracking.current.startTime;
|
|
220
160
|
}
|
|
221
|
-
if (
|
|
222
|
-
|
|
161
|
+
if (tracking.current.timeInteraction === 0) {
|
|
162
|
+
tracking.current.timeInteraction = tracking.current.timeLoad;
|
|
223
163
|
}
|
|
224
164
|
|
|
225
165
|
context.autoTracking?.({
|
|
@@ -229,43 +169,74 @@ const StackScreen: React.FC<ScreenParams> = props => {
|
|
|
229
169
|
screenName,
|
|
230
170
|
componentName: 'Screen',
|
|
231
171
|
state: 'interaction',
|
|
232
|
-
duration:
|
|
172
|
+
duration: tracking.current.timeInteraction - tracking.current.timeLoad,
|
|
173
|
+
totalDuration: tracking.current.timeInteraction,
|
|
233
174
|
});
|
|
234
175
|
navigator?.maxApi?.stopTrace?.(
|
|
235
|
-
|
|
236
|
-
{value:
|
|
176
|
+
tracking.current.traceIdInteraction,
|
|
177
|
+
{value: tracking.current.timeInteraction / 1000},
|
|
237
178
|
null
|
|
238
179
|
);
|
|
239
|
-
|
|
180
|
+
tracking.current.releaseInteraction = true;
|
|
240
181
|
|
|
241
182
|
/**
|
|
242
183
|
* debug toast
|
|
243
184
|
*/
|
|
244
185
|
navigator?.maxApi?.showToastDebug?.({
|
|
245
|
-
appId:
|
|
246
|
-
message: `${screenName} screen_interaction_time ${
|
|
186
|
+
appId: context.appId,
|
|
187
|
+
message: `${screenName} screen_interaction_time ${tracking.current.timeInteraction}`,
|
|
188
|
+
type: 'ERROR',
|
|
247
189
|
});
|
|
248
|
-
if (
|
|
190
|
+
if (tracking.current.timeInteraction <= 0 && context.enableAutoId) {
|
|
249
191
|
Alert.alert(screenName, "Can't get screen interaction time");
|
|
250
192
|
}
|
|
251
193
|
}
|
|
252
194
|
};
|
|
253
195
|
|
|
196
|
+
/**
|
|
197
|
+
* tracking for screen navigated
|
|
198
|
+
*/
|
|
199
|
+
const onScreenNavigated = (preScreenName: string) => {
|
|
200
|
+
context.autoTracking?.({
|
|
201
|
+
appId: context.appId,
|
|
202
|
+
code: context.code,
|
|
203
|
+
buildNumber: context.buildNumber,
|
|
204
|
+
preScreenName,
|
|
205
|
+
screenName,
|
|
206
|
+
componentName: 'Screen',
|
|
207
|
+
state: 'navigated',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* debug toast
|
|
212
|
+
*/
|
|
213
|
+
navigator?.maxApi?.showToastDebug?.({
|
|
214
|
+
appId: context.appId,
|
|
215
|
+
message: `${screenName} screen_navigated`,
|
|
216
|
+
type: 'ERROR',
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
|
|
254
220
|
return (
|
|
255
221
|
<ScreenContext.Provider
|
|
256
222
|
value={{
|
|
257
223
|
screenName,
|
|
258
224
|
onElementLoad: () => {
|
|
259
|
-
clearTimeout(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
225
|
+
clearTimeout(tracking.current.timeoutLoad);
|
|
226
|
+
tracking.current.endTime = Date.now();
|
|
227
|
+
tracking.current.timeoutInteraction?.cancel?.();
|
|
228
|
+
tracking.current.timeoutInteraction =
|
|
229
|
+
InteractionManager.runAfterInteractions(() => {
|
|
230
|
+
tracking.current.timeInteraction =
|
|
231
|
+
Date.now() - tracking.current.startTime;
|
|
232
|
+
});
|
|
233
|
+
tracking.current.timeoutLoad = setTimeout(() => {
|
|
234
|
+
if (tracking.current.timeLoad === 0) {
|
|
235
|
+
tracking.current.timeLoad =
|
|
236
|
+
tracking.current.endTime - tracking.current.startTime;
|
|
268
237
|
}
|
|
238
|
+
onScreenLoad();
|
|
239
|
+
onScreenInteraction();
|
|
269
240
|
}, 2000);
|
|
270
241
|
},
|
|
271
242
|
}}>
|
|
@@ -280,4 +251,78 @@ const StackScreen: React.FC<ScreenParams> = props => {
|
|
|
280
251
|
);
|
|
281
252
|
};
|
|
282
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Empty screen for unvalidated screen name
|
|
256
|
+
* @constructor
|
|
257
|
+
*/
|
|
258
|
+
const EmptyScreen: React.FC = () => {
|
|
259
|
+
return (
|
|
260
|
+
<View
|
|
261
|
+
style={{
|
|
262
|
+
backgroundColor: Colors.black_01,
|
|
263
|
+
padding: Spacing.M,
|
|
264
|
+
borderRadius: Radius.M,
|
|
265
|
+
}}>
|
|
266
|
+
<Text typography={'header_m_bold'}>Unvalidated screen name</Text>
|
|
267
|
+
<Text
|
|
268
|
+
typography={'description_default_regular'}
|
|
269
|
+
style={{paddingVertical: Spacing.S}}>
|
|
270
|
+
Your screen has not been rendered because Platform has not detected the
|
|
271
|
+
screen name. Please migrate to support this feature.
|
|
272
|
+
</Text>
|
|
273
|
+
<Text typography={'header_default'} style={{width: '100%'}}>
|
|
274
|
+
Possible fixes:
|
|
275
|
+
</Text>
|
|
276
|
+
<View style={{marginLeft: Spacing.S}}>
|
|
277
|
+
<Text typography={'body_default_regular'}>1. Off webpack config</Text>
|
|
278
|
+
<Text typography={'body_default_regular'}>
|
|
279
|
+
2. Screen name refactoring
|
|
280
|
+
</Text>
|
|
281
|
+
<View style={{marginLeft: Spacing.S}}>
|
|
282
|
+
<Text
|
|
283
|
+
typography={'description_default_regular'}
|
|
284
|
+
style={{marginBottom: Spacing.S}}>
|
|
285
|
+
- Với các screen push/import dạng arrow function ={'>'} đổi thành
|
|
286
|
+
named function
|
|
287
|
+
</Text>
|
|
288
|
+
<Text
|
|
289
|
+
typography={'description_default_regular'}
|
|
290
|
+
style={{marginBottom: Spacing.S}}>
|
|
291
|
+
- Với các screen import dạng High Order Component ={'>'} rename
|
|
292
|
+
generic function thành named function
|
|
293
|
+
</Text>
|
|
294
|
+
<Text
|
|
295
|
+
typography={'description_default_regular'}
|
|
296
|
+
style={{marginBottom: Spacing.S}}>
|
|
297
|
+
- Với Tab Navigator, Platform sẽ lấy Tab Container làm screen name.
|
|
298
|
+
Trong trường hợp miniapp muốn track riêng action cho từng tab, tham
|
|
299
|
+
khảo theo docs
|
|
300
|
+
</Text>
|
|
301
|
+
</View>
|
|
302
|
+
</View>
|
|
303
|
+
<Text typography={'header_s_semibold'}>More information: </Text>
|
|
304
|
+
<Text
|
|
305
|
+
typography={'body_default_regular'}
|
|
306
|
+
color={Colors.blue_01}
|
|
307
|
+
onPress={() => {
|
|
308
|
+
Linking.openURL(
|
|
309
|
+
'https://gitlab.mservice.com.vn/momo-platform/mini-app/-/tree/vn.momo.uikits/dev/app'
|
|
310
|
+
);
|
|
311
|
+
}}>
|
|
312
|
+
Mini App: vn.momo.uikits/dev
|
|
313
|
+
</Text>
|
|
314
|
+
<Text
|
|
315
|
+
typography={'body_default_regular'}
|
|
316
|
+
color={Colors.blue_01}
|
|
317
|
+
onPress={() => {
|
|
318
|
+
Linking.openURL(
|
|
319
|
+
'https://docs.google.com/presentation/d/1bIFoh8gRXb6hsnGJz4O1Kxg4_oNikB1KuuEt2wULCDM/edit?usp=sharing'
|
|
320
|
+
);
|
|
321
|
+
}}>
|
|
322
|
+
https://docs.google.com/presentation/d/1bIFoh8gRXb6hsnGJz4O1Kxg4_oNikB1KuuEt2wULCDM/edit?usp=sharing
|
|
323
|
+
</Text>
|
|
324
|
+
</View>
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
283
328
|
export default StackScreen;
|
package/Application/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {Platform} from 'react-native';
|
|
2
|
+
import {createContext} from 'react';
|
|
2
3
|
import {NavigationContainer} from './NavigationContainer';
|
|
4
|
+
import Localize from './Localize';
|
|
3
5
|
import Screen from '../Layout/Screen';
|
|
4
6
|
import {
|
|
5
7
|
HeaderAnimated,
|
|
@@ -10,7 +12,6 @@ import {
|
|
|
10
12
|
HeaderRightAction,
|
|
11
13
|
} from './Components';
|
|
12
14
|
import BottomTab from './BottomTab';
|
|
13
|
-
import {createContext} from 'react';
|
|
14
15
|
import {defaultContext} from '../Consts';
|
|
15
16
|
|
|
16
17
|
const Context = createContext({});
|
|
@@ -26,6 +27,7 @@ export {
|
|
|
26
27
|
ScreenContext,
|
|
27
28
|
ComponentContext,
|
|
28
29
|
NavigationContainer,
|
|
30
|
+
Localize,
|
|
29
31
|
Screen,
|
|
30
32
|
BottomTab,
|
|
31
33
|
NavigationButton,
|
package/Application/types.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {StackNavigationOptions} from '@react-navigation/stack';
|
|
2
1
|
import {EventArg} from '@react-navigation/core';
|
|
2
|
+
import {StackNavigationOptions} from '@react-navigation/stack';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import {Animated, ViewProps} from 'react-native';
|
|
5
5
|
import {PopupNotifyProps} from '../Popup/types';
|
|
6
|
+
import Localize from './Localize';
|
|
6
7
|
import Navigation from './Navigation';
|
|
7
8
|
import Navigator from './Navigator';
|
|
8
9
|
|
|
@@ -85,8 +86,8 @@ export type NavigationContainerProps = {
|
|
|
85
86
|
options?: NavigationOptions;
|
|
86
87
|
theme: Theme;
|
|
87
88
|
maxApi: any;
|
|
88
|
-
localization?: LocalizationObject;
|
|
89
89
|
initialParams?: any;
|
|
90
|
+
localize: Localize;
|
|
90
91
|
};
|
|
91
92
|
|
|
92
93
|
export type NavigationScreenProps = {
|
|
@@ -129,7 +130,7 @@ export type NavigationButtonProps = {
|
|
|
129
130
|
tintColor?: string;
|
|
130
131
|
useBorder?: boolean;
|
|
131
132
|
onPress: () => void;
|
|
132
|
-
badgeType?: 'dot' | 'number';
|
|
133
|
+
badgeType?: 'dot' | 'number' | 'dot-animation';
|
|
133
134
|
badgeValue?: number;
|
|
134
135
|
accessibilityLabel?: string;
|
|
135
136
|
};
|
|
@@ -137,7 +138,7 @@ export type NavigationButtonProps = {
|
|
|
137
138
|
export type PinnedToolType = {
|
|
138
139
|
key: string;
|
|
139
140
|
badgeValue?: number;
|
|
140
|
-
badgeType?: 'number' | 'dot';
|
|
141
|
+
badgeType?: 'number' | 'dot' | 'dot-animation';
|
|
141
142
|
};
|
|
142
143
|
|
|
143
144
|
export type RuntimeToolType = {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, {useEffect, useRef} from 'react';
|
|
2
|
+
import {Animated, View} from 'react-native';
|
|
3
|
+
import {BadgeDotProps} from './types';
|
|
4
|
+
import styles from './styles';
|
|
5
|
+
|
|
6
|
+
const DURATION = 500;
|
|
7
|
+
|
|
8
|
+
const BadgeDotAnimation = ({size, style}: BadgeDotProps) => {
|
|
9
|
+
// Refs for animated values
|
|
10
|
+
const scaleAnim = useRef(new Animated.Value(1)).current;
|
|
11
|
+
const waveScaleAnim = useRef(new Animated.Value(1)).current;
|
|
12
|
+
const waveOpacityAnim = useRef(new Animated.Value(0)).current;
|
|
13
|
+
|
|
14
|
+
const dotStyle =
|
|
15
|
+
size === 'small' ? styles.dotAnimationSmall : styles.dotAnimation;
|
|
16
|
+
const waveStyle = size === 'small' ? styles.waveSmall : styles.wave;
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
// Infinite loop animation for the scale and wave effect
|
|
20
|
+
const animation = Animated.loop(
|
|
21
|
+
Animated.parallel([
|
|
22
|
+
// Dot pulse animation
|
|
23
|
+
Animated.sequence([
|
|
24
|
+
Animated.spring(scaleAnim, {
|
|
25
|
+
toValue: 1, // Scale up slightly
|
|
26
|
+
friction: 5, // Controls the "bounciness" of the spring
|
|
27
|
+
tension: 30, // Controls the "stiffness" of the spring
|
|
28
|
+
useNativeDriver: true,
|
|
29
|
+
}),
|
|
30
|
+
Animated.spring(scaleAnim, {
|
|
31
|
+
toValue: 1.1,
|
|
32
|
+
friction: 5,
|
|
33
|
+
tension: 30,
|
|
34
|
+
useNativeDriver: true,
|
|
35
|
+
}),
|
|
36
|
+
]), // Wave animation
|
|
37
|
+
Animated.sequence([
|
|
38
|
+
Animated.timing(waveScaleAnim, {
|
|
39
|
+
toValue: 2.5,
|
|
40
|
+
duration: DURATION * 3,
|
|
41
|
+
useNativeDriver: true,
|
|
42
|
+
}),
|
|
43
|
+
Animated.timing(waveScaleAnim, {
|
|
44
|
+
toValue: 1, // Reset wave size
|
|
45
|
+
duration: 0,
|
|
46
|
+
useNativeDriver: true,
|
|
47
|
+
}),
|
|
48
|
+
]), // Wave opacity animation
|
|
49
|
+
Animated.sequence([
|
|
50
|
+
Animated.timing(waveOpacityAnim, {
|
|
51
|
+
toValue: 0.3, // Wave becomes visible
|
|
52
|
+
duration: DURATION * 2,
|
|
53
|
+
useNativeDriver: true,
|
|
54
|
+
}),
|
|
55
|
+
Animated.timing(waveOpacityAnim, {
|
|
56
|
+
toValue: 0, // Wave fades out
|
|
57
|
+
duration: DURATION,
|
|
58
|
+
useNativeDriver: true,
|
|
59
|
+
}),
|
|
60
|
+
]),
|
|
61
|
+
])
|
|
62
|
+
);
|
|
63
|
+
animation.start();
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
animation.stop();
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<View style={[styles.dotAnimationContainer, style]}>
|
|
72
|
+
{/* Wave Animation */}
|
|
73
|
+
<Animated.View
|
|
74
|
+
style={[
|
|
75
|
+
waveStyle,
|
|
76
|
+
{
|
|
77
|
+
transform: [{scale: waveScaleAnim}],
|
|
78
|
+
opacity: waveOpacityAnim,
|
|
79
|
+
},
|
|
80
|
+
]}
|
|
81
|
+
/>
|
|
82
|
+
{/* Dot Animation */}
|
|
83
|
+
<Animated.View
|
|
84
|
+
style={[
|
|
85
|
+
dotStyle,
|
|
86
|
+
{
|
|
87
|
+
transform: [{scale: scaleAnim}],
|
|
88
|
+
},
|
|
89
|
+
]}
|
|
90
|
+
/>
|
|
91
|
+
</View>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export default BadgeDotAnimation;
|
package/Badge/index.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import Badge from './Badge';
|
|
2
2
|
import BadgeDot from './BadgeDot';
|
|
3
3
|
import BadgeRibbon from './BadgeRibbon';
|
|
4
|
+
import BadgeDotAnimation from './BadgeDotAnimation';
|
|
4
5
|
import {BadgeProps, BadgeDotProps, BadgeRibbonProps} from './types';
|
|
5
6
|
|
|
6
|
-
export {Badge, BadgeRibbon, BadgeDot};
|
|
7
|
+
export {Badge, BadgeRibbon, BadgeDot, BadgeDotAnimation};
|
|
7
8
|
export type {BadgeProps, BadgeDotProps, BadgeRibbonProps};
|
package/Badge/styles.ts
CHANGED
|
@@ -2,6 +2,10 @@ import {StyleSheet} from 'react-native';
|
|
|
2
2
|
import {Colors, Radius, Spacing} from '../Consts';
|
|
3
3
|
import {scaleSize} from '../Text';
|
|
4
4
|
|
|
5
|
+
const DOT_SIZE = 16;
|
|
6
|
+
|
|
7
|
+
const DOT_SMALL_SIZE = 10;
|
|
8
|
+
|
|
5
9
|
export default StyleSheet.create({
|
|
6
10
|
badge: {
|
|
7
11
|
paddingHorizontal: Spacing.XS,
|
|
@@ -15,21 +19,61 @@ export default StyleSheet.create({
|
|
|
15
19
|
borderColor: Colors.black_01,
|
|
16
20
|
alignSelf: 'baseline',
|
|
17
21
|
},
|
|
22
|
+
dotAnimationContainer: {
|
|
23
|
+
position: 'relative',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
justifyContent: 'center',
|
|
26
|
+
},
|
|
18
27
|
dot: {
|
|
19
|
-
width:
|
|
20
|
-
height:
|
|
28
|
+
width: DOT_SIZE,
|
|
29
|
+
height: DOT_SIZE,
|
|
21
30
|
borderWidth: 2,
|
|
22
31
|
borderColor: Colors.black_01,
|
|
23
32
|
backgroundColor: Colors.red_03,
|
|
24
|
-
borderRadius:
|
|
33
|
+
borderRadius: DOT_SIZE / 2,
|
|
25
34
|
},
|
|
26
35
|
dotSmall: {
|
|
27
|
-
width:
|
|
28
|
-
height:
|
|
36
|
+
width: DOT_SMALL_SIZE,
|
|
37
|
+
height: DOT_SMALL_SIZE,
|
|
29
38
|
borderWidth: 1,
|
|
30
39
|
borderColor: Colors.black_01,
|
|
31
40
|
backgroundColor: Colors.red_03,
|
|
32
|
-
borderRadius:
|
|
41
|
+
borderRadius: DOT_SMALL_SIZE / 2,
|
|
42
|
+
},
|
|
43
|
+
dotAnimation: {
|
|
44
|
+
width: DOT_SIZE,
|
|
45
|
+
height: DOT_SIZE,
|
|
46
|
+
borderColor: Colors.black_01,
|
|
47
|
+
backgroundColor: Colors.red_03,
|
|
48
|
+
borderRadius: DOT_SIZE / 2,
|
|
49
|
+
borderWidth: 0,
|
|
50
|
+
},
|
|
51
|
+
dotAnimationSmall: {
|
|
52
|
+
width: DOT_SMALL_SIZE,
|
|
53
|
+
height: DOT_SMALL_SIZE,
|
|
54
|
+
borderColor: Colors.black_01,
|
|
55
|
+
backgroundColor: Colors.red_03,
|
|
56
|
+
borderRadius: DOT_SMALL_SIZE / 2,
|
|
57
|
+
borderWidth: 0,
|
|
58
|
+
},
|
|
59
|
+
wave: {
|
|
60
|
+
width: DOT_SIZE,
|
|
61
|
+
height: DOT_SIZE,
|
|
62
|
+
borderColor: Colors.black_01,
|
|
63
|
+
backgroundColor: Colors.red_03,
|
|
64
|
+
borderRadius: DOT_SIZE / 2,
|
|
65
|
+
opacity: 0,
|
|
66
|
+
position: 'absolute',
|
|
67
|
+
borderWidth: 0,
|
|
68
|
+
},
|
|
69
|
+
waveSmall: {
|
|
70
|
+
width: DOT_SMALL_SIZE,
|
|
71
|
+
height: DOT_SMALL_SIZE,
|
|
72
|
+
borderColor: Colors.black_01,
|
|
73
|
+
backgroundColor: Colors.red_03,
|
|
74
|
+
borderRadius: DOT_SMALL_SIZE / 2,
|
|
75
|
+
borderWidth: 0,
|
|
76
|
+
position: 'absolute',
|
|
33
77
|
},
|
|
34
78
|
ribbon: {
|
|
35
79
|
alignSelf: 'baseline',
|
package/Input/styles.ts
CHANGED
|
@@ -32,10 +32,9 @@ export default StyleSheet.create({
|
|
|
32
32
|
alignItems: 'center',
|
|
33
33
|
},
|
|
34
34
|
floatingIcon: {marginLeft: Spacing.XS},
|
|
35
|
-
errorIcon: {marginRight: Spacing.XS},
|
|
35
|
+
errorIcon: {marginRight: Spacing.XS, marginTop: Spacing.XXS},
|
|
36
36
|
errorView: {
|
|
37
37
|
flexDirection: 'row',
|
|
38
|
-
alignItems: 'center',
|
|
39
38
|
marginTop: Spacing.XS,
|
|
40
39
|
},
|
|
41
40
|
inputView: {
|
package/Layout/Screen.tsx
CHANGED
|
@@ -132,13 +132,12 @@ const Screen = forwardRef(
|
|
|
132
132
|
inputSearchProps,
|
|
133
133
|
inputSearchRef,
|
|
134
134
|
}: ScreenProps,
|
|
135
|
-
ref
|
|
135
|
+
ref: any
|
|
136
136
|
) => {
|
|
137
137
|
const {theme} = useContext(ApplicationContext);
|
|
138
138
|
const insets = useSafeAreaInsets();
|
|
139
139
|
const heightHeader = useHeaderHeight();
|
|
140
140
|
const animatedValue = useRef<Animated.Value>(new Animated.Value(0));
|
|
141
|
-
const scrollViewRef = useRef<any>(ref);
|
|
142
141
|
const currentTint = useRef(Colors.black_01);
|
|
143
142
|
const isTab = navigation?.instance?.getState?.()?.type === 'tab';
|
|
144
143
|
|
|
@@ -345,7 +344,7 @@ const Screen = forwardRef(
|
|
|
345
344
|
useNativeDriver: true,
|
|
346
345
|
duration: 300,
|
|
347
346
|
}).start();
|
|
348
|
-
|
|
347
|
+
ref?.scrollTo({y: 0, animated: true});
|
|
349
348
|
}
|
|
350
349
|
scrollViewProps?.onScrollEndDrag?.(e);
|
|
351
350
|
};
|
|
@@ -433,7 +432,7 @@ const Screen = forwardRef(
|
|
|
433
432
|
|
|
434
433
|
<Component
|
|
435
434
|
{...scrollViewProps}
|
|
436
|
-
ref={
|
|
435
|
+
ref={ref}
|
|
437
436
|
showsVerticalScrollIndicator={false}
|
|
438
437
|
onScroll={handleScroll}
|
|
439
438
|
onScrollEndDrag={handleScrollEnd}
|
package/package.json
CHANGED
package/publish.sh
CHANGED
|
@@ -9,8 +9,8 @@ elif [ "$1" == "latest" ]; then
|
|
|
9
9
|
npm version prerelease --preid=rc
|
|
10
10
|
npm publish --tag latest --access=public
|
|
11
11
|
else
|
|
12
|
-
npm version $(npm view @momo-kits/foundation@beta version)
|
|
13
|
-
npm version prerelease --preid=beta
|
|
12
|
+
# npm version $(npm view @momo-kits/foundation@beta version)
|
|
13
|
+
# npm version prerelease --preid=beta
|
|
14
14
|
npm publish --tag beta --access=public
|
|
15
15
|
fi
|
|
16
16
|
|