@controleonline/ui-common 1.2.71 → 1.2.75

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 (150) hide show
  1. package/.github/actions/workers/manager/action.yml +147 -0
  2. package/.github/actions/workers/qa/action.yml +44 -0
  3. package/.github/actions/workers/security/action.yml +44 -0
  4. package/.github/actions/workers/technical-documenter/action.yml +51 -0
  5. package/.github/workflows/manager-worker.yml +168 -0
  6. package/.github/workflows/publish-npm.yml +45 -0
  7. package/.gitmodules +3 -0
  8. package/AGENTS.md +6 -0
  9. package/README.md +14 -0
  10. package/package.json +3 -2
  11. package/src/api/fetch.js +1 -1
  12. package/src/api/index.js +92 -22
  13. package/src/react/components/AddImportModal.js +94 -110
  14. package/src/react/components/BottomNavigationBar.config.js +211 -231
  15. package/src/react/components/CategoryForm.js +52 -35
  16. package/src/react/components/DefaultProvider.native.js +131 -95
  17. package/src/react/components/DefaultProvider.web.js +148 -74
  18. package/src/react/components/NetworkPrinterPreviewModal.js +249 -0
  19. package/src/react/components/NetworkPrinterPreviewModal.styles.js +159 -0
  20. package/src/react/components/RuntimeInfoFooter.js +100 -36
  21. package/src/react/components/RuntimeInfoFooter.styles.js +3 -0
  22. package/src/react/components/UnifiedPaymentBar.js +7 -4
  23. package/src/react/components/UserAvatar.js +160 -0
  24. package/src/react/components/integrations/IntegrationHero/index.js +48 -0
  25. package/src/react/components/integrations/IntegrationHero/styles.js +57 -0
  26. package/src/react/components/integrations/IntegrationTabs/index.js +89 -0
  27. package/src/react/components/integrations/IntegrationTabs/styles.js +44 -0
  28. package/src/react/config/deviceConfigBootstrap.js +5 -3
  29. package/src/react/pages/Connections.js +229 -0
  30. package/src/react/pages/Connections.styles.js +222 -0
  31. package/src/react/pages/DeviceDetailPage.js +2814 -0
  32. package/src/react/pages/DeviceDetailPage.styles.js +308 -0
  33. package/src/react/pages/Devices/currentDevice.js +194 -0
  34. package/src/react/pages/Devices/deviceTypes/DeviceGroupCard.js +257 -0
  35. package/src/react/pages/Devices/deviceTypes/all.js +23 -0
  36. package/src/react/pages/Devices/deviceTypes/device.js +25 -0
  37. package/src/react/pages/Devices/deviceTypes/deviceListHelpers.js +178 -0
  38. package/src/react/pages/Devices/deviceTypes/display.js +26 -0
  39. package/src/react/pages/Devices/deviceTypes/index.js +145 -0
  40. package/src/react/pages/Devices/deviceTypes/ipCamera.js +35 -0
  41. package/src/react/pages/Devices/deviceTypes/pdv.js +88 -0
  42. package/src/react/pages/Devices/deviceTypes/printer.js +37 -0
  43. package/src/react/pages/Devices/deviceTypes/shared.js +453 -0
  44. package/src/react/pages/Devices/deviceTypes/useDeviceNetworkConnectivity.js +145 -0
  45. package/src/react/pages/Devices.js +183 -0
  46. package/src/react/pages/Devices.styles.js +353 -0
  47. package/src/react/pages/Food99IntegrationPage/components/Food99CatalogTab.js +219 -0
  48. package/src/react/pages/Food99IntegrationPage/components/Food99OverviewTab.js +134 -0
  49. package/src/react/pages/Food99IntegrationPage/components/Food99PreviewModal.js +173 -0
  50. package/src/react/pages/Food99IntegrationPage/components/Food99ProductCard.js +1 -0
  51. package/src/react/pages/Food99IntegrationPage/components/Food99QuickWalletModal.js +181 -0
  52. package/src/react/pages/Food99IntegrationPage/components/Food99SettingsTab.js +306 -0
  53. package/src/react/pages/Food99IntegrationPage/components/Food99StoreTab.js +417 -0
  54. package/src/react/pages/Food99IntegrationPage/index.js +1530 -0
  55. package/src/react/pages/Food99IntegrationPage/styles.js +792 -0
  56. package/src/react/pages/Food99IntegrationPage/utils.js +155 -0
  57. package/src/react/pages/Food99IntegrationPage.js +1 -0
  58. package/src/react/pages/Food99IntegrationPage.styles.js +1 -0
  59. package/src/react/pages/GenericLogPage.js +42 -167
  60. package/src/react/pages/IFoodIntegrationPage/components/IFoodCatalogTab.js +8 -0
  61. package/src/react/pages/IFoodIntegrationPage/components/IFoodOperationsTab.js +420 -0
  62. package/src/react/pages/IFoodIntegrationPage/components/IFoodOverviewTab.js +118 -0
  63. package/src/react/pages/IFoodIntegrationPage/components/IFoodPreviewModal.js +103 -0
  64. package/src/react/pages/IFoodIntegrationPage/components/IFoodProductCard.js +1 -0
  65. package/src/react/pages/IFoodIntegrationPage/components/IFoodStoreTab.js +150 -0
  66. package/src/react/pages/IFoodIntegrationPage/index.js +1298 -0
  67. package/src/react/pages/IFoodIntegrationPage/styles.js +764 -0
  68. package/src/react/pages/IFoodIntegrationPage/utils.js +91 -0
  69. package/src/react/pages/IFoodIntegrationPage.js +1 -0
  70. package/src/react/pages/IFoodIntegrationPage.styles.js +1 -0
  71. package/src/react/pages/Imports.js +214 -166
  72. package/src/react/pages/IntegrationConfigPage.js +744 -0
  73. package/src/react/pages/IntegrationConfigPage.styles.js +269 -0
  74. package/src/react/pages/Integrations.js +299 -0
  75. package/src/react/pages/Integrations.styles.js +130 -0
  76. package/src/react/pages/ManagerOrderNotificationsPage.js +642 -0
  77. package/src/react/pages/MarketplaceIntegrationPage/components/MarketplaceProductCard.js +205 -0
  78. package/src/react/pages/MarketplaceIntegrationPage/components/RemoteMarketplaceMenuTab.js +335 -0
  79. package/src/react/pages/MarketplaceIntegrationPage/index.js +60 -0
  80. package/src/react/pages/MarketplaceIntegrationPage/utils.js +137 -0
  81. package/src/react/pages/MercadoLivreIntegrationPage.js +538 -0
  82. package/src/react/pages/PrinterDeviceDetailPage.js +1916 -0
  83. package/src/react/pages/PrinterDeviceDetailPage.styles.js +223 -0
  84. package/src/react/pages/PrinterDeviceFormPage.js +521 -0
  85. package/src/react/pages/PrinterDeviceFormPage.styles.js +159 -0
  86. package/src/react/pages/WhatsAppConnectionPage.js +459 -0
  87. package/src/react/pages/WhatsAppConnectionPage.styles.js +284 -0
  88. package/src/react/pages/integrationsCatalog.js +399 -0
  89. package/src/react/services/Cielo/Cielo.js +193 -193
  90. package/src/react/utils/categoryContexts.js +46 -0
  91. package/src/react/utils/entityDisplay.js +222 -217
  92. package/src/react/utils/fileUrl.js +39 -20
  93. package/src/react/utils/integrationPage.js +16 -0
  94. package/src/react/utils/runtimeFooter.js +40 -22
  95. package/src/react/utils/runtimeLanguage.js +50 -9
  96. package/src/react/utils/runtimeMenu.js +191 -131
  97. package/src/react/utils/runtimeZoom.js +43 -0
  98. package/src/react/utils/shopConfig.js +27 -15
  99. package/src/react/utils/shopFranchises.js +126 -1
  100. package/src/react/utils/storeColumns.js +23 -4
  101. package/src/react/utils/userAvatar.js +104 -0
  102. package/src/store/categories/index.js +48 -20
  103. package/src/store/device_config/index.js +62 -21
  104. package/src/store/entity_log/index.js +22 -1
  105. package/src/store/imports/customActions.js +51 -0
  106. package/src/store/imports/index.js +89 -88
  107. package/src/store/status/index.js +11 -11
  108. package/src/store/themes/index.js +22 -0
  109. package/src/store/timezones/index.js +3 -1
  110. package/src/tests/browser/manager/connections.spec.js +143 -0
  111. package/src/tests/browser/manager/devices-current.spec.js +343 -0
  112. package/src/tests/browser/manager/mercado-livre-integration.spec.js +268 -0
  113. package/src/tests/react/api/deviceHeaders.test.js +114 -0
  114. package/src/tests/react/components/AddImportModal.test.js +166 -0
  115. package/src/tests/react/components/BottomNavigationBar.config.test.js +20 -0
  116. package/src/tests/react/components/UserAvatar.test.js +52 -0
  117. package/src/tests/react/pages/DevicePickerStyles.test.js +47 -0
  118. package/src/tests/react/pages/Devices/deviceListHelpers.test.js +39 -0
  119. package/src/tests/react/pages/DevicesCurrentDevice.test.js +198 -0
  120. package/src/tests/react/pages/connectionsHelpers.test.js +34 -0
  121. package/src/tests/react/pages/mercadoLivreIntegrationPageContract.test.mjs +16 -0
  122. package/src/tests/react/services/Cielo.test.js +190 -190
  123. package/src/tests/react/store/categoriesOrdering.test.js +26 -0
  124. package/src/tests/react/utils/appDomain.test.js +62 -0
  125. package/src/tests/react/utils/entityDisplay.test.js +2 -2
  126. package/src/tests/react/utils/fileUrl.test.js +3 -3
  127. package/src/tests/react/utils/formatter.test.mjs +33 -0
  128. package/src/tests/react/utils/runtimeFooter.test.js +29 -4
  129. package/src/tests/react/utils/runtimeLanguage.test.js +79 -9
  130. package/src/tests/react/utils/runtimeMenu.test.js +198 -134
  131. package/src/tests/react/utils/runtimeZoom.test.js +38 -0
  132. package/src/tests/react/utils/shopConfig.test.js +10 -9
  133. package/src/tests/react/utils/shopFranchises.test.js +43 -0
  134. package/src/tests/react/utils/translate.discovery.test.mjs +170 -0
  135. package/src/tests/react/utils/translate.test.helpers.mjs +152 -0
  136. package/src/tests/react/utils/translate.test.mjs +141 -349
  137. package/src/tests/react/utils/userAvatar.test.js +63 -0
  138. package/src/utils/appDomain.js +15 -4
  139. package/src/utils/formatter.js +104 -16
  140. package/src/utils/integrationConfigs.js +80 -80
  141. package/src/utils/translate.js +222 -438
  142. package/src/utils/translateDiscovery.js +96 -0
  143. package/src/utils/translateNormalize.js +33 -0
  144. package/src/utils/translatePending.js +92 -0
  145. package/src/utils/translateStorage.js +142 -0
  146. package/.github/agents/developer.agent.md +0 -30
  147. package/.github/agents/devops.agent.md +0 -30
  148. package/.github/agents/qa.agent.md +0 -30
  149. package/.github/agents/security.agent.md +0 -30
  150. package/.github/workflows/npmjs.yml +0 -35
