@controleonline/ui-common 1.2.86 → 1.2.88

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 (73) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +7 -156
  3. package/.github/workflows/publish-npm.yml +73 -27
  4. package/.github/workflows/pull-request-checks.yml +7 -5
  5. package/docs/technical/Device-Detail-Alias-UI-Sync.md +141 -0
  6. package/package.json +13 -8
  7. package/src/react/components/AddImportModal.js +61 -34
  8. package/src/react/components/BottomNavigationBar.js +178 -174
  9. package/src/react/components/BottomNavigationBar.styles.js +139 -118
  10. package/src/react/components/DefaultProvider.native.js +427 -421
  11. package/src/react/components/DefaultProvider.web.js +131 -125
  12. package/src/react/components/RemoteCheckoutService.js +3 -3
  13. package/src/react/components/RuntimeFooterMarqueeText.js +88 -36
  14. package/src/react/components/RuntimeInfoFooter.js +76 -43
  15. package/src/react/components/RuntimeInfoFooter.styles.js +22 -14
  16. package/src/react/config/deviceConfigBootstrap.js +3 -3
  17. package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +4 -22
  18. package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +6 -2
  19. package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +4 -0
  20. package/src/react/pages/Devices/detail/DeviceDetailScreen.js +23 -338
  21. package/src/react/pages/Devices/detail/createDeviceDetailContext.js +20 -0
  22. package/src/react/pages/Devices/detail/useDeviceDetailActions.js +11 -36
  23. package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +129 -37
  24. package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +11 -38
  25. package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +9 -28
  26. package/src/react/pages/Imports.js +13 -11
  27. package/src/react/pages/IntegrationConfigFields.js +1 -0
  28. package/src/react/pages/IntegrationConfigPage.js +101 -45
  29. package/src/react/pages/IntegrationConfigPage.utils.js +21 -13
  30. package/src/react/pages/fetchPeopleConfigs.js +21 -0
  31. package/src/react/pages/integrationsCatalog.js +15 -110
  32. package/src/react/services/Cielo/Cielo.js +9 -9
  33. package/src/react/services/Getnet/Getnet.web.js +25 -0
  34. package/src/react/utils/companyAuthority.js +36 -0
  35. package/src/react/utils/fileUrl.js +14 -5
  36. package/src/react/utils/paymentDevices.js +375 -270
  37. package/src/react/utils/printerDevices.js +2 -2
  38. package/src/react/utils/runtimeFooter.js +178 -224
  39. package/src/react/utils/runtimeFooterText.js +160 -0
  40. package/src/react/utils/runtimeLanguage.js +5 -5
  41. package/src/react/utils/runtimeZoom.js +29 -5
  42. package/src/react/utils/shopFranchises.js +167 -12
  43. package/src/react/utils/storeColumns.js +235 -217
  44. package/src/react/utils/systemErrorMessage.js +2 -1
  45. package/src/store/imports/customActions.js +42 -2
  46. package/src/store/integration/index.js +43 -0
  47. package/src/tests/browser/manager/device-detail-apply-config-defined.spec.js +31 -0
  48. package/src/tests/browser/manager/device-detail-composition.spec.js +295 -0
  49. package/src/tests/browser/manager/device-detail-copy-config.spec.js +11 -1
  50. package/src/tests/browser/manager/device-detail-pdv-current-company.spec.js +273 -0
  51. package/src/tests/browser/manager/runtime-footer-rotation.spec.js +397 -0
  52. package/src/tests/react/components/DefaultProviderAuthority.test.js +28 -0
  53. package/src/tests/react/components/RuntimeFooterMarqueeText.build.test.js +29 -0
  54. package/src/tests/react/config/deviceConfigBootstrap.test.js +24 -1
  55. package/src/tests/react/pages/applyCurrentDeviceConfig.test.js +85 -0
  56. package/src/tests/react/pages/deviceDetailComposition.test.js +65 -0
  57. package/src/tests/react/pages/deviceDetailHookDependencies.test.mjs +31 -0
  58. package/src/tests/react/pages/deviceDetailLoaderDependencies.test.mjs +28 -0
  59. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +27 -6
  60. package/src/tests/react/services/Cielo.test.js +2 -2
  61. package/src/tests/react/utils/companyAuthority.test.js +53 -0
  62. package/src/tests/react/utils/formatter.test.mjs +6 -0
  63. package/src/tests/react/utils/paymentDevices.test.js +72 -0
  64. package/src/tests/react/utils/runtimeFooter.test.js +35 -0
  65. package/src/tests/react/utils/runtimeFooterText.test.js +88 -0
  66. package/src/tests/react/utils/runtimeLanguage.test.js +4 -4
  67. package/src/tests/react/utils/runtimeZoom.test.js +20 -0
  68. package/src/tests/react/utils/shopFranchises.test.js +32 -13
  69. package/src/tests/react/utils/translate.test.mjs +6 -6
  70. package/src/utils/formatter.js +7 -3
  71. package/src/utils/translate.js +16 -16
  72. package/src/utils/translatePending.js +6 -6
  73. package/src/utils/translateStorage.js +2 -2
