@momo-kits/foundation 0.151.1-beta.1 → 0.151.1-beta.2

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.
@@ -49,6 +49,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
49
49
  keyboardVerticalOffset,
50
50
  useDivider = true,
51
51
  footerComponent,
52
+ leftOptions,
52
53
  }: BottomSheetParams = props.route.params;
53
54
 
54
55
  const translateY = useSharedValue(heightDevice);
@@ -167,11 +168,23 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
167
168
  },
168
169
  ]}
169
170
  >
171
+ {leftOptions?.onPressIconLeft ? (
172
+ <TouchableOpacity
173
+ style={styles.iconButton}
174
+ onPress={() => {
175
+ onDismiss(true, leftOptions.onPressIconLeft);
176
+ }}
177
+ >
178
+ <Icon source={leftOptions.iconLeft ?? 'ic_back'} />
179
+ </TouchableOpacity>
180
+ ) : (
181
+ <View style={styles.iconButton} />
182
+ )}
183
+
170
184
  {options.header ? (
171
185
  <View style={Styles.flex}>{options.header}</View>
172
186
  ) : (
173
187
  <>
174
- <View style={styles.iconButton} />
175
188
  <Text
176
189
  style={[Styles.flex, { textAlign: 'center' }]}
177
190
  numberOfLines={1}
@@ -195,6 +208,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
195
208
  );
196
209
  }, [
197
210
  onDismiss,
211
+ leftOptions,
198
212
  options.header,
199
213
  options.title,
200
214
  panResponder.panHandlers,
@@ -167,6 +167,10 @@ export type BottomSheetParams = {
167
167
  keyboardVerticalOffset?: number;
168
168
  useDivider?: boolean;
169
169
  footerComponent?: React.ReactNode;
170
+ leftOptions?: {
171
+ iconLeft?: string;
172
+ onPressIconLeft?: () => void;
173
+ };
170
174
  };
171
175
 
172
176
  export interface NavigationButtonProps extends TouchableOpacityProps {
@@ -98,7 +98,9 @@
98
98
  "tutorial": "Hướng dẫn sử dụng",
99
99
  "question": "Câu hỏi thường gặp",
100
100
  "support": "Trung tâm hỗ trợ",
101
- "skip": "Bỏ qua"
101
+ "skip": "Bỏ qua",
102
+ "enterPhoneNumber": "Vui lòng nhập số điện thoại",
103
+ "invalidPhoneNumber": "Số điện thoại không đúng"
102
104
  },
103
105
  "en": {
104
106
  "seeMore": "See more",
@@ -199,6 +201,8 @@
199
201
  "question": "FAQ",
200
202
  "support": "Support center",
201
203
  "errorCode": "Error code: ",
202
- "skip": "Skip"
204
+ "skip": "Skip",
205
+ "enterPhoneNumber": "Please enter your phone number",
206
+ "invalidPhoneNumber": "Invalid phone number"
203
207
  }
204
208
  }
@@ -80,7 +80,7 @@ const CheckBox: FC<CheckBoxProps> = ({
80
80
  />
81
81
  )}
82
82
  </View>
83
- {!!label && <Text typography={'body_default_regular'}>{label}</Text>}
83
+ {!!label && <Text typography={'body_default_regular'} style={styles.label}>{label}</Text>}
84
84
  </TouchableOpacity>
85
85
  </ComponentContext.Provider>
86
86
  );
@@ -12,4 +12,5 @@ export default StyleSheet.create({
12
12
  alignItems: 'center',
13
13
  },
14
14
  container: {flexDirection: 'row', alignItems: 'center'},
15
+ label: {flexShrink: 1}
15
16
  });