@@ -0,0 +1,159 @@
1
+ import {Platform, StyleSheet} from 'react-native';
2
+
3
+ const styles = StyleSheet.create({
4
+ backdrop: {
5
+ flex: 1,
6
+ backgroundColor: 'rgba(15,23,42,0.42)',
7
+ justifyContent: 'center',
8
+ padding: 16,
9
+ },
10
+ panel: {
11
+ maxHeight: '88%',
12
+ borderRadius: 16,
13
+ backgroundColor: '#fff',
14
+ overflow: 'hidden',
15
+ },
16
+ header: {
17
+ borderBottomWidth: 1,
18
+ borderBottomColor: '#E2E8F0',
19
+ paddingHorizontal: 16,
20
+ paddingVertical: 14,
21
+ flexDirection: 'row',
22
+ alignItems: 'center',
23
+ gap: 12,
24
+ },
25
+ headerCopy: {
26
+ flex: 1,
27
+ gap: 4,
28
+ },
29
+ title: {
30
+ fontSize: 16,
31
+ fontWeight: '800',
32
+ color: '#0F172A',
33
+ },
34
+ subtitle: {
35
+ fontSize: 12,
36
+ color: '#64748B',
37
+ lineHeight: 17,
38
+ },
39
+ closeButton: {
40
+ width: 36,
41
+ height: 36,
42
+ borderRadius: 10,
43
+ alignItems: 'center',
44
+ justifyContent: 'center',
45
+ backgroundColor: '#F1F5F9',
46
+ },
47
+ content: {
48
+ padding: 16,
49
+ gap: 12,
50
+ },
51
+ metaGrid: {
52
+ flexDirection: 'row',
53
+ flexWrap: 'wrap',
54
+ gap: 8,
55
+ },
56
+ metaChip: {
57
+ borderRadius: 999,
58
+ backgroundColor: '#E0F2FE',
59
+ paddingHorizontal: 10,
60
+ paddingVertical: 6,
61
+ },
62
+ metaText: {
63
+ fontSize: 11,
64
+ fontWeight: '800',
65
+ color: '#075985',
66
+ },
67
+ paper: {
68
+ borderWidth: 1,
69
+ borderColor: '#CBD5E1',
70
+ borderRadius: 12,
71
+ backgroundColor: '#F8FAFC',
72
+ padding: 12,
73
+ },
74
+ receiptWrap: {
75
+ alignSelf: 'center',
76
+ maxWidth: '100%',
77
+ },
78
+ logoWrap: {
79
+ alignItems: 'center',
80
+ marginBottom: 8,
81
+ },
82
+ logo: {
83
+ width: 96,
84
+ height: 40,
85
+ },
86
+ cutLine: {
87
+ marginVertical: 12,
88
+ borderTopWidth: 1,
89
+ borderTopColor: '#94A3B8',
90
+ borderStyle: 'dashed',
91
+ alignItems: 'center',
92
+ },
93
+ cutText: {
94
+ marginTop: -9,
95
+ paddingHorizontal: 8,
96
+ backgroundColor: '#fff',
97
+ fontSize: 11,
98
+ fontWeight: '800',
99
+ color: '#64748B',
100
+ textTransform: 'uppercase',
101
+ },
102
+ receiptText: {
103
+ fontFamily: Platform.select({
104
+ ios: 'Courier',
105
+ android: 'monospace',
106
+ default: 'monospace',
107
+ }),
108
+ fontSize: 12,
109
+ lineHeight: 17,
110
+ color: '#0F172A',
111
+ },
112
+ receiptTextCenter: {
113
+ textAlign: 'center',
114
+ },
115
+ receiptTextBold: {
116
+ fontWeight: '800',
117
+ },
118
+ receiptTextLarge: {
119
+ fontSize: 22,
120
+ lineHeight: 28,
121
+ fontWeight: '900',
122
+ },
123
+ receiptTextReverse: {
124
+ alignSelf: 'flex-start',
125
+ backgroundColor: '#0F172A',
126
+ color: '#F8FAFC',
127
+ paddingHorizontal: 3,
128
+ },
129
+ receiptTextReverseCenter: {
130
+ alignSelf: 'stretch',
131
+ },
132
+ hint: {
133
+ fontSize: 12,
134
+ lineHeight: 18,
135
+ color: '#64748B',
136
+ },
137
+ stateBox: {
138
+ borderRadius: 12,
139
+ backgroundColor: '#F1F5F9',
140
+ padding: 14,
141
+ },
142
+ stateText: {
143
+ fontSize: 13,
144
+ fontWeight: '700',
145
+ color: '#475569',
146
+ },
147
+ errorBox: {
148
+ borderRadius: 12,
149
+ backgroundColor: '#FEF2F2',
150
+ padding: 14,
151
+ },
152
+ errorText: {
153
+ fontSize: 13,
154
+ fontWeight: '700',
155
+ color: '#B91C1C',
156
+ },
157
+ });
158
+
159
+ export default styles;
@@ -1,5 +1,11 @@
1
- import React, {useEffect, useMemo, useState} from 'react';
2
- import {ActivityIndicator, Text, View, useWindowDimensions} from 'react-native';
1
+ import React, {useEffect, useMemo, useRef, useState} from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Animated,
5
+ Text,
6
+ View,
7
+ useWindowDimensions,
8
+ } from 'react-native';
3
9
  import {useStore, useStores} from '@store';
