@controleonline/ui-common 1.2.99 → 1.2.101

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.
@@ -1,10 +1,10 @@
1
1
  {
2
- "version": "1.2.98",
3
- "rc": 2,
4
- "branch": "rc/1.2.98-rc.2",
5
- "baseMaster": "aa5ad406d201d23bb88023bc4b7a7d4f9b11fd0a",
6
- "tasks": [88],
7
- "repositories": { "ControleOnline/ui-common": "0c96fc6ebac3af856cfba103fa0694171f590ce1" },
8
- "compositionNotes": ["Task 88: rodapé mostra …últimos 2 blocos quando o identificador não é IPv4. rc.2 inclui bump de versão 1.2.98 para publicação npm; supersedes rc.1."],
2
+ "version": "1.2.93",
3
+ "rc": 1,
4
+ "branch": "rc/1.2.93-rc.1",
5
+ "baseMaster": "b4db94eb1ccd3bf878dc96e3d32c0249aad07402",
6
+ "tasks": [77],
7
+ "repositories": { "ControleOnline/ui-common": "4dfdb2d7ccb160f265115bdad3125d6ecd26bdfd" },
8
+ "compositionNotes": ["Verified on Android emulator: Text width was still 0 with flexBasis auto because Yoga prioritises flex over flexGrow/flexBasis. flex:0 fixes it."],
9
9
  "frozen": true
10
10
  }
@@ -0,0 +1,16 @@
1
+ # Homepage ADMIN/MANAGER — app-community#1056
2
+
3
+ Soft appearance is opt-in for HomePage only. Existing menu routes/parameters, permissions, stores and translations are preserved. App-provided homepage colors and components are used only in this appearance. Default consumers retain their current behavior.
4
+
5
+ Source: https://github.com/ControleOnline/app-community/issues/1056
6
+ Local evidence: 40 homepage tests and 34 runtime footer/zoom tests passed. No production authorization.
7
+
8
+ ## Web login translation bootstrap
9
+
10
+ Keep the navigation tree mounted while translations initialize. Hide the shell and disable pointer/accessibility interaction until the existing translation readiness contract succeeds. Unmounting navigation during login resets its route and repeatedly mounts/registers the bottom bar, causing React maximum update depth. No route, permission, translation source, theme or zoom changes. Native behavior is unchanged.
11
+
12
+ Regression evidence: app-community homepage bootstrapNavigation.test.js reproduced maximum update depth before the fix, then passed after it. 44 tests across 9 homepage suites passed. Runtime Dev reproduced the unfixed bug; new runtime verification is required after consuming ui-common 1.2.96.
13
+
14
+ Release 1.2.96 uses the task-only c91510d source plus version metadata. The integration merge into dev preserves independent team work, but those changes are excluded from this homepage package. Publication uses the existing tag-triggered trusted publishing workflow.
15
+
16
+ Release 1.2.97 completes the web bootstrap gate with visibility:hidden and aria-hidden: pending descendants cannot receive keyboard focus while navigation stays mounted. Native behavior remains unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@controleonline/ui-common",
3
- "version": "1.2.99",
3
+ "version": "1.2.101",
4
4
  "author": "ControleOnline",
5
5
  "description": "ControleOnline Common Quasar UI Component",
6
6
  "license": "MIT",
package/src/api/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import {readStoredSessionToken} from '@controleonline/ui-common/src/utils/storedSessionToken';
2
1
  import myFetch from '@controleonline/ui-common/src/api/fetch';
3
2
  import axios from 'axios';
4
3
  import {env as APP_ENV} from '@env';