@@ -11,18 +11,29 @@ import Icon from 'react-native-vector-icons/MaterialIcons';
11
11
  import { useStore } from '@store';
12
12
  import AnimatedModal from './AnimatedModal';
13
13
  import { useMessage } from '@controleonline/ui-common/src/react/components/MessageService';
14
- import ImportsPage from '@controleonline/ui-common/src/react/pages/Imports';
15
14
  import { useState } from 'react';
16
15
  import { resolveSystemErrorMessage } from '@controleonline/ui-common/src/react/utils/systemErrorMessage';
17
16
  import styles from './AddImportModal.styles';
18
17
 
18
+ const FORBIDDEN_EXTENSIONS = new Set(['*', '*.*', '', '.', '.*']);
19
+
20
+ const sanitizeAllowedExtensions = (extensions, importType) => {
21
+ const fallback =
22
+ importType === 'invoice_tax' || importType === 'xml' ? ['xml', 'zip'] : ['csv'];
23
+ const source = Array.isArray(extensions) && extensions.length > 0 ? extensions : fallback;
24
+ return [...new Set(
25
+ source
26
+ .map(item => String(item || '').trim().replace(/^\./, '').toLowerCase())
27
+ .filter(item => item && !FORBIDDEN_EXTENSIONS.has(item) && !item.includes('*')),
28
+ )];
29
+ };
30
+
19
31
  const AddImportModal = ({
20
32
  visible,
21
33
  onClose,
22
34
  onSuccess,
23
35
  context = {},
24
- allowedExtensions = [], // default will be set to ['csv'] if empty
25
- // acceptedTypes will be derived from allowedExtensions for security
36
+ allowedExtensions = [],
26
37
  helperLabel,
27
38
  modalTitle,
28
39
  fileLabel,
@@ -46,12 +57,14 @@ const AddImportModal = ({
46
57
  const _importSuccessLabel = importSuccessLabel ?? fallbackSuccessLabel;
47
58
  const _importErrorLabel = importErrorLabel ?? fallbackErrorLabel;
48
59
  const _helperLabel = helperLabel ?? fallbackInvalidFileLabel;
49
- // Resolve allowed extensions securely – default to CSV only
50
- const _allowedExtensions = (allowedExtensions && allowedExtensions.length > 0) ? allowedExtensions : ['csv'];
51
- // Build acceptedTypes string for file input (e.g., ".csv,.xml,.zip")
60
+ const resolvedImportType =
61
+ importType ?? context.context ?? (allowedExtensions.includes('xml') ? 'xml' : 'csv');
62
+ const _allowedExtensions = sanitizeAllowedExtensions(
63
+ allowedExtensions.length > 0 ? allowedExtensions : context.allowedExtensions,
64
+ resolvedImportType,
65
+ );
52
66
  const _acceptedTypes = _allowedExtensions.map(ext => `.${ext}`).join(',');
53
- // Determine import type: prioritize prop, then infer from allowed extensions, default to 'csv'
54
- const _importType = importType ?? (_allowedExtensions.includes('xml') ? 'xml' : (_allowedExtensions.includes('zip') ? 'zip' : 'csv'));
67
+ const _importType = resolvedImportType;
55
68
  const { showError, showSuccess } = useMessage();
56
69
  const peopleStore = useStore('people');
57
70
  const importsStore = useStore('imports');
@@ -68,15 +81,36 @@ const AddImportModal = ({
68
81
  };
69
82
 
70
83
  const [showImportList, setShowImportList] = useState(false);
71
- const [importResult, setImportResult] = useState(null); // true = success, false = error
84
+ const [importResult, setImportResult] = useState(null);
72
85
  const [uploading, setUploading] = useState(false);
73
86
 
74
- const handleUploadImportFile = async ({ file }) => {
75
- if (_allowedExtensions.length > 0) {
76
- const extensionRegex = new RegExp(`\\\\.(${_allowedExtensions.join('|')})$`, 'i');
77
- if (!file?.name?.toLowerCase().match(extensionRegex)) {
87
+ const handleUploadImportFile = async ({ file } = {}) => {
88
+ if (!file) {
89
+ if (typeof document === 'undefined') {
78
90
  throw new Error(_helperLabel);
79
91
  }
92
+
93
+ file = await new Promise((resolve, reject) => {
94
+ const input = document.createElement('input');
95
+ input.type = 'file';
96
+ input.accept = _acceptedTypes;
97
+ input.onchange = () => resolve(input.files?.[0] || null);
98
+ input.onerror = () => reject(new Error(_helperLabel));
99
+ input.click();
100
+ });
101
+ }
102
+
103
+ if (!file) {
104
+ return null;
105
+ }
106
+
107
+ if (_allowedExtensions.length === 0) {
108
+ throw new Error('Importar *.* nao e permitido.');
109
+ }
110
+
111
+ const extensionRegex = new RegExp(`\\.(${_allowedExtensions.join('|')})$`, 'i');
112
+ if (!file?.name?.toLowerCase().match(extensionRegex)) {
113
+ throw new Error(_helperLabel);
80
114
  }
81
115
 
82
116
  setUploading(true);
@@ -85,9 +119,9 @@ const AddImportModal = ({
85
119
  file,
86
120
  importType: _importType,
87
121
  peopleId: currentCompany.id,
122
+ allowedExtensions: _allowedExtensions,
88
123
  });
89
124
  showSuccess(_importSuccessLabel);
90
- // Indicate successful import and open import list
91
125
  setImportResult(true);
92
126
  setShowImportList(true);
93
127
  if (onSuccess) onSuccess();
@@ -124,25 +158,19 @@ const AddImportModal = ({
124
158
  </View>
125
159
 
126
160
  <ScrollView style={styles.content} keyboardShouldPersistTaps="always">
127
- <TouchableOpacity
128
- onPress={handleUploadImportFile}
129
- style={[styles.filePicker, { backgroundColor: buttonPalette.buttonBackground, borderColor: buttonPalette.buttonBorder }]}
130
- >
131
- <Text numberOfLines={1} style={[styles.fileName, { color: buttonPalette.buttonText }]}>{_selectFileLabel}</Text>
132
- {uploading ? (
133
- <ActivityIndicator color={buttonPalette.buttonIcon} />
134
- ) : (
135
- <Icon name="upload-file" size={22} color={buttonPalette.buttonIcon} />
136
- )}
137
- </TouchableOpacity>
138
- <TouchableOpacity
139
- onPress={() => setShowImportList(true)}
140
- style={[styles.filePicker, { marginTop: 10, backgroundColor: buttonPalette.buttonBackground, borderColor: buttonPalette.buttonBorder }]}
141
- >
142
- <Text style={[styles.fileName, { color: buttonPalette.buttonText }]}>{global.t?.t('imports', 'button', 'view_imports') || 'View Imports'}</Text>
143
- </TouchableOpacity>
144
- </ScrollView>
145
-
161
+ <Text style={styles.fileName}>{_fileLabel} ({_acceptedTypes})</Text>
162
+ <TouchableOpacity
163
+ onPress={() => handleUploadImportFile()}
164
+ style={[styles.filePicker, { backgroundColor: buttonPalette.buttonBackground, borderColor: buttonPalette.buttonBorder }]}
165
+ >
166
+ <Text numberOfLines={1} style={[styles.fileName, { color: buttonPalette.buttonText }]}>{_selectFileLabel}</Text>
167
+ {uploading ? (
168
+ <ActivityIndicator color={buttonPalette.buttonIcon} />
169
+ ) : (
170
+ <Icon name="upload-file" size={22} color={buttonPalette.buttonIcon} />
171
+ )}
172
+ </TouchableOpacity>
173
+ </ScrollView>
146
174
 
147
175
  <View style={styles.footer}>
148
176
  <TouchableOpacity onPress={handleClose} style={styles.secondaryButton}>
@@ -155,4 +183,3 @@ const AddImportModal = ({
155
183
  };
156
184
 
157
185
  export default AddImportModal;
158
- // TODO(store-first): quando este arquivo for mexido, mover a leitura para stores e evitar chamadas HTTP diretas quando o store ja resolver isso.
@@ -1,175 +1,179 @@
1
- import React, {useLayoutEffect, useMemo} from 'react';
2
- import {Pressable, Text, View} from 'react-native';
3
- import Icon from 'react-native-vector-icons/Feather';
4
- import {useTheme} from './DefaultProvider';
5
- import RuntimeInfoFooter from './RuntimeInfoFooter';
1
+ import React, {useLayoutEffect, useMemo} from 'react';
2
+ import {Pressable, Text, View} from 'react-native';
3
+ import Icon from 'react-native-vector-icons/Feather';
4
+ import {useTheme} from './DefaultProvider';
5
+ import RuntimeInfoFooter from './RuntimeInfoFooter';
6
6
  import {resolveMenuRouteParams} from '../utils/menuNavigation';
7
- import createStyles from './BottomNavigationBar.styles';
8
-
9
- const BottomNavigationBar = ({
10
- navigation,
11
- items = [],
12
- activeRouteName,
13
- disabled = false,
14
- colors = {},
15
- testID = 'bottom-navigation',
16
- useModernWebChromeProps = false,
17
- }) => {
18
- const theme = useTheme?.() || {};
19
- const runtimeFooter = theme?.runtimeFooter || null;
20
- const registerBottomNavigation =
21
- theme?.bottomChrome?.registerBottomNavigation || null;
22
- const activeBackground = colors.navigationActiveBackground;
23
- const activeBorder = colors.navigationActiveBorder;
24
- const activeIcon = colors.navigationActiveIcon;
25
- const activeText = colors.navigationActiveText;
26
- const dockBackground = colors.navigationBackground;
27
- const dockBorder = colors.navigationBorder;
28
- const dockShadow = colors.navigationShadow;
29
- const inactiveIcon = colors.navigationIcon;
30
- const inactiveText = colors.navigationText;
31
-
32
- const styles = useMemo(
33
- () =>
34
- createStyles({
35
- activeBackground,
36
- dockBackground,
37
- dockBorder,
38
- dockShadow,
39
- activeBorder,
40
- useModernWebChromeProps,
41
- }),
42
- [
43
- activeBackground,
44
- activeBorder,
45
- dockBackground,
46
- dockBorder,
47
- dockShadow,
48
- useModernWebChromeProps,
49
- ],
50
- );
51
-
52
- const resolveItemColors = ({isActive, isDisabled}) => {
53
- if (isActive) {
54
- return {
55
- iconColor: activeIcon,
56
- textColor: activeText,
57
- };
58
- }
59
-
60
- if (isDisabled) {
61
- return {
62
- iconColor: colors.navigationDisabledIcon,
63
- textColor: colors.navigationDisabledText,
64
- };
65
- }
66
-
67
- return {
68
- iconColor: inactiveIcon,
69
- textColor: inactiveText,
70
- };
71
- };
72
-
73
- const resolveItemStateStyles = ({isActive, isDisabled}) => [
74
- styles.item,
75
- isActive && styles.itemActive,
76
- isDisabled && styles.itemDisabled,
77
- isDisabled && {
78
- backgroundColor: colors.navigationDisabledBackground,
79
- borderColor: colors.navigationDisabledBorder,
80
- },
81
- ];
82
-
83
- const resolvePressedStyles = ({pressed, isDisabled}) =>
84
- pressed && !isDisabled
85
- ? [
86
- styles.itemPressed,
87
- ]
88
- : [];
89
-
90
- useLayoutEffect(() => {
91
- if (typeof registerBottomNavigation !== 'function') {
92
- return undefined;
93
- }
94
-
95
- return registerBottomNavigation();
96
- }, [registerBottomNavigation]);
97
-
98
- const routeItems = Array.isArray(items) ? items : [];
99
- const knownRoute = routeItems.some(item => item?.route === activeRouteName);
100
- const effectiveActiveRoute = knownRoute
101
- ? activeRouteName
102
- : routeItems[0]?.route || '';
103
-
104
- const navigateTo = item => {
105
- try {
106
- navigation?.navigate?.(item?.route, resolveMenuRouteParams(item?.routeParams));
107
- } catch {
108
- // Keep the footer stable if a route is unavailable in the current app flavor.
109
- }
110
- };
111
-
112
- const hostProps = useModernWebChromeProps
113
- ? {}
114
- : {pointerEvents: 'box-none'};
115
- const hostStyle = useModernWebChromeProps
116
- ? [styles.host, styles.hostPointerEventsBoxNone]
117
- : styles.host;
118
- const footerProps = {
119
- appVersion: runtimeFooter?.appVersion,
120
- colors: runtimeFooter?.colors || {},
121
- defaultCompany: runtimeFooter?.defaultCompany,
122
- device: runtimeFooter?.device,
123
- useModernWebChromeProps,
124
- };
125
-
126
- return (
127
- <View {...hostProps} style={hostStyle}>
128
- <View style={styles.stack}>
129
- <View style={styles.dock} testID={testID}>
130
- {routeItems.map(item => {
131
- const isActive = effectiveActiveRoute === item.route;
132
- const isDisabled = disabled || item.disabled;
133
- const iconSize = item.iconSize || 18;
134
- const {iconColor, textColor} = resolveItemColors({
135
- isActive,
136
- isDisabled,
137
- });
138
-
139
- return (
140
- <Pressable
141
- key={item.route}
142
- accessibilityRole="button"
143
- disabled={isDisabled}
144
- onPress={() => navigateTo(item)}
145
- style={({pressed}) => [
146
- ...resolveItemStateStyles({isActive, isDisabled}),
147
- ...resolvePressedStyles({pressed, isDisabled}),
148
- ]}>
149
- <View style={styles.iconWrap}>
150
- <Icon color={iconColor} name={item.icon} size={iconSize} />
151
- </View>
152
- <Text
153
- numberOfLines={1}
154
- style={[
155
- styles.itemLabel,
156
- {
157
- color: textColor,
158
- },
159
- ]}>
160
- {item.label}
161
- </Text>
162
- </Pressable>
163
- );
164
- })}
165
- </View>
166
-
167
- {runtimeFooter && (
168
- <RuntimeInfoFooter {...footerProps} />
169
- )}
170
- </View>
171
- </View>
172
- );
173
- };
174
-
175
- export default BottomNavigationBar;
7
+ import createStyles from './BottomNavigationBar.styles';
8
+
9
+ const BottomNavigationBar = ({
10
+ navigation,
11
+ items = [],
12
+ activeRouteName,
13
+ disabled = false,
14
+ colors = {},
15
+ testID = 'bottom-navigation',
16
+ useModernWebChromeProps = false,
17
+ }) => {
18
+ const theme = useTheme?.() || {};
19
+ const runtimeFooter = theme?.runtimeFooter || null;
20
+ const registerBottomNavigation =
21
+ theme?.bottomChrome?.registerBottomNavigation || null;
22
+ const activeBackground = colors.navigationActiveBackground;
23
+ const activeBorder = colors.navigationActiveBorder;
24
+ const activeIcon = colors.navigationActiveIcon;
25
+ const activeText = colors.navigationActiveText;
26
+ const dockBackground = colors.navigationBackground;
27
+ const dockBorder = colors.navigationBorder;
28
+ const dockShadow = colors.navigationShadow;
29
+ const inactiveIcon = colors.navigationIcon;
30
+ const inactiveText = colors.navigationText;
31
+
32
+ const styles = useMemo(
33
+ () =>
34
+ createStyles({
35
+ activeBackground,
36
+ dockBackground,
37
+ dockBorder,
38
+ dockShadow,
39
+ activeBorder,
40
+ useModernWebChromeProps,
41
+ }),
42
+ [
43
+ activeBackground,
44
+ activeBorder,
45
+ dockBackground,
46
+ dockBorder,
47
+ dockShadow,
48
+ useModernWebChromeProps,
49
+ ],
50
+ );
51
+
52
+ const resolveItemColors = ({isActive, isDisabled}) => {
53
+ if (isActive) {
54
+ return {
55
+ iconColor: activeIcon,
56
+ textColor: activeText,
57
+ };
58
+ }
59
+
60
+ if (isDisabled) {
61
+ return {
62
+ iconColor: colors.navigationDisabledIcon,
63
+ textColor: colors.navigationDisabledText,
64
+ };
65
+ }
66
+
67
+ return {
68
+ iconColor: inactiveIcon,
69
+ textColor: inactiveText,
70
+ };
71
+ };
72
+
73
+ const resolveItemStateStyles = ({isActive, isDisabled}) => [
74
+ styles.item,
75
+ isActive && styles.itemActive,
76
+ isDisabled && styles.itemDisabled,
77
+ isDisabled && {
78
+ backgroundColor: colors.navigationDisabledBackground,
79
+ borderColor: colors.navigationDisabledBorder,
80
+ },
81
+ ];
82
+
83
+ const resolvePressedStyles = ({pressed, isDisabled}) =>
84
+ pressed && !isDisabled
85
+ ? [
86
+ styles.itemPressed,
87
+ ]
88
+ : [];
89
+
90
+ useLayoutEffect(() => {
91
+ if (typeof registerBottomNavigation !== 'function') {
92
+ return undefined;
93
+ }
94
+
95
+ return registerBottomNavigation();
96
+ }, [registerBottomNavigation]);
97
+
98
+ const routeItems = Array.isArray(items) ? items : [];
99
+ const knownRoute = routeItems.some(item => item?.route === activeRouteName);
100
+ const effectiveActiveRoute = knownRoute
101
+ ? activeRouteName
102
+ : routeItems[0]?.route || '';
103
+
104
+ const navigateTo = item => {
105
+ try {
106
+ navigation?.navigate?.(item?.route, resolveMenuRouteParams(item?.routeParams));
107
+ } catch {
108
+ // Keep the footer stable if a route is unavailable in the current app flavor.
109
+ }
110
+ };
111
+
112
+ const hostProps = useModernWebChromeProps
113
+ ? {}
114
+ : {pointerEvents: 'box-none'};
115
+ const hostStyle = useModernWebChromeProps
116
+ ? [styles.host, styles.hostPointerEventsBoxNone]
117
+ : styles.host;
118
+ const footerProps = {
119
+ appVersion: runtimeFooter?.appVersion,
120
+ colors: runtimeFooter?.colors || {},
121
+ mainCompany: runtimeFooter?.mainCompany,
122
+ device: runtimeFooter?.device,
123
+ useModernWebChromeProps,
124
+ };
125
+
126
+ return (
127
+ <View {...hostProps} style={hostStyle}>
128
+ <View style={styles.stack}>
129
+ <View style={styles.dock} testID={testID}>
130
+ <View style={styles.itemsRow}>
131
+ {routeItems.map(item => {
132
+ const isActive = effectiveActiveRoute === item.route;
133
+ const isDisabled = disabled || item.disabled;
134
+ const iconSize = item.iconSize || 18;
135
+ const {iconColor, textColor} = resolveItemColors({
136
+ isActive,
137
+ isDisabled,
138
+ });
139
+
140
+ return (
141
+ <Pressable
142
+ key={item.route}
143
+ accessibilityRole="button"
144
+ disabled={isDisabled}
145
+ onPress={() => navigateTo(item)}
146
+ style={({pressed}) => [
147
+ ...resolveItemStateStyles({isActive, isDisabled}),
148
+ ...resolvePressedStyles({pressed, isDisabled}),
149
+ ]}>
150
+ <View style={styles.iconWrap}>
151
+ <Icon color={iconColor} name={item.icon} size={iconSize} />
152
+ </View>
153
+ <Text
154
+ numberOfLines={1}
155
+ style={[
156
+ styles.itemLabel,
157
+ {
158
+ color: textColor,
159
+ },
160
+ ]}>
161
+ {item.label}
162
+ </Text>
163
+ </Pressable>
164
+ );
165
+ })}
166
+ </View>
167
+ {/* app-community#384: texto do rodapé DEPOIS dos botões (não antes) */}
168
+ {runtimeFooter ? (
169
+ <View style={styles.footerSlot} testID="bottom-navigation-footer-slot">
170
+ <RuntimeInfoFooter {...footerProps} />
171
+ </View>
172
+ ) : null}
173
+ </View>
174
+ </View>
175
+ </View>
176
+ );
177
+ };
178
+
179
+ export default BottomNavigationBar;