4
10
  import {useSafeAreaInsets} from 'react-native-safe-area-context';
5
11
  import {
@@ -8,15 +14,18 @@ import {
8
14
  parseConfigsObject,
9
15
  } from '@controleonline/ui-common/src/react/config/deviceConfigBootstrap';
10
16
  import {
11
- getRuntimeFooterDebugInfo,
12
- getRuntimeFooterPrimaryText,
13
- getRuntimeFooterText,
14
- } from '@controleonline/ui-common/src/react/utils/runtimeFooter';
15
- import styles from './RuntimeInfoFooter.styles';
16
-
17
- const ROTATION_INTERVAL_MS = 6000;
18
- const COMPACT_BREAKPOINT = 720;
19
- const MAX_INLINE_TEXT_LENGTH = 84;
17
+ getRuntimeFooterDebugInfo,
18
+ getRuntimeFooterPrimaryText,
19
+ getRuntimeFooterRotationEntries,
20
+ getRuntimeFooterText,
21
+ getRuntimeFooterTextLines,
22
+ } from '@controleonline/ui-common/src/react/utils/runtimeFooter';
23
+ import styles from './RuntimeInfoFooter.styles';
24
+
25
+ const ROTATION_INTERVAL_MS = 6000;
26
+ const FADE_DURATION_MS = 260;
27
+ const COMPACT_BREAKPOINT = 720;
28
+ const MAX_INLINE_TEXT_LENGTH = 84;
20
29
 
