@momo-kits/foundation 0.169.8 → 0.169.9

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 (39) hide show
  1. package/Application/BottomSheet.hooks.ts +27 -17
  2. package/Application/BottomSheet.tsx +35 -27
  3. package/Application/BottomTab/BottomTabBar.tsx +6 -6
  4. package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
  5. package/Application/BottomTab/TabScreen.tsx +11 -10
  6. package/Application/BottomTab/index.tsx +5 -5
  7. package/Application/BottomTab/usePressLift.ts +3 -3
  8. package/Application/Components/HeaderToolkitAction.tsx +26 -18
  9. package/Application/Components/useToolkitActions.tsx +22 -21
  10. package/Application/ModalScreen.tsx +19 -15
  11. package/Application/StackScreen.tsx +13 -9
  12. package/Application/modalLayout.ts +1 -3
  13. package/Context/index.ts +3 -1
  14. package/Image/index.tsx +6 -6
  15. package/Input/ErrorView.tsx +59 -0
  16. package/Input/FloatingView.tsx +101 -0
  17. package/Input/Input.tsx +40 -37
  18. package/Input/InputDropDown.tsx +13 -10
  19. package/Input/InputMoney.tsx +17 -17
  20. package/Input/InputMoneyView.tsx +12 -12
  21. package/Input/InputOTP.tsx +46 -34
  22. package/Input/InputPhoneNumber.tsx +48 -43
  23. package/Input/InputSearch.tsx +27 -24
  24. package/Input/InputTextArea.tsx +29 -23
  25. package/Input/RenderTrailing.tsx +79 -0
  26. package/Input/constants.ts +8 -0
  27. package/Input/utils.ts +126 -7
  28. package/Layout/FloatingButton.tsx +38 -24
  29. package/Layout/layoutMetrics.ts +43 -34
  30. package/Layout/screenTypes.ts +2 -3
  31. package/Pagination/PaginationScroll.tsx +5 -3
  32. package/Pagination/types.ts +3 -5
  33. package/Popup/PopupNotify.tsx +16 -13
  34. package/Popup/PopupPromotion.tsx +5 -4
  35. package/Popup/types.ts +2 -4
  36. package/Skeleton/index.tsx +15 -12
  37. package/package.json +1 -1
  38. package/Input/common.tsx +0 -257
  39. package/Input/moneyUtils.ts +0 -23
@@ -19,13 +19,16 @@ import {
19
19
  MiniAppContext,
20
20
  } from '../Context';
21
21
  import { Icon } from '../Icon';
22
+ import { Styles } from '../Consts';
22
23
  import { Text, useScaleSize, exportFontFamily } from '../Text';
24
+ import { CLEAR_ICON_SIZE, INPUT_FONT_SIZE, PRESSED_OPACITY } from './constants';
23
25
  import { InputRef, InputSearchProps } from './index';
24
26
  import { TextTyping } from './SearchTextTyping';
25
27
  import styles from './styles';
26
- import { Styles } from '../Consts';
27
28
  import SystemTextInput from './SystemTextInput';
28
29
 
