@controleonline/ui-common 1.2.101 → 1.2.102

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.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."],
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."],
9
9
  "frozen": true
10
10
  }
@@ -9,8 +9,4 @@ Local evidence: 40 homepage tests and 34 runtime footer/zoom tests passed. No pr
9
9
 
10
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
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.
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.95.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@controleonline/ui-common",
3
- "version": "1.2.101",
3
+ "version": "1.2.102",
4
4
  "author": "ControleOnline",
5
5
  "description": "ControleOnline Common Quasar UI Component",
6
6
  "license": "MIT",
package/src/api/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import {readStoredSessionToken} from '@controleonline/ui-common/src/utils/storedSessionToken';
1
2
  import myFetch from '@controleonline/ui-common/src/api/fetch';
2
3
  import axios from 'axios';
3
4
  import {env as APP_ENV} from '@env';
@@ -215,8 +216,8 @@ async function requestSmokeUrl(url, options) {
215
216
  export const api = {
216
217
  device: JSON.parse(localStorage.getItem('device') || '{}'),
217
218
  masterDevice: JSON.parse(localStorage.getItem('master-device') || '{}'),
218
-
219
-
219
+
220
+
220
221
  upload: async function (uri, formData) {
221
222
  const token = await this.getToken();
222
223
  const appDomain = resolveAppDomain(APP_ENV.DOMAIN);
@@ -242,8 +243,8 @@ export const api = {
242
243
  fetch: async function (uri, options = {}) {
243
244
  if (typeof options.headers === 'undefined')
244
245
  Object.assign(options, { headers: new Headers() });
245
-
246
- // Always refresh device from localStorage in case it was set after module load
246
+
247
+ // Always refresh device from localStorage in case it was set after module load
247
248
  try {
248
249
  this.device = JSON.parse(localStorage.getItem('device') || '{}');
249
250
  } catch (e) {
@@ -270,16 +271,16 @@ export const api = {
270
271
  if (options.body) {
271
272
  if (typeof options.body === 'object') {
272
273
  bodyDeviceId = options.body.device || null;
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
-
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
+
283
284
  const requestDevice = resolveRequestDevice(
284
285
  this.masterDevice,
285
286
  this.device,
@@ -296,10 +297,10 @@ export const api = {
296
297
  options.headers.set('DEVICE-TYPE', requestDeviceType);
297
298
  }
298
299
  }
299
- if (options.responseType != 'text') {
300
- options.headers.set('Content-Type', MIME_TYPE);
301
- options.headers.set('Accept', MIME_TYPE);
302
- }
300
+ if (options.responseType != 'text') {
301
+ options.headers.set('Content-Type', MIME_TYPE);
302
+ options.headers.set('Accept', MIME_TYPE);
303
+ }
303
304
  options.headers.set('App-Domain', resolveAppDomain(APP_ENV.DOMAIN));
304
305
 
305
306
  if (options.body && typeof options.body != 'string') {
@@ -310,28 +311,13 @@ export const api = {
310
311
  if (options.params) {
311
312
  uri = this.buildQueryString(uri, options);
312
313
  }
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
- },
314
+ return myFetch(uri, options).catch(e => {
315
+ throw e;
316
+ });
317
+ },
318
+ async getToken() {
319
+ return readStoredSessionToken();
320
+ },
335
321
  serialize(obj, prefix = '') {
336
322
  const pairs = [];
337
323
 
@@ -344,12 +330,12 @@ export const api = {
344
330
 
345
331
  const value = obj[key];
346
332
  let fullKey = prefix ? `${prefix}.${key}` : key; // Usa ponto para objetos
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));
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));
353
339
  } else if (Array.isArray(value)) {
354
340
  // Array: adiciona [] e itera sobre os valores
355
341
  if (value.length === 0) {
@@ -364,14 +350,14 @@ export const api = {
364
350
  });
365
351
  }
366
352
  } else {
367
- // Valor simples
368
- pairs.push(`${fullKey}=${encodeURIComponent(value)}`);
369
- }
370
- }
371
- }
372
- return pairs;
373
- },
374
-
353
+ // Valor simples
354
+ pairs.push(`${fullKey}=${encodeURIComponent(value)}`);
355
+ }
356
+ }
357
+ }
358
+ return pairs;
359
+ },
360
+
375
361
  buildQueryString(uri, options) {
376
362
  const safeUri = stripDomainQueryParams(uri);
377
363
 
@@ -38,6 +38,8 @@ export default function AnimatedModal({
38
38
  onRequestClose,
39
39
  children,
40
40
  style,
41
+ overlayColor,
42
+ contentStyle,
41
43
  }) {
42
44
  const [showing, setShowing] = useState(visible);
43
45
  const [keyboardHeight, setKeyboardHeight] = useState(0);
@@ -125,6 +127,8 @@ export default function AnimatedModal({
125
127
  const overlayStyle = [
126
128
  styles.overlay,
127
129
  style,
130
+ overlayColor,
131
+ contentStyle,
128
132
  Platform.OS === 'android' ? {paddingBottom: keyboardHeight} : null,
129
133
  ];
130
134
 
@@ -152,11 +156,12 @@ export default function AnimatedModal({
152
156
  style={[
153
157
  StyleSheet.absoluteFillObject,
154
158
  styles.overlayBg,
159
+ overlayColor ? {backgroundColor: overlayColor} : null,
155
160
  {opacity: overlayOpacity},
156
161
  ]}
157
162
  />
158
163
  <Animated.View
159
- style={[styles.content, {transform: [{translateY: slideY}]}]}>
164
+ style={[styles.content, contentStyle, {transform: [{translateY: slideY}]}]}>
160
165
  {normalizedChildren}
161
166
  </Animated.View>
162
167
  <Toasts
@@ -17,7 +17,8 @@ const BottomNavigationBar = ({
17
17
  colors = {},
18
18
  testID = 'bottom-navigation',
19
19
  useModernWebChromeProps = false,
20
- appearance = 'legacy',
20
+ appearance = 'soft',
21
+ placement = 'overlay',
21
22
  }) => {
22
23
  const Icon = appearance === 'soft' ? HomeIcon : LegacyIcon;
23
24
  colors = appearance === 'soft' ? resolveHomepagePalette(colors) : colors;
@@ -104,9 +105,7 @@ const BottomNavigationBar = ({
104
105
 
105
106
  const routeItems = Array.isArray(items) ? items : [];
106
107
  const knownRoute = routeItems.some(item => item?.route === activeRouteName);
107
- const effectiveActiveRoute = knownRoute
108
- ? activeRouteName
109
- : routeItems[0]?.route || '';
108
+ const effectiveActiveRoute = knownRoute ? activeRouteName : '';
110
109
 
111
110
  const navigateTo = item => {
112
111
  try {
@@ -134,7 +133,7 @@ const BottomNavigationBar = ({
134
133
  };
135
134
 
136
135
  return (
137
- <View {...hostProps} style={hostStyle}>
136
+ <View {...hostProps} style={[hostStyle, placement === 'inline' ? {position: 'relative', flexShrink: 0, zIndex: 0} : null]}>
138
137
  <View style={styles.stack}>
139
138
  <View style={styles.dock} testID={testID}>
140
139
  <View style={styles.itemsRow}>
@@ -151,6 +150,8 @@ const BottomNavigationBar = ({
151
150
  <Pressable
152
151
  key={item.route}
153
152
  accessibilityRole="button"
153
+ accessibilityLabel={item.label}
154
+ accessibilityState={{selected: isActive, disabled: Boolean(isDisabled)}}
154
155
  disabled={isDisabled}
155
156
  onPress={() => navigateTo(item)}
156
157
  style={({pressed}) => [
@@ -1,6 +1,6 @@
1
1
  import {Platform} from 'react-native';
2
2
 
3
- // Only the homepage opts into this chrome; preserve all default navigation styles.
3
+ // Shared compact chrome on every route; explicit legacy consumers can opt out.
4
4
  export default (styles, colors, appearance) => appearance !== 'soft' ? styles : {
5
5
  ...styles,
6
6
  dock: [styles.dock, {
@@ -16,9 +16,10 @@ 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: 64, paddingHorizontal: 12, paddingVertical: 5}],
20
- item: [styles.item, {minHeight: 54, borderRadius: 12}],
21
- itemLabel: [styles.itemLabel, {fontSize: 11, fontWeight: '400'}],
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'}],
22
23
  footerSlot: [styles.footerSlot, {
23
24
  borderTopWidth: 1,
24
25
  borderColor: colors.navigationBorder,
@@ -1,45 +1,53 @@
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;
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;
@@ -19,7 +19,8 @@ const {
19
19
  } = require('@controleonline/ui-common/src/react/utils/bottomNavigationToolbar');
20
20
 
21
21
  const RuntimeBottomNavigationBar = ({
22
- appearance = 'legacy',
22
+ appearance = 'soft',
23
+ placement = 'overlay',
23
24
  activeRouteName: activeRouteNameProp,
24
25
  colors: colorsOverride,
25
26
  disabled: disabledOverride,
@@ -42,13 +43,16 @@ const RuntimeBottomNavigationBar = ({
42
43
  const peopleGetters = peopleStore?.getters || {};
43
44
  const currentCompany = peopleGetters.currentCompany || {};
44
45
  const themeColors = themeGetters.colors || {};
45
- const [runtimeMenus, setRuntimeMenus] = useState([]);
46
+ const [menuState, setMenuState] = useState({scope: '', menus: []});
46
47
  const preset = presetKey ? getBottomNavigationPreset(presetKey) : null;
47
48
  const routeAliases = routeAliasesProp || preset?.routeAliases || {};
48
49
  const appType = normalizeAppType(app_type);
50
+ const menuScope = JSON.stringify([appType, currentCompany?.id, menuType, presetKey]);
51
+ const runtimeMenus = menuState.scope === menuScope ? menuState.menus : [];
49
52
 
50
53
  // Always consult menus-people for role-aware toolbar; preset is fallback only.
51
54
  useEffect(() => {
55
+ const setRuntimeMenus = menus => setMenuState({scope: menuScope, menus});
52
56
  if (!currentCompany?.id || !menuType) {
53
57
  setRuntimeMenus(
54
58
  resolveToolbarRuntimeMenus({
@@ -105,7 +109,7 @@ const RuntimeBottomNavigationBar = ({
105
109
  return () => {
106
110
  cancelled = true;
107
111
  };
108
- }, [appType, currentCompany?.id, menuType, preset, presetKey]);
112
+ }, [appType, currentCompany?.id, menuType, preset, presetKey, menuScope]);
109
113
 
110
114
  const colors = useMemo(() => {
111
115
  if (colorsOverride) {
@@ -151,16 +155,19 @@ const RuntimeBottomNavigationBar = ({
151
155
  const knownRoute = navItems.some(item => item.route === resolvedActiveRoute);
152
156
  const effectiveActiveRoute = knownRoute
153
157
  ? resolvedActiveRoute
154
- : navItems[0]?.route || activeRouteName;
158
+ : activeRouteName;
155
159
 
156
160
  const isDisabled =
157
161
  typeof disabledOverride === 'boolean'
158
162
  ? disabledOverride
159
163
  : !currentCompany || Object.keys(currentCompany).length === 0;
160
164
 
165
+ if (!navItems.length) return null;
166
+
161
167
  return (
162
168
  <BottomNavigationBar
163
169
  appearance={appearance}
170
+ placement={placement}
164
171
  activeRouteName={effectiveActiveRoute}
165
172
  colors={colors}
166
173
  disabled={isDisabled}
@@ -1,3 +1,5 @@
1
+ import {readStoredSessionToken} from '../../utils/storedSessionToken';
2
+ import {isTrustedApiUrl, filterApiHeaders} from '../../utils/trustedApiUrl';
1
3
  import React, {useEffect, useMemo, useState} from 'react';
2
4
  import {Image, Platform, StyleSheet, Text, View} from 'react-native';
3
5
  import {env as APP_ENV} from '@env';
@@ -14,18 +16,6 @@ const isBackendDownloadUrl = uri =>
14
16
  /** Session cache: gravatar URL → true (ok) | false (missing). Avoids repeat probes. */
15
17
  const gravatarProbeCache = new Map();
16
18
 
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
- };
29
19
 
30
20
  /**
31
21
  * Probe Gravatar (d=404) via fetch BEFORE exposing URL to <Image>.
@@ -86,10 +76,11 @@ const useDisplayUri = uri => {
86
76
  return;
87
77
  }
88
78
 
89
- const token = readSessionToken();
79
+ const token = readStoredSessionToken();
90
80
 
91
81
  // Authenticated backend download on web
92
- if (isBackendDownloadUrl(next) && Platform.OS === 'web' && token) {
82
+ if (isBackendDownloadUrl(next) && isTrustedApiUrl(next, APP_ENV?.API_ENTRYPOINT)
83
+ && Platform.OS === 'web' && token) {
93
84
  try {
94
85
  const headers = {
95
86
  Accept: '*/*',
@@ -102,7 +93,7 @@ const useDisplayUri = uri => {
102
93
  headers['App-Domain'] = host;
103
94
  }
104
95
 
105
- const response = await fetch(next, {method: 'GET', headers});
96
+ const response = await fetch(next, {method: 'GET', headers, redirect: 'error'});
106
97
  if (!response.ok) {
107
98
  if (!cancelled) {
108
99
  setDisplayUri('');
@@ -217,7 +208,7 @@ const UserAvatar = ({
217
208
  {displayUri ? (
218
209
  <Image
219
210
  source={imageHeaders && Platform.OS !== 'web' && displayUri === imageUrl
220
- ? {uri: displayUri, headers: imageHeaders} : {uri: displayUri}}
211
+ ? {uri: displayUri, headers: filterApiHeaders(displayUri, imageHeaders, APP_ENV?.API_ENTRYPOINT)} : {uri: displayUri}}
221
212
  style={styles.image}
222
213
  onError={() => setSourceIndex(index => index + 1)}
223
214
  />
@@ -0,0 +1,49 @@
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
+ }
@@ -24,7 +24,7 @@ import {
24
24
  parseConfigsObject,
25
25
  tt,
26
26
  } from './deviceListHelpers';
27
- import styles from '../../Devices.styles';
27
+ import legacyStyles from '../../Devices.styles';
28
28
 
29
29
  /**
30
30
  * Normalize DefaultTable card `item` into a device group shape.
@@ -65,6 +65,7 @@ const normalizeDeviceGroup = item => {
65
65
  export default function DeviceGroupCard({
66
66
  item,
67
67
  brandColors,
68
+ themedStyles = null,
68
69
  creatingPdv,
69
70
  goToDetail,
70
71
  handleCreateCurrentPdv,
@@ -74,6 +75,7 @@ export default function DeviceGroupCard({
74
75
  runtimeDeviceType,
75
76
  showCurrentPdvSetup,
76
77
  }) {
78
+ const styles = themedStyles || legacyStyles;
77
79
  const deviceGroup = normalizeDeviceGroup(item);
78
80
  if (!deviceGroup) {
79
81
  return null;
@@ -116,7 +118,7 @@ export default function DeviceGroupCard({
116
118
  deviceGroup.device?.id ||
117
119
  deviceGroup.key,
118
120
  );
119
- const accent = getDeviceTypeAccent(normalizedType);
121
+ const accent = brandColors.deviceAccent || getDeviceTypeAccent(normalizedType);
120
122
  const metaChips = [];
121
123
 
122
124
  if (isPdv && primaryConfig) {
@@ -196,7 +198,7 @@ export default function DeviceGroupCard({
196
198
  <View style={styles.deviceConfigRow}>
197
199
  {deviceConfigs.map(deviceConfig => {
198
200
  const configType = getDeviceConfigType(deviceConfig);
199
- const configAccent = getDeviceTypeAccent(configType);
201
+ const configAccent = brandColors.deviceAccent || getDeviceTypeAccent(configType);
200
202
  const isSessionConfig =
201
203
  isCurrentDevice &&
202
204
  runtimeDeviceType &&
@@ -297,5 +299,5 @@ export default function DeviceGroupCard({
297
299
  </View>
298
300
  </View>
299
301
  );
300
-
302
+
301
303
  }
@@ -36,7 +36,7 @@ import {
36
36
  prioritizeCurrentDeviceGroups,
37
37
  readStoredRuntimeDevice,
38
38
  } from '../currentDevice';
39
- import styles from '../../Devices.styles';
39
+ import legacyStyles, {createStyles} 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 = () => {
92
+ const DeviceTypeTab = ({themePalette = null} = {}) => {
93
93
  const navigation = useNavigation();
94
94
  const peopleStore = useStore('people');
95
95
  const deviceStore = useStore('device');
@@ -101,13 +101,15 @@ export const createDeviceTypeTab = ({
101
101
 
102
102
  const brandColors = useMemo(
103
103
  () =>
104
- resolveThemePalette(
104
+ themePalette || resolveThemePalette(
105
105
  {...themeColors, ...(currentCompany?.theme?.colors || {})},
106
106
  colors,
107
107
  ),
108
- [themeColors, currentCompany?.id],
108
+ [themePalette, themeColors, currentCompany?.id],
109
109
  );
110
110
 
111
+ const styles = useMemo(() => themePalette ? createStyles(brandColors) : legacyStyles, [themePalette, brandColors]);
112
+
111
113
  const [deviceConfigs, setDeviceConfigs] = useState([]);
112
114
  const [loading, setLoading] = useState(false);
113
115
  const [refreshing, setRefreshing] = useState(false);
@@ -372,6 +374,7 @@ export const createDeviceTypeTab = ({
372
374
  <DeviceGroupCard
373
375
  {...cardProps}
374
376
  brandColors={brandColors}
377
+ themedStyles={themePalette ? styles : null}
375
378
  creatingPdv={creatingPdv}
376
379
  goToDetail={goToDetail}
377
380
  handleCreateCurrentPdv={handleCreateCurrentPdv}
@@ -384,6 +387,8 @@ export const createDeviceTypeTab = ({
384
387
  ),
385
388
  [
386
389
  brandColors,
390
+ styles,
391
+ themePalette,
387
392
  creatingPdv,
388
393
  goToDetail,
389
394
  handleCreateCurrentPdv,