@@ -0,0 +1,274 @@
1
+ import React, {
2
+ forwardRef,
3
+ useContext,
4
+ useImperativeHandle,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
8
+ import {
9
+ NativeSyntheticEvent,
10
+ StyleSheet,
11
+ TextInput,
12
+ TextInputFocusEventData,
13
+ TouchableOpacity,
14
+ View,
15
+ } from 'react-native';
16
+ import {
17
+ ApplicationContext,
18
+ ComponentContext,
19
+ useComponentId,
20
+ } from '../Application';
21
+ import { Colors, Spacing, Styles } from '../Consts';
22
+ import { Icon } from '../Icon';
23
+ import { scaleSize, Text } from '../Text';
24
+ import {
25
+ ErrorView,
26
+ getBorderColor,
27
+ getSizeStyle,
28
+ RenderTrailing,
29
+ } from './common';
30
+ import { InputPhoneNumberProps } from './index';
31
+ import styles from './styles';
32
+ import SystemTextInput from './SystemTextInput';
33
+ import { checkTyping } from './utils';
34
+ import { Image } from '../Image';
35
+ import { Typography } from '../Text/types';
36
+
37
+ /**
38
+ * Input default component
39
+ */
40
+ const InputPhoneNumber = forwardRef(
41
+ (
42
+ {
43
+ value,
44
+ onChangeText,
45
+ size = 'small',
46
+ placeholder = '0123456789',
47
+ onBlur,
48
+ onFocus,
49
+ errorMessage,
50
+ trailing,
51
+ trailingColor,
52
+ onPressTrailing,
53
+ disabled = false,
54
+ errorSpacing,
55
+ loading = false,
56
+ secureTextEntry,
57
+ keyboardType,
58
+ style,
59
+ params,
60
+ hintText,
61
+ editable = true,
62
+ showClearIcon = true,
63
+ ...props
64
+ }: InputPhoneNumberProps,
65
+ ref,
66
+ ) => {
67
+ const { theme } = useContext(ApplicationContext);
68
+ const [focused, setFocused] = useState(false);
69
+ const [haveValue, setHaveValue] = useState(!!value || !!props.defaultValue);
70
+ const inputRef = useRef<TextInput | null>(null);
71
+ const componentName = 'InputPhoneNumber';
72
+
73
+ const { componentId } = useComponentId(
74
+ `${componentName}/${placeholder}`,
75
+ props.accessibilityLabel,
76
+ );
77
+
78
+ const onClearText = () => {
79
+ inputRef?.current?.clear();
80
+ _onChangeText('');
81
+ };
82
+
83
+ const _onChangeText = (text: string) => {
84
+ checkTyping(text, haveValue, setHaveValue);
85
+ onChangeText?.(text);
86
+ };
87
+
88
+ const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
89
+ setFocused(true);
90
+ onFocus?.(e);
91
+ };
92
+
93
+ const _onBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
94
+ setFocused(false);
95
+ onBlur?.(e);
96
+ };
97
+
98
+ const _setText = (text: string) => {
99
+ inputRef?.current?.setNativeProps({ text });
100
+ _onChangeText(text);
101
+ };
102
+
103
+ useImperativeHandle(ref, () => {
104
+ return {
105
+ clear: onClearText,
106
+ focus: () => inputRef.current?.focus(),
107
+ blur: () => inputRef.current?.blur(),
108
+ setText: _setText,
109
+ };
110
+ });
111
+
112
+ let inputTextStyles = {};
113
+ let dividerHeight = 24;
114
+ let typography: Typography;
115
+ if (size === 'small') {
116
+ inputTextStyles = { fontSize: scaleSize(14), fontWeight: '600' };
117
+ dividerHeight = 24;
118
+ typography = 'header_s_semibold';
119
+ } else {
120
+ inputTextStyles = { fontSize: scaleSize(18), fontWeight: '700' };
121
+ dividerHeight = 32;
122
+ typography = 'header_m_bold';
123
+ }
124
+
125
+ /**
126
+ * Render input view
127
+ */
128
+ const renderInputView = () => {
129
+ const disabledColor = theme.colors.text.disable;
130
+ const secure = secureTextEntry;
131
+ let textColor = theme.colors.text.default;
132
+ let placeholderColor = theme.colors.text.hint;
133
+ let iconTintColor = trailingColor ?? theme.colors.text.default;
134
+ const borderWidth = focused ? 1.5 : 1;
135
+
136
+ if (disabled) {
137
+ textColor = disabledColor;
138
+ placeholderColor = disabledColor;
139
+ iconTintColor = disabledColor;
140
+ }
141
+
142
+ return (
143
+ <View
144
+ style={[
145
+ styles.inputWrapper,
146
+ { backgroundColor: theme.colors.background.surface },
147
+ getSizeStyle(size),
148
+ { borderWidth },
149
+ getBorderColor(theme, focused, errorMessage, disabled),
150
+ ]}
151
+ >
152
+ <View style={styles.inputView}>
153
+ <View style={[styles.leadingIconContainer, Styles.row]}>
154
+ <Image
155
+ source={{
156
+ uri: 'https://static.momocdn.net/app/img/icon/ic-qrcode-package/ic_vn_flag.png',
157
+ }}
158
+ style={innerStyle.iconFlag}
159
+ resizeMode="contain"
160
+ accessibilityLabel={`${componentId}|leading-icon`}
161
+ />
162
+ <View style={{ width: Spacing.XS }} />
163
+ <Text typography={typography} color={textColor}>
164
+ {'+84'}
165
+ </Text>
166
+ <View
167
+ style={[
168
+ styles.phoneNumberDivider,
169
+ {
170
+ height: dividerHeight,
171
+ backgroundColor: Colors.black_04,
172
+ },
173
+ ]}
174
+ />
175
+ </View>
176
+ <SystemTextInput
177
+ {...props}
178
+ accessibilityLabel={`${componentId}`}
179
+ editable={editable && !disabled}
180
+ secureTextEntry={secure}
181
+ textAlignVertical="center"
182
+ ref={inputRef}
183
+ accessibilityState={{
184
+ disabled,
185
+ ...props.accessibilityState,
186
+ }}
187
+ style={[
188
+ styles.input,
189
+ {
190
+ color: textColor,
191
+ },
192
+ inputTextStyles,
193
+ secure && haveValue && { fontSize: size === 'small' ? 18 : 22 },
194
+ ]}
195
+ allowFontScaling={false}
196
+ textBreakStrategy="highQuality"
197
+ value={value}
198
+ onChangeText={_onChangeText}
199
+ onFocus={_onFocus}
200
+ onBlur={_onBlur}
201
+ placeholder={placeholder}
202
+ selectionColor={theme.colors.primary}
203
+ placeholderTextColor={placeholderColor}
204
+ keyboardType={keyboardType ?? 'number-pad'}
205
+ />
206
+ </View>
207
+ <View style={styles.iconView}>
208
+ {showClearIcon && focused && haveValue && (
209
+ <TouchableOpacity
210
+ accessibilityLabel={`${componentId}|clear-icon-touch`}
211
+ accessible={false}
212
+ style={styles.iconWrapper}
213
+ onPress={onClearText}
214
+ >
215
+ <Icon
216
+ source="24_navigation_close_circle_full"
217
+ size={16}
218
+ color={theme.colors.text.hint}
219
+ accessibilityLabel={`${componentId}|clear-icon`}
220
+ />
221
+ </TouchableOpacity>
222
+ )}
223
+ <RenderTrailing
224
+ color={iconTintColor}
225
+ icon={trailing}
226
+ onPressIcon={onPressTrailing}
227
+ loading={loading}
228
+ componentId={componentId}
229
+ />
230
+ </View>
231
+ </View>
232
+ );
233
+ };
234
+
235
+ let inputState = 'active';
236
+ if (value && value?.length > 0) {
237
+ inputState = 'filled';
238
+ }
239
+ if (errorMessage && errorMessage?.length > 0) {
240
+ inputState = 'error';
241
+ }
242
+
243
+ return (
244
+ <ComponentContext.Provider
245
+ value={{
246
+ componentName,
247
+ componentId,
248
+ params,
249
+ state: inputState,
250
+ }}
251
+ >
252
+ <View style={[style, styles.wrapper]}>
253
+ {renderInputView()}
254
+ <ErrorView
255
+ errorMessage={errorMessage}
256
+ errorSpacing={errorSpacing}
257
+ hintText={hintText}
258
+ componentId={componentId}
259
+ />
260
+ </View>
261
+ </ComponentContext.Provider>
262
+ );
263
+ },
264
+ );
265
+
266
+ const innerStyle = StyleSheet.create({
267
+ iconFlag: {
268
+ width: 24,
269
+ height: 24,
270
+ marginHorizontal: 2,
271
+ },
272
+ });
273
+
274
+ export default InputPhoneNumber;
package/Input/index.tsx CHANGED
@@ -9,6 +9,8 @@ import InputMoney from './InputMoney';
9
9
  import InputOTP from './InputOTP';