30
+ const SEARCH_ICON_SIZE = 24;
31
+
29
32
  /**
30
33
  * Search field with search icon, clear icon, optional trailing icon and a
31
34
  * cancel button; with `typingData` it shows an animated placeholder instead.
@@ -55,15 +58,15 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
55
58
  ) => {
56
59
  const { theme } = useContext(ApplicationContext);
57
60
  const context = useContext<any>(MiniAppContext);
58
- const scaledFontSize = useScaleSize(14);
61
+ const scaledFontSize = useScaleSize(INPUT_FONT_SIZE);
59
62
  const searchFontFamily = exportFontFamily('regular');
60
63
 
61
64
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
62
65
 
63
- const [focused, setFocused] = useState(false);
66
+ const [isFocused, setIsFocused] = useState(false);
64
67
  const [internalText, setInternalText] = useState(defaultValue || '');
65
68
  const currentValue = value !== undefined ? value : internalText;
66
- const haveValue = !!currentValue;
69
+ const hasValue = !!currentValue;
67
70
  const inputRef = useRef<TextInput | null>(null);
68
71
 
69
72
  const componentName = 'InputSearch';
@@ -73,40 +76,40 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
73
76
  props.accessibilityLabel,
74
77
  );
75
78
 
76
- const _onFocus = (e: TextInputFocusEvent) => {
77
- setFocused(true);
79
+ const handleFocus = (e: TextInputFocusEvent) => {
80
+ setIsFocused(true);
78
81
  onFocus?.(e);
79
82
  };
80
83
 
81
- const _onBlur = (e: TextInputFocusEvent) => {
82
- setFocused(false);
84
+ const handleBlur = (e: TextInputFocusEvent) => {
85
+ setIsFocused(false);
83
86
  onBlur?.(e);
84
87
  };
85
88
 
86
- const onClearText = () => {
89
+ const handleClearText = () => {
87
90
  inputRef?.current?.clear();
88
91
  inputRef.current?.setNativeProps({ text: '' });
89
- _onChangeText('');
92
+ handleChangeText('');
90
93
  };
91
94
 
92
- const _onChangeText = (text: string) => {
95
+ const handleChangeText = (text: string) => {
93
96
  if (value === undefined) {
94
97
  setInternalText(text);
95
98
  }
96
99
  onChangeText?.(text);
97
100
  };
98
101
 
99
- const _setText = (text: string) => {
102
+ const handleSetText = (text: string) => {
100
103
  inputRef?.current?.setNativeProps({ text });
101
- _onChangeText(text);
104
+ handleChangeText(text);
102
105
  };
103
106
 
104
107
  useImperativeHandle(ref, () => {
105
108
  return {
106
- clear: onClearText,
109
+ clear: handleClearText,
107
110
  focus: () => inputRef.current?.focus(),
108
111
  blur: () => inputRef.current?.blur(),
109
- setText: _setText,
112
+ setText: handleSetText,
110
113
  };
111
114
  });
112
115
 
@@ -116,7 +119,7 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
116
119
  <TouchableOpacity
117
120
  style={[styles.searchInput, styles.searchInputTyping]}
118
121
  onPress={onPress}
119
- activeOpacity={onPress ? 0.6 : 1}
122
+ activeOpacity={onPress ? PRESSED_OPACITY : 1}
120
123
  >
121
124
  <TextTyping data={typingData} />
122
125
  </TouchableOpacity>
@@ -139,10 +142,10 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
139
142
  },
140
143
  ]}
141
144
  value={value}
142
- onChangeText={_onChangeText}
145
+ onChangeText={handleChangeText}
143
146
  defaultValue={defaultValue}
144
- onFocus={_onFocus}
145
- onBlur={_onBlur}
147
+ onFocus={handleFocus}
148
+ onBlur={handleBlur}
146
149
  placeholder={placeholder}
147
150
  selectionColor={theme.colors.primary}
148
151
  placeholderTextColor={theme.colors.text.hint}
@@ -153,15 +156,15 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
153
156
  const renderTrailing = () => {
154
157
  return (
155
158
  <View style={Styles.row}>
156
- {haveValue && (
159
+ {hasValue && (
157
160
  <TouchableOpacity
158
161
  style={styles.iconWrapper}
159
- onPress={onClearText}
162
+ onPress={handleClearText}
160
163
  accessibilityLabel={`${componentId}|close-icon-touch`}
161
164
  >
162
165
  <Icon
163
166
  source="24_navigation_close_circle_full"
164
- size={16}
167
+ size={CLEAR_ICON_SIZE}
165
168
  color={theme.colors.text.hint}
166
169
  />
167
170
  </TouchableOpacity>
@@ -192,7 +195,7 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
192
195
  };
193
196
 
194
197
  let inputState = 'empty';
195
- if (focused) {
198
+ if (isFocused) {
196
199
  inputState = 'focus';
197
200
  }
198
201
  if (value && value?.length > 0) {
@@ -225,7 +228,7 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
225
228
  >
226
229
  <Icon
227
230
  source={'navigation_search'}
228
- size={24}
231
+ size={SEARCH_ICON_SIZE}
229
232
  color={theme.colors.text.hint}
230
233
  />
231
234
  {renderInputView()}
@@ -12,14 +12,20 @@ import {
12
12
  TouchableOpacity,
13
13
  View,
14
14
  } from 'react-native';
15
+ import { setAutomationID, useComponentId } from '../Application';
15
16
  import { ApplicationContext, MiniAppContext } from '../Context';
16
17
  import { Icon } from '../Icon';
17
18
  import { Text, useScaleSize, exportFontFamily } from '../Text';
18
- import { ErrorView, FloatingView, getBorderColor, MAX_LENGTH } from './common';
19
+ import { CLEAR_ICON_SIZE, INPUT_FONT_SIZE } from './constants';
20
+ import { ErrorView } from './ErrorView';
21
+ import { FloatingView } from './FloatingView';
19
22
  import { InputTextAreaProps } from './index';
20
23
  import styles from './styles';
21
24
  import SystemTextInput from './SystemTextInput';
22
- import { setAutomationID, useComponentId } from '../Application';
25
+ import { getBorderColor } from './utils';
26
+
27
+ const DEFAULT_MAX_LENGTH = 300;
28
+ const DEFAULT_HEIGHT = 104;
23
29
 
24
30
  /**
25
31
  * Multiline input with floating label, clear icon and a `length/maxLength`
@@ -40,7 +46,7 @@ const InputTextArea = forwardRef(
40
46
  defaultValue,
41
47
  height,
42
48
  placeholder,
43
- maxLength = MAX_LENGTH,
49
+ maxLength = DEFAULT_MAX_LENGTH,
44
50
  required,
45
51
  errorSpacing,
46
52
  style,
@@ -57,12 +63,12 @@ const InputTextArea = forwardRef(
57
63
  );
58
64
  const { theme } = useContext(ApplicationContext);
59
65
  const context = useContext<any>(MiniAppContext);
60
- const DEFAULT_HEIGHT = useScaleSize(104);
66
+ const defaultHeight = useScaleSize(DEFAULT_HEIGHT);
61
67
 
62
68
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
63
- const scaledFontSize = useScaleSize(14);
69
+ const scaledFontSize = useScaleSize(INPUT_FONT_SIZE);
64
70
  const inputFontFamily = exportFontFamily('regular');
65
- const [focused, setFocused] = useState(false);
71
+ const [isFocused, setIsFocused] = useState(false);
66
72
  const [inputValue, setInputValue] = useState(defaultValue ?? '');
67
73
  const inputRef = useRef<TextInput | null>(null);
68
74
 
@@ -70,33 +76,33 @@ const InputTextArea = forwardRef(
70
76
  setInputValue(value ?? '');
71
77
  }, [value]);
72
78
 
73
- const onClearText = () => {
79
+ const handleClearText = () => {
74
80
  inputRef?.current?.clear();
75
81
  inputRef.current?.setNativeProps({ text: '' });
76
- _onChangeText('');
82
+ handleChangeText('');
77
83
  };
78
84
 
79
- const _onChangeText = (text: string) => {
85
+ const handleChangeText = (text: string) => {
80
86
  onChangeText?.(text);
81
87
  setInputValue(text);
82
88
  };
83
89
 
84
- const _onFocus = (e: TextInputFocusEvent) => {
85
- setFocused(true);
90
+ const handleFocus = (e: TextInputFocusEvent) => {
91
+ setIsFocused(true);
86
92
  onFocus?.(e);
87
93
  };
88
94
 
89
- const _onBlur = (e: TextInputFocusEvent) => {
90
- setFocused(false);
95
+ const handleBlur = (e: TextInputFocusEvent) => {
96
+ setIsFocused(false);
91
97
  onBlur?.(e);
92
98
  };
93
99
 
94
100
  useImperativeHandle(ref, () => {
95
101
  return {
96
- clear: onClearText,
102
+ clear: handleClearText,
97
103
  focus: () => inputRef.current?.focus(),
98
104
  blur: () => inputRef.current?.blur(),
99
- setText: (text: string) => _onChangeText(text),
105
+ setText: (text: string) => handleChangeText(text),
100
106
  };
101
107
  });
102
108
 
@@ -129,10 +135,10 @@ const InputTextArea = forwardRef(
129
135
  style={[
130
136
  styles.textAreaWrapper,
131
137
  {
132
- height: height || DEFAULT_HEIGHT,
138
+ height: height || defaultHeight,
133
139
  backgroundColor: theme.colors.background.surface,
134
140
  },
135
- getBorderColor(theme, focused, errorMessage, disabled),
141
+ getBorderColor(theme, isFocused, errorMessage, disabled),
136
142
  ]}
137
143
  >
138
144
  <FloatingView
@@ -175,23 +181,23 @@ const InputTextArea = forwardRef(
175
181
  textBreakStrategy="highQuality"
176
182
  multiline={true}
177
183
  value={inputValue}
178
- onChangeText={_onChangeText}
179
- onFocus={_onFocus}
180
- onBlur={_onBlur}
184
+ onChangeText={handleChangeText}
185
+ onFocus={handleFocus}
186
+ onBlur={handleBlur}
181
187
  placeholder={placeholder}
182
188
  selectionColor={theme.colors.primary}
183
189
  placeholderTextColor={placeholderColor}
184
190
  />
185
191
  </View>
186
- {focused && inputValue.length > 0 && (
192
+ {isFocused && inputValue.length > 0 && (
187
193
  <TouchableOpacity
188
194
  style={styles.iconWrapper}
189
- onPress={onClearText}
195
+ onPress={handleClearText}
190
196
  accessibilityLabel={`${componentId}|clear-icon-touch`}
191
197
  >
192
198
  <Icon
193
199
  source="24_navigation_close_circle_full"
194
- size={16}
200
+ size={CLEAR_ICON_SIZE}
195
201
  color={theme.colors.text.hint}
196
202
  accessibilityLabel={`${componentId}|clear-icon`}
197
203
  />
@@ -0,0 +1,79 @@
1
+ import React, { type FC, type ReactNode, useContext } from 'react';
2
+ import { TouchableOpacity } from 'react-native';
3
+ import IconSources from '../Assets/icon.json';
4
+ import { ApplicationContext } from '../Context';
5
+ import { Icon } from '../Icon';
6
+ import { Loader } from '../Loader';
7
+ import { Text } from '../Text';
8
+ import styles from './styles';
9
+
10
+ const ICON_SIZE = 24;
11
+ const TEXT_MAX_LENGTH = 15;
12
+
13
+ type IconSourceMap = Record<string, { uri?: string } | undefined>;
14
+
15
+ type TrailingProps = {
16
+ loading?: boolean;
17
+ color?: string;
18
+ icon?: string;
19
+ componentId?: string;
20
+ onPressIcon?: () => void;
21
+ };
22
+
23
+ /**
24
+ * Trailing slot: spinner while `loading`, an icon when `icon` is a known icon
25
+ * name or URL, otherwise `icon` as a text action (first 15 chars).
26
+ */
27
+ const RenderTrailing: FC<TrailingProps> = ({
28
+ loading,
29
+ color,
30
+ icon,
31
+ onPressIcon,
32
+ componentId,
33
+ }) => {
34
+ const { theme } = useContext(ApplicationContext);
35
+ if (loading) {
36
+ return <Loader type={'spinner'} color={color} style={styles.icon} />;
37
+ }
38
+ if (!icon) {
39
+ return null;
40
+ }
41
+
42
+ const iconSources: IconSourceMap = IconSources;
43
+ const isIconName = !!iconSources[icon]?.uri;
44
+ const isIcon = isIconName || icon.includes('http');
45
+ const renderIconTouchable = (content: ReactNode) => {
46
+ return (
47
+ <TouchableOpacity
48
+ accessibilityLabel={`${componentId}|trailing-touch`}
49
+ onPress={onPressIcon}
50
+ style={styles.icon}
51
+ >
52
+ {content}
53
+ </TouchableOpacity>
54
+ );
55
+ };
56
+
57
+ if (isIcon) {
58
+ return renderIconTouchable(
59
+ <Icon
60
+ color={color}
61
+ source={icon}
62
+ size={ICON_SIZE}
63
+ accessibilityLabel={`${componentId}|trailing-icon`}
64
+ />,
65
+ );
66
+ }
67
+ return renderIconTouchable(
68
+ <Text
69
+ typography="action_xs_bold"
70
+ color={color ?? theme.colors.primary}
71
+ numberOfLines={1}
72
+ accessibilityLabel={`${componentId}|trailing-text`}
73
+ >
74
+ {icon.substring(0, TEXT_MAX_LENGTH)}
75
+ </Text>,
76
+ );
77
+ };
78
+
79
+ export { RenderTrailing };
@@ -0,0 +1,8 @@
1
+ /** Input text size before font scaling. */
2
+ const INPUT_FONT_SIZE = 14;
3
+ /** Size of the clear (x) icon shown while a focused field has a value. */
4
+ const CLEAR_ICON_SIZE = 16;
5
+ /** Pressed opacity of the floating icon, dropdown and typing placeholder. */
6
+ const PRESSED_OPACITY = 0.6;
7
+
8
+ export { INPUT_FONT_SIZE, CLEAR_ICON_SIZE, PRESSED_OPACITY };
package/Input/utils.ts CHANGED
@@ -1,4 +1,110 @@
1
- import { Localize } from '../Application';
1
+ import { type ViewStyle } from 'react-native';
2
+ import Localize from '../Application/Localize';
3
+ import { type Theme } from '../Application/types';
4
+ import styles from './styles';
5
+
6
+ const SMALL_HEIGHT = 48;
7
+ const LARGE_HEIGHT = 56;
8
+ const SMALL_LEADING_ICON_SIZE = 24;
9
+ const LARGE_LEADING_ICON_SIZE = 32;
10
+ const THOUSANDS_GROUP_SIZE = 3;
11
+ const PHONE_DIGITS_WITHOUT_ZERO = 9;
12
+ const MIN_PHONE_DIGITS = 10;
13
+ const MAX_PHONE_DIGITS = 11;
14
+
15
+ /** vi-VN number format (`.` thousands) for the InputMoney display value. */
16
+ const moneyFormatter = new Intl.NumberFormat('vi-VN');
17
+
18
+ /** Field height for `size` before font scaling; unset means large. */
19
+ const getInputHeight = (size?: 'small' | 'large') => {
20
+ if (size === 'small') {
21
+ return SMALL_HEIGHT;
22
+ }
23
+ return LARGE_HEIGHT;
24
+ };
25
+
26
+ /** Leading icon size for `size`; unset means large. */
27
+ const getLeadingIconSize = (size?: 'small' | 'large') => {
28
+ if (size === 'small') {
29
+ return SMALL_LEADING_ICON_SIZE;
30
+ }
31
+ return LARGE_LEADING_ICON_SIZE;
32
+ };
33
+
34
+ /** Border color by state; priority is disabled > error > focused > default. */
35
+ const getBorderColor = (
36
+ theme: Theme,
37
+ isFocused: boolean,
38
+ errorMessage?: string,
39
+ isDisabled?: boolean,
40
+ ) => {
41
+ let borderColor = theme.colors.border.default;
42
+
43
+ if (isFocused) {
44
+ borderColor = theme.colors.primary;
45
+ }
46
+
47
+ if (errorMessage) {
48
+ borderColor = theme.colors.error.primary;
49
+ }
50
+
51
+ if (isDisabled) {
52
+ borderColor = theme.colors.border.disable;
53
+ }
54
+
55
+ return { borderColor };
56
+ };
57
+
58
+ /**
59
+ * Container style for `size`; multiline uses `height` as minHeight so the
60
+ * field can grow with its content.
61
+ */
62
+ const getSizeStyle = (
63
+ size?: 'small' | 'large',
64
+ isMultiline: boolean = false,
65
+ height?: number,
66
+ ) => {
67
+ let scaleStyle: ViewStyle = {};
68
+ if (height) {
69
+ scaleStyle = { height };
70
+ }
71
+
72
+ if (isMultiline) {
73
+ return [
74
+ styles.multilineContainer,
75
+ {
76
+ minHeight: height,
77
+ },
78
+ ];
79
+ }
80
+
81
+ if (size === 'small') {
82
+ return [styles.smallContainer, scaleStyle];
83
+ }
84
+ return [styles.container, scaleStyle];
85
+ };
86
+
87
+ /**
88
+ * Locates the single edited span between `old` and `text`: common prefix
89
+ * length (`start`), common suffix length (`suffix`) and removed + inserted
90
+ * char count (`size`).
91
+ */
92
+ const findEdit = (old: string, text: string) => {
93
+ let start = 0;
94
+ while (start < text.length && old[start] === text[start]) {
95
+ start += 1;
96
+ }
97
+ const maxSuffix = Math.min(text.length, old.length) - start;
98
+ let suffix = 0;
99
+ while (suffix < maxSuffix) {
100
+ if (old[old.length - 1 - suffix] !== text[text.length - 1 - suffix]) {
101
+ break;
102
+ }
103
+ suffix += 1;
104
+ }
105
+ const size = old.length + text.length - 2 * (start + suffix);
106
+ return { old, start, suffix, size };
107
+ };
2
108
 
