@controleonline/ui-common 1.2.71 → 1.2.76

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 +164 -104
  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 +2865 -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,23 +1,32 @@
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 {
6
12
  DEVICE_RUNTIME_DEBUG_INFO_ENABLED_KEY,
7
13
  isTruthyValue,
8
- parseConfigsObject,
9
- } from '@controleonline/ui-common/src/react/config/deviceConfigBootstrap';
10
- 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;
20
-
14
+ parseConfigsObject,
15
+ } from '@controleonline/ui-common/src/react/config/deviceConfigBootstrap';
16
+ import {
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 = 4000;
26
+ const FADE_DURATION_MS = 260;
27
+ const COMPACT_BREAKPOINT = 720;
28
+ const MAX_INLINE_TEXT_LENGTH = 84;
29
+
21
30
  const RuntimeInfoFooter = ({
22
31
  appVersion,
23
32
  defaultCompany,
@@ -28,85 +37,97 @@ 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');
34
44
  const deviceConfigItem = deviceConfigStore?.getters?.item || {};
35
45
  const runtimeDebugSummary = runtimeDebugStore?.getters?.summary || {};
36
-
37
- const footerDebugInfo = useMemo(
38
- () =>
39
- getRuntimeFooterDebugInfo({
40
- device,
41
- appVersion,
42
- deviceConfig: deviceConfigItem,
43
- }),
44
- [appVersion, device, deviceConfigItem],
45
- );
46
- const primaryText = useMemo(
47
- () =>
48
- footerDebugInfo.primaryText ||
49
- getRuntimeFooterPrimaryText({
50
- device,
51
- appVersion,
52
- deviceConfig: deviceConfigItem,
53
- }),
54
- [appVersion, device, deviceConfigItem, footerDebugInfo.primaryText],
55
- );
56
- const companyFooterText = useMemo(
57
- () => getRuntimeFooterText(defaultCompany),
58
- [defaultCompany?.configs],
59
- );
60
- const deviceConfigs = useMemo(
61
- () => parseConfigsObject(deviceConfigItem?.configs),
62
- [deviceConfigItem?.configs],
63
- );
64
- const showDebugInfo = useMemo(
65
- () =>
66
- isTruthyValue(
67
- deviceConfigs?.[DEVICE_RUNTIME_DEBUG_INFO_ENABLED_KEY],
68
- ),
69
- [deviceConfigs],
70
- );
71
-
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);
81
- const footerEntries = useMemo(
82
- () =>
83
- Object.values(runtimeDebugSummary?.entries || {})
84
- .filter(entry => entry && Array.isArray(entry.lines) && entry.lines.length > 0)
85
- .sort((left, right) => {
86
- const leftOrder = Number(left?.order || 100);
87
- const rightOrder = Number(right?.order || 100);
88
- if (leftOrder !== rightOrder) {
89
- return leftOrder - rightOrder;
90
- }
91
-
92
- const leftTime = Date.parse(left?.updatedAt || '');
93
- const rightTime = Date.parse(right?.updatedAt || '');
94
- if (Number.isFinite(leftTime) && Number.isFinite(rightTime)) {
95
- return rightTime - leftTime;
96
- }
97
-
98
- if (Number.isFinite(rightTime)) {
99
- return 1;
100
- }
101
-
102
- if (Number.isFinite(leftTime)) {
103
- return -1;
104
- }
105
-
106
- return 0;
107
- }),
108
- [runtimeDebugSummary?.entries],
109
- );
46
+
47
+ const footerDebugInfo = useMemo(
48
+ () =>
49
+ getRuntimeFooterDebugInfo({
50
+ device,
51
+ appVersion,
52
+ deviceConfig: deviceConfigItem,
53
+ }),
54
+ [appVersion, device, deviceConfigItem],
55
+ );
56
+ const primaryText = useMemo(
57
+ () =>
58
+ footerDebugInfo.primaryText ||
59
+ getRuntimeFooterPrimaryText({
60
+ device,
61
+ appVersion,
62
+ deviceConfig: deviceConfigItem,
63
+ }),
64
+ [appVersion, device, deviceConfigItem, footerDebugInfo.primaryText],
65
+ );
66
+ const companyFooterText = useMemo(
67
+ () => getRuntimeFooterText(defaultCompany),
68
+ [defaultCompany?.configs],
69
+ );
70
+ const footerTextLines = useMemo(
71
+ () => getRuntimeFooterTextLines(companyFooterText),
72
+ [companyFooterText],
73
+ );
74
+ const deviceConfigs = useMemo(
75
+ () => parseConfigsObject(deviceConfigItem?.configs),
76
+ [deviceConfigItem?.configs],
77
+ );
78
+ const showDebugInfo = useMemo(
79
+ () =>
80
+ isTruthyValue(
81
+ deviceConfigs?.[DEVICE_RUNTIME_DEBUG_INFO_ENABLED_KEY],
82
+ ),
83
+ [deviceConfigs],
84
+ );
85
+
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
+ // Always rotate when there is more than one entry (e.g. 1 footer line + primaryText).
99
+ // Do not fall back to concatenated inlineText in that case.
100
+ const shouldRotate =
101
+ !showDebugInfo && rotationEntries.length > 1;
102
+ const footerEntries = useMemo(
103
+ () =>
104
+ Object.values(runtimeDebugSummary?.entries || {})
105
+ .filter(entry => entry && Array.isArray(entry.lines) && entry.lines.length > 0)
106
+ .sort((left, right) => {
107
+ const leftOrder = Number(left?.order || 100);
108
+ const rightOrder = Number(right?.order || 100);
109
+ if (leftOrder !== rightOrder) {
110
+ return leftOrder - rightOrder;
111
+ }
112
+
113
+ const leftTime = Date.parse(left?.updatedAt || '');
114
+ const rightTime = Date.parse(right?.updatedAt || '');
115
+ if (Number.isFinite(leftTime) && Number.isFinite(rightTime)) {
116
+ return rightTime - leftTime;
117
+ }
118
+
119
+ if (Number.isFinite(rightTime)) {
120
+ return 1;
121
+ }
122
+
123
+ if (Number.isFinite(leftTime)) {
124
+ return -1;
125
+ }
126
+
127
+ return 0;
128
+ }),
129
+ [runtimeDebugSummary?.entries],
130
+ );
110
131
  const socketEntry = useMemo(
111
132
  () => footerEntries.find(entry => entry?.key === 'socket') || null,
112
133
  [footerEntries],
@@ -145,27 +166,63 @@ const RuntimeInfoFooter = ({
145
166
  [allStores],
146
167
  );
147
168
  const bottomInset = Math.max(Number(insets.bottom) || 0, 16);
148
-
169
+
149
170
  useEffect(() => {
150
- if (!shouldRotate || entries.length <= 1) {
171
+ if (!shouldRotate || rotationEntries.length <= 1) {
151
172
  setActiveIndex(0);
173
+ fadeOpacity.stopAnimation();
174
+ fadeOpacity.setValue(1);
152
175
  return;
153
176
  }
154
-
155
- const intervalId = setInterval(() => {
156
- setActiveIndex(current => (current + 1) % entries.length);
157
- }, ROTATION_INTERVAL_MS);
158
-
177
+
178
+ let timeoutId;
179
+ let cancelled = false;
180
+
181
+ const scheduleTransition = () => {
182
+ timeoutId = setTimeout(() => {
183
+ Animated.timing(fadeOpacity, {
184
+ toValue: 0,
185
+ duration: FADE_DURATION_MS,
186
+ useNativeDriver: true,
187
+ }).start(({finished}) => {
188
+ if (!finished || cancelled) {
189
+ return;
190
+ }
191
+
192
+ setActiveIndex(current => (current + 1) % rotationEntries.length);
193
+
194
+ Animated.timing(fadeOpacity, {
195
+ toValue: 1,
196
+ duration: FADE_DURATION_MS,
197
+ useNativeDriver: true,
198
+ }).start(({finished: fadeInFinished}) => {
199
+ if (fadeInFinished && !cancelled) {
200
+ scheduleTransition();
201
+ }
202
+ });
203
+ });
204
+ }, ROTATION_INTERVAL_MS);
205
+ };
206
+
207
+ fadeOpacity.setValue(1);
208
+ scheduleTransition();
209
+
159
210
  return () => {
160
- clearInterval(intervalId);
211
+ cancelled = true;
212
+ if (timeoutId) {
213
+ clearTimeout(timeoutId);
214
+ }
215
+ fadeOpacity.stopAnimation();
161
216
  };
162
- }, [entries.length, shouldRotate]);
217
+ }, [fadeOpacity, rotationEntries.length, shouldRotate]);
163
218
 
164
- if (entries.length === 0 && !showDebugInfo) {
219
+ if (rotationEntries.length === 0 && !showDebugInfo) {
165
220
  return null;
166
221
  }
167
222
 
168
- const displayedText = shouldRotate ? entries[activeIndex] : inlineText;
223
+ const displayedText = shouldRotate
224
+ ? rotationEntries[activeIndex]
225
+ : inlineText;
169
226
  const backgroundColor = colors?.footerBackground;
170
227
  const borderColor = colors?.footerBorder;
171
228
  const textColor = colors?.footerText;
@@ -199,18 +256,21 @@ const RuntimeInfoFooter = ({
199
256
  ]}
200
257
  />
201
258
  </View>
202
- <Text
259
+ <Animated.Text
203
260
  numberOfLines={1}
204
261
  ellipsizeMode="tail"
205
262
  minimumFontScale={0.85}
206
263
  style={[
207
264
  styles.primaryText,
265
+ {
266
+ opacity: showDebugInfo ? 1 : fadeOpacity,
267
+ },
208
268
  {
209
269
  color: textColor,
210
270
  },
211
271
  ]}>
212
272
  {showDebugInfo ? inlineText || primaryText || device?.id || '--' : displayedText}
213
- </Text>
273
+ </Animated.Text>
214
274
  <View style={styles.loadingWrap}>
215
275
  {hasStoreLoading && (
216
276
  <ActivityIndicator color={loadingColor} size="small" />
@@ -238,5 +298,5 @@ const RuntimeInfoFooter = ({
238
298
  </View>
239
299
  );
240
300
  };
241
-
242
- export default RuntimeInfoFooter;
301
+
302
+ export default RuntimeInfoFooter;
@@ -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;