@@ -216,8 +215,8 @@ async function requestSmokeUrl(url, options) {
216
215
  export const api = {
217
216
  device: JSON.parse(localStorage.getItem('device') || '{}'),
218
217
  masterDevice: JSON.parse(localStorage.getItem('master-device') || '{}'),
219
-
220
-
218
+
219
+
221
220
  upload: async function (uri, formData) {
222
221
  const token = await this.getToken();
223
222
  const appDomain = resolveAppDomain(APP_ENV.DOMAIN);
@@ -243,8 +242,8 @@ export const api = {
243
242
  fetch: async function (uri, options = {}) {
244
243
  if (typeof options.headers === 'undefined')
245
244
  Object.assign(options, { headers: new Headers() });
246
-
247
- // Always refresh device from localStorage in case it was set after module load
245
+
246
+ // Always refresh device from localStorage in case it was set after module load
248
247
  try {
249
248
  this.device = JSON.parse(localStorage.getItem('device') || '{}');
250
249
  } catch (e) {
@@ -271,16 +270,16 @@ export const api = {
271
270
  if (options.body) {
272
271
  if (typeof options.body === 'object') {
273
272
  bodyDeviceId = options.body.device || null;
274
- } else if (typeof options.body === 'string') {
275
- try {
276
- const parsedBody = JSON.parse(options.body);
277
- bodyDeviceId = parsedBody?.device || null;
278
- } catch (e) {
279
- bodyDeviceId = null;
280
- }
281
- }
282
- }
283
-
273
+ } else if (typeof options.body === 'string') {
274
+ try {
275
+ const parsedBody = JSON.parse(options.body);
276
+ bodyDeviceId = parsedBody?.device || null;
277
+ } catch (e) {
278
+ bodyDeviceId = null;
279
+ }
280
+ }
281
+ }
282
+
284
283
  const requestDevice = resolveRequestDevice(
285
284
  this.masterDevice,
286
285
  this.device,
@@ -297,10 +296,10 @@ export const api = {
297
296
  options.headers.set('DEVICE-TYPE', requestDeviceType);
298
297
  }
299
298
  }
300
- if (options.responseType != 'text') {
301
- options.headers.set('Content-Type', MIME_TYPE);
302
- options.headers.set('Accept', MIME_TYPE);
303
- }
299
+ if (options.responseType != 'text') {
300
+ options.headers.set('Content-Type', MIME_TYPE);
301
+ options.headers.set('Accept', MIME_TYPE);
302
+ }
304
303
  options.headers.set('App-Domain', resolveAppDomain(APP_ENV.DOMAIN));
305
304
 
306
305
  if (options.body && typeof options.body != 'string') {
@@ -311,13 +310,28 @@ export const api = {
311
310
  if (options.params) {
312
311
  uri = this.buildQueryString(uri, options);
313
312
  }
314
- return myFetch(uri, options).catch(e => {
315
- throw e;
316
- });
317
- },
318
- async getToken() {
319
- return readStoredSessionToken();
320
- },
313
+ return myFetch(uri, options).catch(e => {
314
+ throw e;
315
+ });
316
+ },
317
+ async getToken() {
318
+ const sessionString = localStorage.getItem('session');
319
+ let session = null;
320
+ if (sessionString) {
321
+ try {
322
+ const cleanString =
323
+ typeof sessionString == 'string' &&
324
+ sessionString.startsWith('__q_objt|')
325
+ ? sessionString.substring('__q_objt|'.length)
326
+ : sessionString;
327
+ session = JSON.parse(cleanString); // Transforma a string em objeto
328
+ } catch (e) {
329
+ console.error('Failed to parse session from localStorage', e);
330
+ }
331
+ }
332
+
333
+ return session?.token || session?.api_key;
334
+ },
321
335
  serialize(obj, prefix = '') {
322
336
  const pairs = [];
323
337
 
@@ -330,12 +344,12 @@ export const api = {
330
344
 
331
345
  const value = obj[key];
332
346
  let fullKey = prefix ? `${prefix}.${key}` : key; // Usa ponto para objetos
333
-
334
- if (value === null || value === undefined) {
335
- pairs.push(`${fullKey}=`);
336
- } else if (typeof value === 'object' && !Array.isArray(value)) {
337
- // Objeto aninhado: chama serialize recursivamente com ponto
338
- pairs.push(...this.serialize(value, fullKey));
347
+
348
+ if (value === null || value === undefined) {
349
+ pairs.push(`${fullKey}=`);
350
+ } else if (typeof value === 'object' && !Array.isArray(value)) {
351
+ // Objeto aninhado: chama serialize recursivamente com ponto
352
+ pairs.push(...this.serialize(value, fullKey));
339
353
  } else if (Array.isArray(value)) {
340
354
  // Array: adiciona [] e itera sobre os valores
341
355
  if (value.length === 0) {
@@ -350,14 +364,14 @@ export const api = {
350
364
  });
351
365
  }
352
366
  } else {
353
- // Valor simples
354
- pairs.push(`${fullKey}=${encodeURIComponent(value)}`);
355
- }
356
- }
357
- }
358
- return pairs;
359
- },
360
-
367
+ // Valor simples
368
+ pairs.push(`${fullKey}=${encodeURIComponent(value)}`);
369
+ }
370
+ }
371
+ }
372
+ return pairs;
373
+ },
374
+
361
375
  buildQueryString(uri, options) {
362
376
  const safeUri = stripDomainQueryParams(uri);
363
377
 
@@ -38,8 +38,6 @@ export default function AnimatedModal({
38
38
  onRequestClose,
39
39
  children,
40
40
  style,
41
- overlayColor,
42
- contentStyle,
43
41
  }) {
44
42
  const [showing, setShowing] = useState(visible);
45
43
  const [keyboardHeight, setKeyboardHeight] = useState(0);
@@ -127,8 +125,6 @@ export default function AnimatedModal({
127
125
  const overlayStyle = [
128
126
  styles.overlay,
129
127
  style,
130
- overlayColor,
131
- contentStyle,
132
128
  Platform.OS === 'android' ? {paddingBottom: keyboardHeight} : null,
133
129
  ];
134
130
 
@@ -156,12 +152,11 @@ export default function AnimatedModal({
156
152
  style={[
157
153
  StyleSheet.absoluteFillObject,
158
154
  styles.overlayBg,
159
- overlayColor ? {backgroundColor: overlayColor} : null,
160
155
  {opacity: overlayOpacity},
161
156
  ]}
162
157
  />
163
158
  <Animated.View
164
- style={[styles.content, contentStyle, {transform: [{translateY: slideY}]}]}>
159
+ style={[styles.content, {transform: [{translateY: slideY}]}]}>
165
160
  {normalizedChildren}
166
161
  </Animated.View>
167
162
  <Toasts
@@ -17,8 +17,7 @@ const BottomNavigationBar = ({
17
17
  colors = {},
18
18
  testID = 'bottom-navigation',
19
19
  useModernWebChromeProps = false,
20
- appearance = 'soft',
21
- placement = 'overlay',
20
+ appearance = 'legacy',
22
21
  }) => {
23
22
  const Icon = appearance === 'soft' ? HomeIcon : LegacyIcon;
24
23
  colors = appearance === 'soft' ? resolveHomepagePalette(colors) : colors;
@@ -105,7 +104,9 @@ const BottomNavigationBar = ({
105
104
 
106
105
  const routeItems = Array.isArray(items) ? items : [];
107
106
  const knownRoute = routeItems.some(item => item?.route === activeRouteName);
108
- const effectiveActiveRoute = knownRoute ? activeRouteName : '';
107
+ const effectiveActiveRoute = knownRoute
108
+ ? activeRouteName
109
+ : routeItems[0]?.route || '';
109
110
 
110
111
  const navigateTo = item => {
111
112
  try {
@@ -133,7 +134,7 @@ const BottomNavigationBar = ({
133
134
  };
134
135
 
135
136
  return (
136
- <View {...hostProps} style={[hostStyle, placement === 'inline' ? {position: 'relative', flexShrink: 0, zIndex: 0} : null]}>
137
+ <View {...hostProps} style={hostStyle}>
137
138
  <View style={styles.stack}>
138
139
  <View style={styles.dock} testID={testID}>
139
140
  <View style={styles.itemsRow}>
@@ -150,8 +151,6 @@ const BottomNavigationBar = ({
150
151
  <Pressable
151
152
  key={item.route}
152
153
  accessibilityRole="button"
153
- accessibilityLabel={item.label}
154
- accessibilityState={{selected: isActive, disabled: Boolean(isDisabled)}}
155
154
  disabled={isDisabled}
156
155
  onPress={() => navigateTo(item)}
157
156
  style={({pressed}) => [
@@ -1,6 +1,6 @@
1
1
  import {Platform} from 'react-native';
2
2
 
3
- // Shared compact chrome on every route; explicit legacy consumers can opt out.
3
+ // Only the homepage opts into this chrome; preserve all default navigation styles.
4
4
  export default (styles, colors, appearance) => appearance !== 'soft' ? styles : {
5
5
  ...styles,
6
6
  dock: [styles.dock, {
@@ -16,10 +16,9 @@ export default (styles, colors, appearance) => appearance !== 'soft' ? styles :
16
16
  shadowOpacity: 0,
17
17
  ...(Platform.OS === 'web' ? {boxShadow: 'none'} : {}),
18
18
  }],
19
- itemsRow: [styles.itemsRow, {minHeight: 56, paddingHorizontal: 8, paddingVertical: 4}],
20
- item: [styles.item, {minHeight: 46, paddingVertical: 3, borderRadius: 10}],
21
- iconWrap: [styles.iconWrap, {width: 22, height: 22, marginBottom: 2}],
22
- itemLabel: [styles.itemLabel, {fontSize: 11, lineHeight: 14, fontWeight: '400'}],
19
+ itemsRow: [styles.itemsRow, {minHeight: 64, paddingHorizontal: 12, paddingVertical: 5}],
20
+ item: [styles.item, {minHeight: 54, borderRadius: 12}],
21
+ itemLabel: [styles.itemLabel, {fontSize: 11, fontWeight: '400'}],
23
22
  footerSlot: [styles.footerSlot, {
24
23
  borderTopWidth: 1,
25
24
  borderColor: colors.navigationBorder,
@@ -1,53 +1,45 @@
1
- import React, {useEffect} from 'react';
2
- import {useStore} from '@store';
3
- import {invalidateProductCatalogCache} from '../utils/productCatalogCacheRevision';
4
-
5
- const ProductCatalogCacheService = () => {
6
- const categoriesStore = useStore('categories');
7
- const productsStore = useStore('products');
8
-
9
- const categoryActions = categoriesStore.actions;
10
- const productActions = productsStore.actions;
11
-
12
- const {messages, message} = categoriesStore.getters;
13
-
14
- useEffect(() => {
15
- if (
16
- messages &&
17
- messages.length > 0 &&
18
- (!message || Object.keys(message).length === 0)
19
- ) {
20
- const nextMessages = [...messages];
21
- categoryActions.setMessage(nextMessages.pop());
22
- categoryActions.setMessages(nextMessages);
23
- }
24
- }, [categoryActions, message, messages]);
25
-
26
- useEffect(() => {
27
- if (!message || Object.keys(message).length === 0) {
28
- return;
29
- }
30
-
31
- if (message?.command !== 'clear-product-cache') {
32
- categoryActions.setMessage(null);
33
- return;
34
- }
35
-
36
- invalidateProductCatalogCache();
37
- for (const context of ['products', 'supplies']) {
38
- localStorage.removeItem(`categories:${context}`);
39
- localStorage.removeItem(`categories-company:${context}`);
40
- }
41
- localStorage.setItem('categories', JSON.stringify([]));
42
- localStorage.removeItem('categories-company');
43
- categoryActions.setItems([]);
44
- productActions.setItems([]);
45
- categoryActions.setReload?.(true);
46
- productActions.setReload?.(true);
47
- categoryActions.setMessage(null);
48
- }, [categoryActions, message, productActions]);
49
-
50
- return null;
51
- };
52
-
53
- export default ProductCatalogCacheService;
1
+ import React, {useEffect} from 'react';
2
+ import {useStore} from '@store';
3
+
4
+ const ProductCatalogCacheService = () => {
5
+ const categoriesStore = useStore('categories');
6
+ const productsStore = useStore('products');
7
+
8
+ const categoryActions = categoriesStore.actions;
9
+ const productActions = productsStore.actions;
10
+
11
+ const {messages, message} = categoriesStore.getters;
12
+
13
+ useEffect(() => {
14
+ if (
15
+ messages &&
16
+ messages.length > 0 &&
17
+ (!message || Object.keys(message).length === 0)
18
+ ) {
19
+ const nextMessages = [...messages];
20
+ categoryActions.setMessage(nextMessages.pop());
21
+ categoryActions.setMessages(nextMessages);
22
+ }
23
+ }, [categoryActions, message, messages]);
24
+
25
+ useEffect(() => {
26
+ if (!message || Object.keys(message).length === 0) {
27
+ return;
28
+ }
29
+
30
+ if (message?.command !== 'clear-product-cache') {
31
+ categoryActions.setMessage(null);
32
+ return;
33
+ }
34
+
35
+ localStorage.setItem('categories', JSON.stringify([]));
36
+ localStorage.removeItem('categories-company');
37
+ categoryActions.setItems([]);
38
+ productActions.setItems([]);
39
+ categoryActions.setMessage(null);
40
+ }, [categoryActions, message, productActions]);
41
+
42
+ return null;
43
+ };
44
+
45
+ export default ProductCatalogCacheService;
@@ -19,8 +19,7 @@ const {
19
19
  } = require('@controleonline/ui-common/src/react/utils/bottomNavigationToolbar');
20
20
 
21
21
  const RuntimeBottomNavigationBar = ({
22
- appearance = 'soft',
23
- placement = 'overlay',
22
+ appearance = 'legacy',
24
23
  activeRouteName: activeRouteNameProp,
25
24
  colors: colorsOverride,
26
25
  disabled: disabledOverride,
@@ -43,16 +42,13 @@ const RuntimeBottomNavigationBar = ({
43
42
  const peopleGetters = peopleStore?.getters || {};
44
43
  const currentCompany = peopleGetters.currentCompany || {};
45
44
  const themeColors = themeGetters.colors || {};
46
- const [menuState, setMenuState] = useState({scope: '', menus: []});
45
+ const [runtimeMenus, setRuntimeMenus] = useState([]);
47
46
  const preset = presetKey ? getBottomNavigationPreset(presetKey) : null;
48
47
  const routeAliases = routeAliasesProp || preset?.routeAliases || {};
49
48
  const appType = normalizeAppType(app_type);
50
- const menuScope = JSON.stringify([appType, currentCompany?.id, menuType, presetKey]);
51
- const runtimeMenus = menuState.scope === menuScope ? menuState.menus : [];
52
49
 
53
50
  // Always consult menus-people for role-aware toolbar; preset is fallback only.
54
51
  useEffect(() => {
55
- const setRuntimeMenus = menus => setMenuState({scope: menuScope, menus});
56
52
  if (!currentCompany?.id || !menuType) {
57
53
  setRuntimeMenus(
58
54
  resolveToolbarRuntimeMenus({
@@ -109,7 +105,7 @@ const RuntimeBottomNavigationBar = ({
109
105
  return () => {
110
106
  cancelled = true;
111
107
  };
112
- }, [appType, currentCompany?.id, menuType, preset, presetKey, menuScope]);
108
+ }, [appType, currentCompany?.id, menuType, preset, presetKey]);
113
109
 
114
110
  const colors = useMemo(() => {
115
111
  if (colorsOverride) {
@@ -155,19 +151,16 @@ const RuntimeBottomNavigationBar = ({
155
151
  const knownRoute = navItems.some(item => item.route === resolvedActiveRoute);
156
152
  const effectiveActiveRoute = knownRoute
157
153
  ? resolvedActiveRoute
158
- : activeRouteName;
154
+ : navItems[0]?.route || activeRouteName;
159
155
 
160
156
  const isDisabled =
161
157
  typeof disabledOverride === 'boolean'
162
158
  ? disabledOverride
163
159
  : !currentCompany || Object.keys(currentCompany).length === 0;
164
160
 
165
- if (!navItems.length) return null;
166
-
167
161
  return (
168
162
  <BottomNavigationBar
169
163
  appearance={appearance}
170
- placement={placement}
171
164
  activeRouteName={effectiveActiveRoute}
172
165
  colors={colors}
173
166
  disabled={isDisabled}
@@ -1,4 +1,3 @@
1
- import {readStoredSessionToken} from '../../utils/storedSessionToken';
2
1
  import React, {useEffect, useMemo, useState} from 'react';
3
2
  import {Image, Platform, StyleSheet, Text, View} from 'react-native';
4
3
  import {env as APP_ENV} from '@env';
@@ -15,6 +14,18 @@ const isBackendDownloadUrl = uri =>
15
14
  /** Session cache: gravatar URL → true (ok) | false (missing). Avoids repeat probes. */
16
15
  const gravatarProbeCache = new Map();
17
16
 
17
+ const readSessionToken = () => {
18
+ if (typeof localStorage === 'undefined' || !localStorage?.getItem) {
19
+ return '';
20
+ }
21
+
22
+ try {
23
+ const session = JSON.parse(localStorage.getItem('session') || '{}');
24
+ return normalizeUrl(session?.api_key || session?.token);
25
+ } catch {
26
+ return '';
27
+ }
28
+ };
18
29
 
19
30
  /**
20
31
  * Probe Gravatar (d=404) via fetch BEFORE exposing URL to <Image>.
@@ -75,7 +86,7 @@ const useDisplayUri = uri => {
75
86
  return;
76
87
  }
77
88
 
78
- const token = readStoredSessionToken();
89
+ const token = readSessionToken();
79
90
 
80
91
  // Authenticated backend download on web
81
92
  if (isBackendDownloadUrl(next) && Platform.OS === 'web' && token) {
@@ -24,7 +24,7 @@ import {
24
24
  parseConfigsObject,
25
25
  tt,
26
26
  } from './deviceListHelpers';
27
- import legacyStyles from '../../Devices.styles';
27
+ import styles from '../../Devices.styles';
28
28
 
29
29
  /**
30
30
  * Normalize DefaultTable card `item` into a device group shape.
@@ -65,7 +65,6 @@ const normalizeDeviceGroup = item => {
65
65
  export default function DeviceGroupCard({
66
66
  item,
67
67
  brandColors,
68
- themedStyles = null,
69
68
  creatingPdv,
70
69
  goToDetail,
71
70
  handleCreateCurrentPdv,
@@ -75,7 +74,6 @@ export default function DeviceGroupCard({
75
74
  runtimeDeviceType,
76
75
  showCurrentPdvSetup,
77
76
  }) {
78
- const styles = themedStyles || legacyStyles;
79
77
  const deviceGroup = normalizeDeviceGroup(item);
80
78
  if (!deviceGroup) {
81
79
  return null;
@@ -118,7 +116,7 @@ export default function DeviceGroupCard({
118
116
  deviceGroup.device?.id ||
119
117
  deviceGroup.key,
120
118
  );
121
- const accent = brandColors.deviceAccent || getDeviceTypeAccent(normalizedType);
119
+ const accent = getDeviceTypeAccent(normalizedType);
122
120
  const metaChips = [];
123
121
 
124
122
  if (isPdv && primaryConfig) {
@@ -198,7 +196,7 @@ export default function DeviceGroupCard({
198
196
  <View style={styles.deviceConfigRow}>
199
197
  {deviceConfigs.map(deviceConfig => {
200
198
  const configType = getDeviceConfigType(deviceConfig);
201
- const configAccent = brandColors.deviceAccent || getDeviceTypeAccent(configType);
199
+ const configAccent = getDeviceTypeAccent(configType);
202
200
  const isSessionConfig =
203
201
  isCurrentDevice &&
204
202
  runtimeDeviceType &&
@@ -299,5 +297,5 @@ export default function DeviceGroupCard({
299
297
  </View>
300
298
  </View>
301
299
  );
302
-
300
+
303
301
  }
@@ -36,7 +36,7 @@ import {
36
36
  prioritizeCurrentDeviceGroups,
37
37
  readStoredRuntimeDevice,
38
38
  } from '../currentDevice';
39
- import legacyStyles, {createStyles} from '../../Devices.styles';
39
+ import styles from '../../Devices.styles';
40
40
  import DeviceGroupCard from './DeviceGroupCard';
41
41
  import {useDeviceNetworkConnectivity} from './useDeviceNetworkConnectivity';
42
42
  import {
@@ -89,7 +89,7 @@ export const createDeviceTypeTab = ({
89
89
  clientFilter = null,
90
90
  offerCurrentPdvSetup = false,
91
91
  }) => {
92
- const DeviceTypeTab = ({themePalette = null} = {}) => {
92
+ const DeviceTypeTab = () => {
93
93
  const navigation = useNavigation();
94
94
  const peopleStore = useStore('people');
95
95
  const deviceStore = useStore('device');
@@ -101,15 +101,13 @@ export const createDeviceTypeTab = ({
101
101
 
102
102
  const brandColors = useMemo(
103
103
  () =>
104
- themePalette || resolveThemePalette(
104
+ resolveThemePalette(
105
105
  {...themeColors, ...(currentCompany?.theme?.colors || {})},
106
106
  colors,
107
107
  ),
108
- [themePalette, themeColors, currentCompany?.id],
108
+ [themeColors, currentCompany?.id],
109
109
  );
110
110
 
111
- const styles = useMemo(() => themePalette ? createStyles(brandColors) : legacyStyles, [themePalette, brandColors]);
112
-
113
111
  const [deviceConfigs, setDeviceConfigs] = useState([]);
114
112
  const [loading, setLoading] = useState(false);
115
113
  const [refreshing, setRefreshing] = useState(false);
@@ -374,7 +372,6 @@ export const createDeviceTypeTab = ({
374
372
  <DeviceGroupCard
375
373
  {...cardProps}
376
374
  brandColors={brandColors}
377
- themedStyles={themePalette ? styles : null}
378
375
  creatingPdv={creatingPdv}
379
376
  goToDetail={goToDetail}
380
377
  handleCreateCurrentPdv={handleCreateCurrentPdv}
@@ -387,8 +384,6 @@ export const createDeviceTypeTab = ({
387
384
  ),
388
385
  [
389
386
  brandColors,
390
- styles,
391
- themePalette,
392
387
  creatingPdv,
393
388
  goToDetail,
394
389
  handleCreateCurrentPdv,
@@ -1,184 +1,183 @@
1
- import React, {useEffect, useMemo, useState} from 'react';
2
- import {Text, TouchableOpacity, View} from 'react-native';
3
- import {SafeAreaView} from 'react-native-safe-area-context';
4
- import {useNavigation} from '@react-navigation/native';
5
- import {useStore} from '@store';
6
- import {resolveThemePalette} from '@controleonline/../../src/styles/branding';
7
- import {colors} from '@controleonline/../../src/styles/colors';
8
- import CompactFilterSelector from '@controleonline/ui-default/src/react/components/filters/CompactFilterSelector';
9
- import {PDV_DEVICE_TYPE} from '@controleonline/ui-common/src/react/utils/printerDevices';
10
- import Icon from 'react-native-vector-icons/Feather';
11
- import {
12
- ALL_DEVICE_FILTER,
13
- getDeviceCreationAction,
14
- getDeviceFilterAccent,
15
- getDeviceFilterDefinition,
16
- getDeviceFilterIcon,
17
- getDeviceFilterOptions,
18
- getPdvSubfilterOptions,
19
- hasRegisteredDeviceFilter,
20
- isPdvSubfilter,
21
- } from './Devices/deviceTypes';
22
- import { createStyles } from './Devices.styles';
23
-
24
- const tt = (type, key) => global.t?.t('configs', type, key);
25
-
26
- const Devices = ({themePalette = null} = {}) => {
27
- const navigation = useNavigation();
28
- const peopleStore = useStore('people');
29
- const themeStore = useStore('theme');
30
-
31
- const {currentCompany} = peopleStore.getters;
32
- const {colors: themeColors} = themeStore.getters;
33
-
34
- const [activeFilter, setActiveFilter] = useState(ALL_DEVICE_FILTER);
35
-
36
- const brandColors = useMemo(
37
- () =>
38
- themePalette || resolveThemePalette(
39
- {...themeColors, ...(currentCompany?.theme?.colors || {})},
40
- colors,
41
- ),
42
- [themePalette, themeColors, currentCompany?.id],
43
- );
44
- const styles = useMemo(() => createStyles(brandColors), [brandColors]);
45
- const accentPalette = useMemo(
46
- () => ({
47
- primary: brandColors.info,
48
- success: brandColors.success,
49
- danger: brandColors.error,
50
- warning: brandColors.warning,
51
- }),
52
- [brandColors.error, brandColors.info, brandColors.success, brandColors.warning],
53
- );
54
-
55
- const filterOptions = useMemo(() => getDeviceFilterOptions(), []);
56
- const pdvSubfilterOptions = useMemo(() => getPdvSubfilterOptions(), []);
57
- const activeFilterDefinition = useMemo(
58
- () => getDeviceFilterDefinition(activeFilter),
59
- [activeFilter],
60
- );
61
- const topLevelActiveFilter = useMemo(
62
- () => (isPdvSubfilter(activeFilter) ? PDV_DEVICE_TYPE : activeFilter),
63
- [activeFilter],
64
- );
65
- const activeFilterAction = useMemo(
66
- () => getDeviceCreationAction(topLevelActiveFilter),
67
- [topLevelActiveFilter],
68
- );
69
- const deviceTypeSelectorOptions = useMemo(
70
- () => filterOptions.map(option => ({
71
- key: option.key,
72
- label: option.label,
73
- })),
74
- [filterOptions],
75
- );
76
- const pdvGatewaySelectorOptions = useMemo(
77
- () => [
78
- {key: PDV_DEVICE_TYPE, label: tt('device_filter', 'all') || 'Todos'},
79
- ...pdvSubfilterOptions.map(option => ({
80
- key: option.key,
81
- label: option.label,
82
- })),
83
- ],
84
- [pdvSubfilterOptions],
85
- );
86
- const selectedDeviceTypeLabel = useMemo(
87
- () =>
88
- deviceTypeSelectorOptions.find(option => option.key === topLevelActiveFilter)?.label ||
89
- tt('device_filter', 'allTypes') ||
90
- 'Todos os tipos',
91
- [deviceTypeSelectorOptions, topLevelActiveFilter],
92
- );
93
- const selectedPdvGatewayLabel = useMemo(
94
- () =>
95
- pdvGatewaySelectorOptions.find(option => option.key === activeFilter)?.label ||
96
- tt('device_filter', 'all') ||
97
- 'Todos',
98
- [activeFilter, pdvGatewaySelectorOptions],
99
- );
100
- const ActiveTabComponent = activeFilterDefinition.TabComponent;
101
-
102
- useEffect(() => {
103
- if (!hasRegisteredDeviceFilter(activeFilter)) {
104
- setActiveFilter(ALL_DEVICE_FILTER);
105
- }
106
- }, [activeFilter]);
107
-
108
- return (
109
- <SafeAreaView
110
- style={[styles.container, {backgroundColor: brandColors.background}]}>
111
- <View style={styles.filtersBlock}>
112
- <Text style={styles.filtersLabel}>{tt('device_filter', 'filterByType') || 'Filtrar por tipo'}</Text>
113
- <CompactFilterSelector
114
- icon={getDeviceFilterIcon(topLevelActiveFilter)}
115
- label={selectedDeviceTypeLabel}
116
- title={tt('device_filter', 'filterByType') || 'Filtrar por tipo'}
117
- accentColor={getDeviceFilterAccent(topLevelActiveFilter, {
118
- brandColors,
119
- hex: accentPalette,
120
- })}
121
- active={topLevelActiveFilter !== ALL_DEVICE_FILTER}
122
- options={deviceTypeSelectorOptions}
123
- selectedKey={topLevelActiveFilter}
124
- onSelect={optionKey => {
125
- setActiveFilter(optionKey);
126
- return true;
127
- }}
128
- />
129
- </View>
130
-
131
- {topLevelActiveFilter === PDV_DEVICE_TYPE && pdvSubfilterOptions.length > 0 ? (
132
- <View style={styles.filtersBlock}>
133
- <Text style={styles.filtersLabel}>{tt('device_filter', 'pdvGateway') || 'Gateway do PDV'}</Text>
134
- <CompactFilterSelector
135
- icon={getDeviceFilterIcon(activeFilter)}
136
- label={selectedPdvGatewayLabel}
137
- title={tt('device_filter', 'pdvGateway') || 'Gateway do PDV'}
138
- accentColor={getDeviceFilterAccent(activeFilter, {
139
- brandColors,
140
- hex: accentPalette,
141
- })}
142
- active={activeFilter !== PDV_DEVICE_TYPE}
143
- options={pdvGatewaySelectorOptions}
144
- selectedKey={activeFilter}
145
- onSelect={optionKey => {
146
- setActiveFilter(optionKey);
147
- return true;
148
- }}
149
- />
150
- </View>
151
- ) : null}
152
-
153
- {activeFilterAction ? (
154
- <View style={styles.printerActionBlock}>
155
- <TouchableOpacity
156
- style={[
157
- styles.createPrinterBtn,
158
- {backgroundColor: brandColors.primary},
159
- ]}
160
- activeOpacity={0.86}
161
- onPress={() => navigation.navigate(activeFilterAction.routeName)}>
162
- <Icon name="plus-circle" size={16} color={brandColors.white} />
163
- <Text style={styles.createPrinterBtnText}>
164
- {activeFilterAction.label}
165
- </Text>
166
- </TouchableOpacity>
167
-
168
- <Text style={styles.helperText}>
169
- {activeFilterAction.helperText}
170
- </Text>
171
- </View>
172
- ) : null}
173
-
174
- {ActiveTabComponent ? (
175
- <ActiveTabComponent
176
- themePalette={themePalette}
177
- key={`${activeFilter}-${currentCompany?.id || 'company'}`}
178
- />
179
- ) : null}
180
- </SafeAreaView>
181
- );
182
- };
183
-
184
- export default Devices;
1
+ import React, {useEffect, useMemo, useState} from 'react';
2
+ import {Text, TouchableOpacity, View} from 'react-native';
3
+ import {SafeAreaView} from 'react-native-safe-area-context';
4
+ import {useNavigation} from '@react-navigation/native';
5
+ import {useStore} from '@store';
6
+ import {resolveThemePalette} from '@controleonline/../../src/styles/branding';
7
+ import {colors} from '@controleonline/../../src/styles/colors';
8
+ import CompactFilterSelector from '@controleonline/ui-default/src/react/components/filters/CompactFilterSelector';
9
+ import {PDV_DEVICE_TYPE} from '@controleonline/ui-common/src/react/utils/printerDevices';
10
+ import Icon from 'react-native-vector-icons/Feather';
11
+ import {
12
+ ALL_DEVICE_FILTER,
13
+ getDeviceCreationAction,
14
+ getDeviceFilterAccent,
15
+ getDeviceFilterDefinition,
16
+ getDeviceFilterIcon,
17
+ getDeviceFilterOptions,
18
+ getPdvSubfilterOptions,
19
+ hasRegisteredDeviceFilter,
20
+ isPdvSubfilter,
21
+ } from './Devices/deviceTypes';
22
+ import { createStyles } from './Devices.styles';
23
+
24
+ const tt = (type, key) => global.t?.t('configs', type, key);
25
+
26
+ const Devices = () => {
27
+ const navigation = useNavigation();
28
+ const peopleStore = useStore('people');
29
+ const themeStore = useStore('theme');
30
+
31
+ const {currentCompany} = peopleStore.getters;
32
+ const {colors: themeColors} = themeStore.getters;
33
+
34
+ const [activeFilter, setActiveFilter] = useState(ALL_DEVICE_FILTER);
35
+
36
+ const brandColors = useMemo(
37
+ () =>
38
+ resolveThemePalette(
39
+ {...themeColors, ...(currentCompany?.theme?.colors || {})},
40
+ colors,
41
+ ),
42
+ [themeColors, currentCompany?.id],
43
+ );
44
+ const styles = useMemo(() => createStyles(brandColors), [brandColors]);
45
+ const accentPalette = useMemo(
46
+ () => ({
47
+ primary: brandColors.info,
48
+ success: brandColors.success,
49
+ danger: brandColors.error,
50
+ warning: brandColors.warning,
51
+ }),
52
+ [brandColors.error, brandColors.info, brandColors.success, brandColors.warning],
53
+ );
54
+
55
+ const filterOptions = useMemo(() => getDeviceFilterOptions(), []);
56
+ const pdvSubfilterOptions = useMemo(() => getPdvSubfilterOptions(), []);
57
+ const activeFilterDefinition = useMemo(
58
+ () => getDeviceFilterDefinition(activeFilter),
59
+ [activeFilter],
60
+ );
61
+ const topLevelActiveFilter = useMemo(
62
+ () => (isPdvSubfilter(activeFilter) ? PDV_DEVICE_TYPE : activeFilter),
63
+ [activeFilter],
64
+ );
65
+ const activeFilterAction = useMemo(
66
+ () => getDeviceCreationAction(topLevelActiveFilter),
67
+ [topLevelActiveFilter],
68
+ );
69
+ const deviceTypeSelectorOptions = useMemo(
70
+ () => filterOptions.map(option => ({
71
+ key: option.key,
72
+ label: option.label,
73
+ })),
74
+ [filterOptions],
75
+ );
76
+ const pdvGatewaySelectorOptions = useMemo(
77
+ () => [
78
+ {key: PDV_DEVICE_TYPE, label: tt('device_filter', 'all') || 'Todos'},
79
+ ...pdvSubfilterOptions.map(option => ({
80
+ key: option.key,
81
+ label: option.label,
82
+ })),
83
+ ],
84
+ [pdvSubfilterOptions],
85
+ );
86
+ const selectedDeviceTypeLabel = useMemo(
87
+ () =>
88
+ deviceTypeSelectorOptions.find(option => option.key === topLevelActiveFilter)?.label ||
89
+ tt('device_filter', 'allTypes') ||
90
+ 'Todos os tipos',
91
+ [deviceTypeSelectorOptions, topLevelActiveFilter],
92
+ );
93
+ const selectedPdvGatewayLabel = useMemo(
94
+ () =>
95
+ pdvGatewaySelectorOptions.find(option => option.key === activeFilter)?.label ||
96
+ tt('device_filter', 'all') ||
97
+ 'Todos',
98
+ [activeFilter, pdvGatewaySelectorOptions],
99
+ );
100
+ const ActiveTabComponent = activeFilterDefinition.TabComponent;
101
+
102
+ useEffect(() => {
103
+ if (!hasRegisteredDeviceFilter(activeFilter)) {
104
+ setActiveFilter(ALL_DEVICE_FILTER);
105
+ }
106
+ }, [activeFilter]);
107
+
108
+ return (
109
+ <SafeAreaView
110
+ style={[styles.container, {backgroundColor: brandColors.background}]}>
111
+ <View style={styles.filtersBlock}>
112
+ <Text style={styles.filtersLabel}>{tt('device_filter', 'filterByType') || 'Filtrar por tipo'}</Text>
113
+ <CompactFilterSelector
114
+ icon={getDeviceFilterIcon(topLevelActiveFilter)}
115
+ label={selectedDeviceTypeLabel}
116
+ title={tt('device_filter', 'filterByType') || 'Filtrar por tipo'}
117
+ accentColor={getDeviceFilterAccent(topLevelActiveFilter, {
118
+ brandColors,
119
+ hex: accentPalette,
120
+ })}
121
+ active={topLevelActiveFilter !== ALL_DEVICE_FILTER}
122
+ options={deviceTypeSelectorOptions}
123
+ selectedKey={topLevelActiveFilter}
124
+ onSelect={optionKey => {
125
+ setActiveFilter(optionKey);
126
+ return true;
127
+ }}
128
+ />
129
+ </View>
130
+
131
+ {topLevelActiveFilter === PDV_DEVICE_TYPE && pdvSubfilterOptions.length > 0 ? (
132
+ <View style={styles.filtersBlock}>
133
+ <Text style={styles.filtersLabel}>{tt('device_filter', 'pdvGateway') || 'Gateway do PDV'}</Text>
134
+ <CompactFilterSelector
135
+ icon={getDeviceFilterIcon(activeFilter)}
136
+ label={selectedPdvGatewayLabel}
137
+ title={tt('device_filter', 'pdvGateway') || 'Gateway do PDV'}
138
+ accentColor={getDeviceFilterAccent(activeFilter, {
139
+ brandColors,
140
+ hex: accentPalette,
141
+ })}
142
+ active={activeFilter !== PDV_DEVICE_TYPE}
143
+ options={pdvGatewaySelectorOptions}
144
+ selectedKey={activeFilter}
145
+ onSelect={optionKey => {
146
+ setActiveFilter(optionKey);
147
+ return true;
148
+ }}
149
+ />
150
+ </View>
151
+ ) : null}
152
+
153
+ {activeFilterAction ? (
154
+ <View style={styles.printerActionBlock}>
155
+ <TouchableOpacity
156
+ style={[
157
+ styles.createPrinterBtn,
158
+ {backgroundColor: brandColors.primary},
159
+ ]}
160
+ activeOpacity={0.86}
161
+ onPress={() => navigation.navigate(activeFilterAction.routeName)}>
162
+ <Icon name="plus-circle" size={16} color={brandColors.white} />
163
+ <Text style={styles.createPrinterBtnText}>
164
+ {activeFilterAction.label}
165
+ </Text>
166
+ </TouchableOpacity>
167
+
168
+ <Text style={styles.helperText}>
169
+ {activeFilterAction.helperText}
170
+ </Text>
171
+ </View>
172
+ ) : null}
173
+
174
+ {ActiveTabComponent ? (
175
+ <ActiveTabComponent
176
+ key={`${activeFilter}-${currentCompany?.id || 'company'}`}
177
+ />
178
+ ) : null}
179
+ </SafeAreaView>
180
+ );
181
+ };
182
+
183
+ export default Devices;
@@ -1,49 +0,0 @@
1
- import {useEffect, useRef, useState} from 'react';
2
- import {Platform} from 'react-native';
3
-
4
- const nativeStorage = () => {
5
- const module = require('@react-native-async-storage/async-storage');
6
- return module.default || module;
7
- };
8
- const deviceStorage = {
9
- getItem: key => Platform.OS === 'web' ? globalThis.localStorage?.getItem(key) : nativeStorage().getItem(key),
10
- setItem: (key, value) => Platform.OS === 'web' ? globalThis.localStorage.setItem(key, value) : nativeStorage().setItem(key, value),
11
- };
12
-
13
- // Visibility is personal device state. It never changes server menu permissions.
14
- export default function useBottomNavigationPreference({enabled, principal, appType, storage = deviceStorage}) {
15
- const key = enabled && principal && appType
16
- ? `co.bottom-navigation.v1:${encodeURIComponent(appType)}:${encodeURIComponent(principal)}` : '';
17
- const current = useRef(key);
18
- current.current = key;
19
- const [state, setState] = useState({key: '', ready: false, remembered: true, session: null, error: ''});
20
- useEffect(() => {
21
- current.current = key;
22
- let cancelled = false;
23
- setState({key, ready: false, remembered: true, session: null, error: ''});
24
- if (key) Promise.resolve().then(() => storage.getItem(key)).then(value => {
25
- if (!cancelled) setState({key, ready: true, remembered: value !== 'hidden', session: null, error: ''});
26
- }).catch(() => {
27
- if (!cancelled) setState({key, ready: true, remembered: true, session: null, error: 'Não foi possível recuperar a preferência da barra.'});
28
- });
29
- return () => {cancelled = true; if (current.current === key) current.current = null;};
30
- }, [key, storage]);
31
- const ready = Boolean(key && state.key === key && state.ready);
32
- const setSessionVisible = visible => {
33
- if (ready) setState(previous => ({...previous, session: Boolean(visible), error: ''}));
34
- };
35
- const setRememberedVisible = async visible => {
36
- if (!ready) return false;
37
- try {
38
- await storage.setItem(key, visible ? 'visible' : 'hidden');
39
- if (current.current !== key) return false;
40
- setState(previous => ({...previous, remembered: Boolean(visible), session: null, error: ''}));
41
- return true;
42
- } catch {
43
- if (current.current === key) setState(previous => ({...previous, error: 'Não foi possível lembrar a preferência neste aparelho.'}));
44
- return false;
45
- }
46
- };
47
- return {ready, visible: ready && (state.session ?? state.remembered), rememberedVisible: ready ? state.remembered : true,
48
- error: state.key === key ? state.error : '', setSessionVisible, setRememberedVisible};
49
- }
@@ -1,13 +0,0 @@
1
- import eventBus from '../components/EventBus';
2
-
3
- const key = 'product-catalog-cache-revision';
4
- let revision = '0';
5
- let sequence = 0;
6
- export const getProductCatalogCacheRevision = () => {
7
- try { return localStorage.getItem(key) || revision; } catch { return revision; }
8
- };
9
- export const invalidateProductCatalogCache = () => {
10
- revision = `${Date.now()}:${++sequence}`;
11
- try { localStorage.setItem(key, revision); } catch { /* In-memory invalidation still applies. */ }
12
- eventBus.emit('product-catalog-cache:cleared');
13
- };
@@ -1,22 +0,0 @@
1
- const assert = require('node:assert/strict');
2
- const {test} = require('node:test');
3
- const {readStoredSessionToken} = require('../../../utils/storedSessionToken');
4
-
5
- for (const prefix of ['', '__q_objt|']) {
6
- for (const field of ['token', 'api_key']) {
7
- test(`reads ${field} from ${prefix ? 'wrapped' : 'plain'} session`, () => {
8
- const storage = {getItem: key => key === 'session' ? prefix + JSON.stringify({[field]: 'synthetic-token'}) : null};
9
- assert.equal(readStoredSessionToken(storage), 'synthetic-token');
10
- });
11
- }
12
- }
13
- test('prefers the canonical token field when both are present', () => {
14
- assert.equal(readStoredSessionToken({getItem: () => JSON.stringify({token: 'current', api_key: 'old'})}), 'current');
15
- });
16
- test('missing, malformed and inaccessible sessions have no token', () => {
17
- for (const raw of [null, '', 'broken', '__q_objt|broken', '{}', 'null']) {
18
- assert.equal(readStoredSessionToken({getItem: () => raw}), '');
19
- }
20
- assert.equal(readStoredSessionToken({getItem: () => {throw new Error('unavailable');}}), '');
21
- assert.equal(readStoredSessionToken(null), '');
22
- });
@@ -1,18 +0,0 @@
1
- // Quasar LocalStorage serializes objects with this marker; plain JSON is also used.
2
- const OBJECT_PREFIX = '__q_objt|';
3
-
4
- const readStoredSessionToken = (storage = typeof localStorage === 'undefined' ? null : localStorage) => {
5
- try {
6
- const raw = storage?.getItem?.('session');
7
- if (!raw) return '';
8
- const json = typeof raw === 'string' && raw.startsWith(OBJECT_PREFIX)
9
- ? raw.slice(OBJECT_PREFIX.length)
10
- : raw;
11
- const session = JSON.parse(json);
12
- return session?.token || session?.api_key || '';
13
- } catch {
14
- return '';
15
- }
16
- };
17
-
18
- module.exports = {readStoredSessionToken};