3
109
  /**
4
110
  * Formats a number Vietnamese-style (`.` thousands, `,` decimals) with
@@ -18,7 +124,7 @@ const formatNumberToMoney = (number: number, currency = '') => {
18
124
  }
19
125
 
20
126
  const numberString = Math.abs(number).toString();
21
- if (numberString.length < 3) {
127
+ if (numberString.length < THOUSANDS_GROUP_SIZE) {
22
128
  const sign = isNegative ? '-' : '';
23
129
  return `${sign}${numberString}${currency}`;
24
130
  }
@@ -32,7 +138,7 @@ const formatNumberToMoney = (number: number, currency = '') => {
32
138
  } else {
33
139
  array.push(numberString[i] as string);
34
140
  }
35
- if (count === 3 && i >= 1) {
141
+ if (count === THOUSANDS_GROUP_SIZE && i >= 1) {
36
142
  array.push('.');
37
143
  count = 0;
38
144
  }
@@ -67,6 +173,7 @@ const formatMoneyToNumber = (money: string, currencyUnit: string) => {
67
173
  return Number(money);
68
174
  };
69
175
 
176
+ /** Digits of `phone`, every other char stripped. */
70
177
  function formatPhoneNumber(phone: string): string {
71
178
  return phone.replace(/\D/g, '');
72
179
  }