10
10
  import InputSearch from './InputSearch';
11
11
  import InputTextArea from './InputTextArea';
12
+ import InputPhoneNumber from './InputPhoneNumber';
13
+ import { checkValidPhoneNumber } from './utils';
12
14
 
13
15
  export type OTPInputLength = 2 | 4 | 6 | 8 | 10;
14
16
 
@@ -343,6 +345,14 @@ export interface InputDropDownProps extends TouchableOpacityProps {
343
345
  onPressFloatingIcon?: () => void;
344
346
  }
345
347
 
348
+ export interface InputPhoneNumberProps extends InputProps {
349
+ /**
350
+ * Optional. Represents the country code to be displayed in the InputPhoneNumber component.
351
+ * It should be in the format of a string, e.g., "+1" for the United States.
352
+ */
353
+ countryCode?: string;
354
+ }
355
+
346
356
  export type InputRef = {
347
357
  clear: () => void;
348
358
  focus: () => void | undefined;
@@ -350,4 +360,14 @@ export type InputRef = {
350
360
  setText: (text: string) => void;
351
361
  };
352
362
 
353
- export {Input, InputDropDown, InputMoney, InputOTP, InputSearch, InputTextArea};
363
+ export {
364
+ Input,
365
+ InputDropDown,
366
+ InputMoney,
367
+ InputOTP,
368
+ InputSearch,
369
+ InputTextArea,
370
+ InputPhoneNumber,
371
+ };
372
+
373
+ export {checkValidPhoneNumber};
package/Input/styles.ts CHANGED
@@ -218,4 +218,8 @@ export default StyleSheet.create({
218
218
  overflow: 'hidden',
219
219
  },
220
220
  currency: { fontSize: scaleSize(20), fontWeight: 'bold' },
221
+ phoneNumberDivider: {
222
+ width: 1,
223
+ marginLeft: Spacing.M
224
+ },
221
225
  });