21
30
  const RuntimeInfoFooter = ({
22
31
  appVersion,
@@ -28,6 +37,7 @@ const RuntimeInfoFooter = ({
28
37
  const {width} = useWindowDimensions();
29
38
  const insets = useSafeAreaInsets();
30
39
  const [activeIndex, setActiveIndex] = useState(0);
40
+ const fadeOpacity = useRef(new Animated.Value(1)).current;
31
41
  const allStores = useStores(state => state);
32
42
  const deviceConfigStore = useStore('device_config');
33
43
  const runtimeDebugStore = useStore('runtime_debug');
@@ -53,10 +63,14 @@ const RuntimeInfoFooter = ({
53
63
  }),
54
64
  [appVersion, device, deviceConfigItem, footerDebugInfo.primaryText],
55
65
  );
56
- const companyFooterText = useMemo(
57
- () => getRuntimeFooterText(defaultCompany),
58
- [defaultCompany?.configs],
59
- );
66
+ const companyFooterText = useMemo(
67
+ () => getRuntimeFooterText(defaultCompany),
68
+ [defaultCompany?.configs],
69
+ );
70
+ const footerTextLines = useMemo(
71
+ () => getRuntimeFooterTextLines(companyFooterText),
72
+ [companyFooterText],
73
+ );
60
74
  const deviceConfigs = useMemo(
61
75
  () => parseConfigsObject(deviceConfigItem?.configs),
62
76
  [deviceConfigItem?.configs],
@@ -69,15 +83,26 @@ const RuntimeInfoFooter = ({
69
83
  [deviceConfigs],
70
84
  );
71
85
 
72
- const entries = useMemo(
73
- () => [primaryText, companyFooterText].filter(Boolean),
74
- [companyFooterText, primaryText],
75
- );
76
- const inlineText = useMemo(() => entries.join(' • '), [entries]);
77
- const shouldRotate =
78
- !showDebugInfo &&
79
- entries.length > 1 &&
80
- (width < COMPACT_BREAKPOINT || inlineText.length > MAX_INLINE_TEXT_LENGTH);
86
+ const rotationEntries = useMemo(
87
+ () =>
88
+ getRuntimeFooterRotationEntries({
89
+ companyFooterText,
90
+ primaryText,
91
+ }),
92
+ [companyFooterText, primaryText],
93
+ );
94
+ const inlineText = useMemo(
95
+ () => [primaryText, ...footerTextLines].filter(Boolean).join(' • '),
96
+ [footerTextLines, primaryText],
97
+ );
98
+ const shouldRotate =
99
+ !showDebugInfo &&
100
+ rotationEntries.length > 1 &&
101
+ (
102
+ footerTextLines.length > 1 ||
103
+ width < COMPACT_BREAKPOINT ||
104
+ inlineText.length > MAX_INLINE_TEXT_LENGTH
105
+ );
81
106
  const footerEntries = useMemo(
82
107
  () =>
83
108
  Object.values(runtimeDebugSummary?.entries || {})
@@ -147,25 +172,61 @@ const RuntimeInfoFooter = ({
147
172
  const bottomInset = Math.max(Number(insets.bottom) || 0, 16);
148
173
 
149
174
  useEffect(() => {
150
- if (!shouldRotate || entries.length <= 1) {
175
+ if (!shouldRotate || rotationEntries.length <= 1) {
151
176
  setActiveIndex(0);
177
+ fadeOpacity.stopAnimation();
178
+ fadeOpacity.setValue(1);
152
179
  return;
153
180
  }
154
-
155
- const intervalId = setInterval(() => {
156
- setActiveIndex(current => (current + 1) % entries.length);
157
- }, ROTATION_INTERVAL_MS);
158
-
181
+
182
+ let timeoutId;
183
+ let cancelled = false;
184
+
185
+ const scheduleTransition = () => {
186
+ timeoutId = setTimeout(() => {
187
+ Animated.timing(fadeOpacity, {
188
+ toValue: 0,
189
+ duration: FADE_DURATION_MS,
190
+ useNativeDriver: true,
191
+ }).start(({finished}) => {
192
+ if (!finished || cancelled) {
193
+ return;
194
+ }
195
+
196
+ setActiveIndex(current => (current + 1) % rotationEntries.length);
197
+
198
+ Animated.timing(fadeOpacity, {
199
+ toValue: 1,
200
+ duration: FADE_DURATION_MS,
201
+ useNativeDriver: true,
202
+ }).start(({finished: fadeInFinished}) => {
203
+ if (fadeInFinished && !cancelled) {
204
+ scheduleTransition();
205
+ }
206
+ });
207
+ });
208
+ }, ROTATION_INTERVAL_MS);
209
+ };
210
+
211
+ fadeOpacity.setValue(1);
212
+ scheduleTransition();
213
+
159
214
  return () => {
160
- clearInterval(intervalId);
215
+ cancelled = true;
216
+ if (timeoutId) {
217
+ clearTimeout(timeoutId);
218
+ }
219
+ fadeOpacity.stopAnimation();
161
220
  };
162
- }, [entries.length, shouldRotate]);
221
+ }, [fadeOpacity, rotationEntries.length, shouldRotate]);
163
222
 
164
- if (entries.length === 0 && !showDebugInfo) {
223
+ if (rotationEntries.length === 0 && !showDebugInfo) {
165
224
  return null;
166
225
  }
167
226
 
168
- const displayedText = shouldRotate ? entries[activeIndex] : inlineText;
227
+ const displayedText = shouldRotate
228
+ ? rotationEntries[activeIndex]
229
+ : inlineText;
169
230
  const backgroundColor = colors?.footerBackground;
170
231
  const borderColor = colors?.footerBorder;
171
232
  const textColor = colors?.footerText;
@@ -199,18 +260,21 @@ const RuntimeInfoFooter = ({
199
260
  ]}
200
261
  />
201
262
  </View>
202
- <Text
263
+ <Animated.Text
203
264
  numberOfLines={1}
204
265
  ellipsizeMode="tail"
205
266
  minimumFontScale={0.85}
206
267
  style={[
207
268
  styles.primaryText,
269
+ {
270
+ opacity: showDebugInfo ? 1 : fadeOpacity,
271
+ },
208
272
  {
209
273
  color: textColor,
210
274
  },
211
275
  ]}>
212
276
  {showDebugInfo ? inlineText || primaryText || device?.id || '--' : displayedText}
213
- </Text>
277
+ </Animated.Text>
214
278
  <View style={styles.loadingWrap}>
215
279
  {hasStoreLoading && (
216
280
  <ActivityIndicator color={loadingColor} size="small" />
@@ -18,15 +18,18 @@ const styles = StyleSheet.create({
18
18
  flexDirection: 'row',
19
19
  alignItems: 'center',
20
20
  justifyContent: 'center',
21
+ minHeight: 20,
21
22
  },
22
23
  statusDotWrap: {
23
24
  minWidth: 22,
25
+ minHeight: 20,
24
26
  marginRight: 8,
25
27
  alignItems: 'center',
26
28
  justifyContent: 'center',
27
29
  },
28
30
  loadingWrap: {
29
31
  minWidth: 22,
32
+ minHeight: 20,
30
33
  marginLeft: 8,
31
34
  alignItems: 'center',
32
35
  justifyContent: 'center',
@@ -115,10 +115,13 @@ export default function UnifiedPaymentBar({
115
115
  );
116
116
 
117
117
  return (
118
- <TouchableOpacity
119
- key={key}
120
- disabled={disabled}
121
- onPress={action?.onPress}
118
+ <TouchableOpacity
119
+ key={key}
120
+ accessibilityLabel={action?.accessibilityLabel || action?.label}
121
+ accessibilityRole="button"
122
+ accessibilityState={{disabled}}
123
+ disabled={disabled}
124
+ onPress={action?.onPress}
122
125
  style={[
123
126
  styles.actionButton,
124
127
  visibleActions.length === 1 && styles.actionButtonSingle,
@@ -0,0 +1,160 @@
1
+ import React, {useEffect, useMemo, useState} from 'react';
2
+ import {Image, Platform, StyleSheet, Text, View} from 'react-native';
3
+ import {env as APP_ENV} from '@env';
4
+ import {getGravatarUrl, getUserInitials} from '../utils/userAvatar';
5
+
6
+ const normalizeUrl = value => String(value || '').trim();
7
+
8
+ const readSessionToken = () => {
9
+ if (typeof localStorage === 'undefined' || !localStorage?.getItem) {
10
+ return '';
11
+ }
12
+
13
+ try {
14
+ const session = JSON.parse(localStorage.getItem('session') || '{}');
15
+ return normalizeUrl(session?.api_key || session?.token);
16
+ } catch {
17
+ return '';
18
+ }
19
+ };
20
+
21
+ const useDisplayUri = uri => {
22
+ const [displayUri, setDisplayUri] = useState(normalizeUrl(uri));
23
+
24
+ useEffect(() => {
25
+ let cancelled = false;
26
+ let objectUrl = '';
27
+ const next = normalizeUrl(uri);
28
+
29
+ const run = async () => {
30
+ if (!next) {
31
+ setDisplayUri('');
32
+ return;
33
+ }
34
+
35
+ const isBackendDownload = /\/files\/[^/?#]+\/download/i.test(next);
36
+ const token = readSessionToken();
37
+
38
+ if (!isBackendDownload || Platform.OS !== 'web' || !token) {
39
+ setDisplayUri(next);
40
+ return;
41
+ }
42
+
43
+ try {
44
+ const headers = {
45
+ Accept: '*/*',
46
+ 'API-TOKEN': token,
47
+ };
48
+ const host =
49
+ normalizeUrl(APP_ENV?.DOMAIN) ||
50
+ (typeof location !== 'undefined' ? location.host : '');
51
+ if (host) {
52
+ headers['App-Domain'] = host;
53
+ }
54
+
55
+ const response = await fetch(next, {method: 'GET', headers});
56
+ if (!response.ok) {
57
+ if (!cancelled) setDisplayUri(next);
58
+ return;
59
+ }
60
+
61
+ const blob = await response.blob();
62
+ objectUrl = URL.createObjectURL(blob);
63
+ if (!cancelled) setDisplayUri(objectUrl);
64
+ } catch {
65
+ if (!cancelled) setDisplayUri(next);
66
+ }
67
+ };
68
+
69
+ run();
70
+
71
+ return () => {
72
+ cancelled = true;
73
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
74
+ };
75
+ }, [uri]);
76
+
77
+ return displayUri;
78
+ };
79
+
80
+ const UserAvatar = ({
81
+ imageUrl,
82
+ email,
83
+ name,
84
+ size = 40,
85
+ backgroundColor,
86
+ borderColor,
87
+ borderWidth = 1,
88
+ textColor,
89
+ style,
90
+ useGravatar = true,
91
+ }) => {
92
+ const sources = useMemo(
93
+ () =>
94
+ [
95
+ normalizeUrl(imageUrl),
96
+ useGravatar ? getGravatarUrl(email, Math.max(size * 2, 80)) : '',
97
+ ].filter(Boolean),
98
+ [email, imageUrl, size, useGravatar],
99
+ );
100
+ const [sourceIndex, setSourceIndex] = useState(0);
101
+
102
+ useEffect(() => {
103
+ setSourceIndex(0);
104
+ }, [sources]);
105
+
106
+ const currentSource = sources[sourceIndex];
107
+ const displayUri = useDisplayUri(currentSource);
108
+
109
+ const containerStyle = [
110
+ styles.container,
111
+ style,
112
+ {
113
+ width: size,
114
+ height: size,
115
+ borderRadius: size / 2,
116
+ backgroundColor,
117
+ borderColor,
118
+ borderWidth,
119
+ },
120
+ ];
121
+
122
+ return (
123
+ <View style={containerStyle}>
124
+ {displayUri ? (
125
+ <Image
126
+ source={{uri: displayUri}}
127
+ style={styles.image}
128
+ onError={() => setSourceIndex(index => index + 1)}
129
+ />
130
+ ) : (
131
+ <Text
132
+ style={[
133
+ styles.initials,
134
+ {color: textColor, fontSize: Math.max(Math.round(size * 0.38), 14)},
135
+ ]}>
136
+ {getUserInitials({name, email})}
137
+ </Text>
138
+ )}
139
+ </View>
140
+ );
141
+ };
142
+
143
+ const styles = StyleSheet.create({
144
+ container: {
145
+ alignItems: 'center',
146
+ justifyContent: 'center',
147
+ overflow: 'hidden',
148
+ },
149
+ image: {
150
+ width: '100%',
151
+ height: '100%',
152
+ },
153
+ initials: {
154
+ fontWeight: '700',
155
+ letterSpacing: 0.5,
156
+ textAlign: 'center',
157
+ },
158
+ });
159
+
160
+ export default UserAvatar;
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import { Image, Text, View } from 'react-native';
3
+ import Icon from 'react-native-vector-icons/Feather';
4
+ import { colors } from '@controleonline/../../src/styles/colors';
5
+
6
+ import createStyles from './styles';
7
+
8
+ // Hero visual compartilhado pelas telas de integração.
9
+ export default function IntegrationHero({
10
+ shadowStyle,
11
+ accentColor,
12
+ eyebrow,
13
+ title,
14
+ description,
15
+ logo,
16
+ iconName = 'layers',
17
+ palette = colors,
18
+ }) {
19
+ const styles = createStyles(palette);
20
+
21
+ return (
22
+ <View
23
+ style={[
24
+ styles.card,
25
+ shadowStyle,
26
+ { backgroundColor: palette.actionBackground || accentColor },
27
+ ]}
28
+ >
29
+ <View style={styles.copy}>
30
+ <Text style={styles.eyebrow}>{eyebrow}</Text>
31
+ <Text style={styles.title}>{title}</Text>
32
+ <Text style={styles.description}>{description}</Text>
33
+ </View>
34
+
35
+ <View style={styles.badge}>
36
+ {logo ? (
37
+ <Image source={logo} style={styles.logo} resizeMode="contain" />
38
+ ) : (
39
+ <Icon
40
+ name={iconName}
41
+ size={20}
42
+ color={palette.cardIconColor || palette.iconColor || accentColor}
43
+ />
44
+ )}
45
+ </View>
46
+ </View>
47
+ );
48
+ }
@@ -0,0 +1,57 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import {colors} from '@controleonline/../../src/styles/colors';
3
+ import {withOpacity} from '@controleonline/../../src/styles/branding';
4
+
5
+ const createStyles = (palette = colors) =>
6
+ StyleSheet.create({
7
+ card: {
8
+ borderRadius: 24,
9
+ padding: 22,
10
+ marginBottom: 20,
11
+ overflow: 'hidden',
12
+ flexDirection: 'row',
13
+ justifyContent: 'space-between',
14
+ alignItems: 'flex-start',
15
+ },
16
+ copy: {
17
+ flex: 1,
18
+ paddingRight: 16,
19
+ },
20
+ eyebrow: {
21
+ fontSize: 11,
22
+ fontWeight: '800',
23
+ color: withOpacity(palette.actionText || palette.white, 0.72),
24
+ textTransform: 'uppercase',
25
+ letterSpacing: 1.2,
26
+ },
27
+ title: {
28
+ marginTop: 10,
29
+ fontSize: 28,
30
+ fontWeight: '800',
31
+ color: palette.actionText || palette.white,
32
+ letterSpacing: -0.6,
33
+ },
34
+ description: {
35
+ marginTop: 10,
36
+ fontSize: 14,
37
+ lineHeight: 22,
38
+ color: withOpacity(palette.actionText || palette.white, 0.86),
39
+ },
40
+ badge: {
41
+ position: 'absolute',
42
+ top: 18,
43
+ right: 18,
44
+ width: 48,
45
+ height: 48,
46
+ borderRadius: 16,
47
+ backgroundColor: palette.cardIconBackground || palette.iconBackground || palette.white,
48
+ alignItems: 'center',
49
+ justifyContent: 'center',
50
+ },
51
+ logo: {
52
+ width: 24,
53
+ height: 24,
54
+ },
55
+ });
56
+
57
+ export default createStyles;