@@ -79,20 +186,32 @@ function checkValidPhoneNumber(phone: string): {
79
186
  phoneFormatted: string;
80
187
  error?: string;
81
188
  } {
82
- const isMissingLeadingZero = phone.length === 9 && /^[1-9]/.test(phone);
189
+ const hasLengthWithoutZero = phone.length === PHONE_DIGITS_WITHOUT_ZERO;
190
+ const isMissingLeadingZero = hasLengthWithoutZero && /^[1-9]/.test(phone);
83
191
  const phoneNumber = isMissingLeadingZero ? `0${phone}` : phone;
84
192
 
85
193
  const phoneFormatted = formatPhoneNumber(phoneNumber);
194
+ const digitCount = phoneFormatted.length;
86
195
  const localize = new Localize({ vi: {}, en: {} });
87
196
  let error: string | undefined;
88
197
 
89
- if (phoneFormatted.length === 0) {
198
+ if (digitCount === 0) {
90
199
  error = localize.translate('enterPhoneNumber');
91
- } else if (phoneFormatted.length < 10 || phoneFormatted.length > 11) {
200
+ } else if (digitCount < MIN_PHONE_DIGITS || digitCount > MAX_PHONE_DIGITS) {
92
201
  error = localize.translate('invalidPhoneNumber');
93
202
  }
94
203
 
95
204
  return { phoneFormatted: phoneFormatted, error };
96
205
  }
97
206
 
98
- export { formatMoneyToNumber, formatNumberToMoney, checkValidPhoneNumber };
207
+ export {
208
+ moneyFormatter,
209
+ getInputHeight,
210
+ getLeadingIconSize,
211
+ getBorderColor,
212
+ getSizeStyle,
213
+ findEdit,
214
+ formatMoneyToNumber,
215
+ formatNumberToMoney,
216
+ checkValidPhoneNumber,
217
+ };
@@ -13,12 +13,22 @@ import Animated, {
13
13
  } from 'react-native-reanimated';
14
14
  import { scheduleOnRN } from 'react-native-worklets';
15
15
  import { ApplicationContext } from '../Context';
16
- import { Colors } from '../Consts';
16
+ import { Colors, Spacing } from '../Consts';
17
17
  import { Icon } from '../Icon';
18
18
  import { useScaleSize, exportFontFamily } from '../Text';
19
19
  import { type AnimatedCompatValue } from '../Application/utils';
20
20
  import { useNormalizedSharedValue } from '../Application/hooks';
21
21
 
22
+ const DEFAULT_BOTTOM = Spacing.M;
23
+ const PILL_RADIUS = 100;
24
+ const LABEL_FONT_SIZE = 16;
25
+ const LABEL_LINE_HEIGHT = 22;
26
+ const SMALL_SIZE = 36;
27
+ const LARGE_SIZE = 48;
28
+ const SMALL_ICON_SIZE = 12;
29
+ const LARGE_ICON_SIZE = 24;
30
+ const ANIMATION_DURATION = 100;
31
+
22
32
  export interface FloatingButtonProps {
23
33
  label?: string;
24
34
  onPress?: () => void;
@@ -44,60 +54,64 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
44
54
  iconColor = '',
45
55
  size = 'small',
46
56
  animatedValue,
47
- bottom = 12,
57
+ bottom = DEFAULT_BOTTOM,
48
58
  }: FloatingButtonProps) => {
49
59
  const { theme } = useContext(ApplicationContext);
50
- const enabled = animatedValue != null;
60
+ const hasScrollValue = animatedValue != null;
51
61
  const sharedValue = useNormalizedSharedValue(animatedValue);
52
- const scaledFontSize = useScaleSize(16);
53
- const scaledLineHeight = useScaleSize(22);
62
+ const scaledFontSize = useScaleSize(LABEL_FONT_SIZE);
63
+ const scaledLineHeight = useScaleSize(LABEL_LINE_HEIGHT);
54
64
  const labelFontFamily = exportFontFamily('bold');
55
65
  const maxWidth = useSharedValue(0);
56
- const minWidth = size === 'small' ? 36 : 48;
66
+ const minWidth = size === 'small' ? SMALL_SIZE : LARGE_SIZE;
57
67
  const opacityAnimated = useSharedValue(0);
58
68
  const widthAnimated = useSharedValue<number | null>(null);
59
69
  const lastOffset = useSharedValue(0);
60
70
  const lastDirection = useSharedValue<string | null>(null);
61
- const [showText, setShowText] = useState(true);
71
+ const [isLabelShown, setIsLabelShown] = useState(true);
62
72
 
63
73
  useEffect(() => {
64
74
  if (!label) return;
65
- opacityAnimated.value = withTiming(1, { duration: 100 });
75
+ opacityAnimated.value = withTiming(1, { duration: ANIMATION_DURATION });
66
76
  }, [label, opacityAnimated]);
67
77
 
68
78
  useAnimatedReaction(
69
79
  () => sharedValue.value,
70
80
  value => {
71
81
  'worklet';
72
- if (!label || !enabled) return;
82
+ if (!label || !hasScrollValue) return;
73
83
  if (value !== lastOffset.value && value > 0) {
74
84
  const direction = value > lastOffset.value ? 'down' : 'up';
75
85
  lastOffset.value = value;
76
86
  if (lastDirection.value !== direction) {
77
87
  lastDirection.value = direction;
78
88
  if (direction === 'down') {
79
- opacityAnimated.value = withTiming(0, { duration: 100 });
89
+ opacityAnimated.value = withTiming(0, {
90
+ duration: ANIMATION_DURATION,
91
+ });
80
92
  widthAnimated.value = withTiming(
81
93
  minWidth,
82
- { duration: 100 },
94
+ { duration: ANIMATION_DURATION },
83
95
  finished => {
84
- if (finished) scheduleOnRN(setShowText, false);
96
+ if (finished) scheduleOnRN(setIsLabelShown, false);
85
97
  },
86
98
  );
87
99
  } else {
88
- opacityAnimated.value = withTiming(1, { duration: 100 });
100
+ opacityAnimated.value = withTiming(1, {
101
+ duration: ANIMATION_DURATION,
102
+ });
89
103
  widthAnimated.value = withTiming(
90
104
  maxWidth.value,
91
- { duration: 100 },
105
+ { duration: ANIMATION_DURATION },
92
106
  finished => {
93
- if (finished) scheduleOnRN(setShowText, true);
107
+ if (finished) scheduleOnRN(setIsLabelShown, true);
94
108
  },
95
109
  );
96
110
  }
97
111
  }
98
112
  }
99
113
  },
100
- [label, enabled, minWidth],
114
+ [label, hasScrollValue, minWidth],
101
115
  );
102
116
 
103
117
  const containerStyle = useAnimatedStyle(() => ({
@@ -146,10 +160,10 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
146
160
  <TouchableOpacity style={styles.content} onPress={onPress}>
147
161
  <Icon
148
162
  source={icon}
149
- size={size === 'small' ? 12 : 24}
163
+ size={size === 'small' ? SMALL_ICON_SIZE : LARGE_ICON_SIZE}
150
164
  color={iconColor}
151
165
  />
152
- {showText && !!label && (
166
+ {isLabelShown && !!label && (
153
167
  <Animated.Text
154
168
  allowFontScaling={false}
155
169
  style={[
@@ -178,19 +192,19 @@ const styles = StyleSheet.create({
178
192
  alignContent: 'center',
179
193
  alignItems: 'center',
180
194
  position: 'absolute',
181
- borderRadius: 100,
182
- paddingHorizontal: 12,
195
+ borderRadius: PILL_RADIUS,
196
+ paddingHorizontal: Spacing.M,
183
197
  flexWrap: 'wrap',
184
198
  alignSelf: 'flex-end',
185
199
  },
186
200
  label: {
187
- marginLeft: 8,
201
+ marginLeft: Spacing.S,
188
202
  },
189
- alignRight: { right: 12 },
203
+ alignRight: { right: Spacing.M },
190
204
  alignCenter: { alignSelf: 'center' },
191
205
  alignEnd: { alignSelf: 'flex-end' },
192
- small: { height: 36 },
193
- large: { height: 48 },
206
+ small: { height: SMALL_SIZE },
207
+ large: { height: LARGE_SIZE },
194
208
  content: {
195
209
  flexDirection: 'row',
196
210
  alignItems: 'center',