package/Input/utils.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { Localize } from '../Application';
2
+
1
3
  const formatNumberToMoney = (number: number, currency = '') => {
2
4
  if (!number || isNaN(number) || Number(number) === 0) {
3
5
  return `0${currency}`;
@@ -64,4 +66,43 @@ const checkTyping = (text: string, value: boolean, setValue: any) => {
64
66
  }
65
67
  };
66
68
 
67
- export { formatMoneyToNumber, formatNumberToMoney, checkTyping };
69
+ // --- PHONE VALIDATION ---
70
+ function formatPhoneNumber(phone: string): string {
71
+ return phone.replace(/\D/g, '');
72
+ }
73
+
74
+ function checkValidPhoneNumber(phone: string): {
75
+ phoneFormatted: string;
76
+ error?: string;
77
+ } {
78
+ let phoneNumber = phone;
79
+
80
+ // if not starting with 0 and has 9 digits => prepend 0
81
+ if (
82
+ phoneNumber.length > 0 &&
83
+ phoneNumber[0] !== '0' &&
84
+ /^\d$/.test(phoneNumber[0]) &&
85
+ phoneNumber.length === 9
86
+ ) {
87
+ phoneNumber = '0' + phoneNumber;
88
+ }
89
+
90
+ const phoneFormatted = formatPhoneNumber(phoneNumber);
91
+ const localize = new Localize({ vi: {}, en: {} });
92
+ let error: string | undefined;
93
+
94
+ if (phoneFormatted.length === 0) {
95
+ error = localize.translate('enterPhoneNumber');
96
+ } else if (phoneFormatted.length < 10 || phoneFormatted.length > 11) {
97
+ error = localize.translate('invalidPhoneNumber');
98
+ }
99
+
100
+ return { phoneFormatted: phoneFormatted, error };
101
+ }
102
+
103
+ export {
104
+ formatMoneyToNumber,
105
+ formatNumberToMoney,
106
+ checkTyping,
107
+ checkValidPhoneNumber,
108
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.151.1-beta.1",
3
+ "version": "0.151.1-beta.2",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},