@umituz/react-native-design-system 4.25.96 → 4.25.98

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "4.25.96",
3
+ "version": "4.25.98",
4
4
  "description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive, safe area, exception, infinite scroll, UUID, image, timezone, offline, onboarding, and loading utilities",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -95,7 +95,7 @@ export const PickerModal: React.FC<PickerModalProps> = React.memo(({
95
95
  {selected && <AtomicIcon name={icons.checkCircle} size="md" color="primary" />}
96
96
  </TouchableOpacity>
97
97
  );
98
- }, [isSelected, onSelect, tokens, testID]);
98
+ }, [icons.checkCircle, isSelected, onSelect, tokens, testID]);
99
99
 
100
100
  return (
101
101
  <Modal visible={visible} animationType="none" transparent onRequestClose={onClose} testID={`${testID}-modal`} accessibilityViewIsModal={true}>
@@ -47,6 +47,12 @@ export function useInfiniteScroll<T>(
47
47
  const isLoadingRef = useRef(false);
48
48
  const isMountedRef = useRef(true);
49
49
  const abortControllerRef = useRef<AbortController | null>(null);
50
+ const stateRef = useRef(state);
51
+
52
+ // Keep stateRef in sync with state
53
+ useEffect(() => {
54
+ stateRef.current = state;
55
+ }, [state]);
50
56
 
51
57
  useEffect(() => {
52
58
  isMountedRef.current = true;
@@ -89,22 +95,25 @@ export function useInfiniteScroll<T>(
89
95
  }, [config, initialPage, pageSize, totalItems, maxRetries, retryDelay, cancelPendingRequests]);
90
96
 
91
97
  const loadMore = useCallback(async () => {
98
+ // Get fresh state from ref to avoid stale closure
99
+ const currentState = stateRef.current;
100
+
92
101
  if (
93
102
  isLoadingRef.current ||
94
- !state.hasMore ||
95
- state.isLoadingMore ||
96
- state.isLoading
103
+ !currentState.hasMore ||
104
+ currentState.isLoadingMore ||
105
+ currentState.isLoading
97
106
  )
98
107
  return;
99
108
 
100
- if (isCursorMode(config) && !state.cursor) return;
109
+ if (isCursorMode(config) && !currentState.cursor) return;
101
110
 
102
111
  isLoadingRef.current = true;
103
112
  if (isMountedRef.current) setState((prev) => ({ ...prev, isLoadingMore: true, error: null }));
104
113
 
105
114
  try {
106
115
  const updates = await retryWithBackoff(
107
- () => loadMoreData(config, state, pageSize),
116
+ () => loadMoreData(config, currentState, pageSize),
108
117
  maxRetries,
109
118
  retryDelay,
110
119
  );
@@ -120,7 +129,7 @@ export function useInfiniteScroll<T>(
120
129
  } finally {
121
130
  isLoadingRef.current = false;
122
131
  }
123
- }, [config, state, pageSize, maxRetries, retryDelay]);
132
+ }, [config, pageSize, maxRetries, retryDelay]);
124
133
 
125
134
  const refresh = useCallback(async () => {
126
135
  if (isLoadingRef.current) return;
@@ -15,6 +15,7 @@ import type { ScreenLayoutProps } from './types';
15
15
  let KCKeyboardAvoidingView: React.ComponentType<any> | null = null;
16
16
  let KCKeyboardAwareScrollView: React.ComponentType<any> | null = null;
17
17
  try {
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
18
19
  const kc = require('react-native-keyboard-controller');
19
20
  KCKeyboardAvoidingView = kc.KeyboardAvoidingView ?? null;
20
21
  KCKeyboardAwareScrollView = kc.KeyboardAwareScrollView ?? null;
@@ -1,6 +1,4 @@
1
- export * from './AppHeader';
2
1
  export * from './Container';
3
2
  export * from './FormLayout';
4
3
  export * from './Grid';
5
- export * from './ScreenHeader';
6
4
  export * from './ScreenLayout';
@@ -7,7 +7,7 @@
7
7
  import React, { useMemo } from 'react';
8
8
  import { View, ViewStyle, StyleProp } from 'react-native';
9
9
  import { useAppDesignTokens } from '../theme';
10
- import { ConfirmationModalVariant } from './confirmation-modal/types/';
10
+ import { ConfirmationModalVariant } from './confirmation-modal/types';
11
11
  import {
12
12
  getVariantConfig,
13
13
  getModalContainerStyle,
@@ -7,7 +7,7 @@
7
7
  import React, { useCallback } from 'react';
8
8
  import { View, Modal, TouchableOpacity } from 'react-native';
9
9
  import { useAppDesignTokens } from '../theme';
10
- import { ConfirmationModalProps } from './confirmation-modal/types/';
10
+ import { ConfirmationModalProps } from './confirmation-modal/types';
11
11
  import {
12
12
  getModalOverlayStyle,
13
13
  getBackdropStyle,
@@ -7,6 +7,7 @@ import { Alert } from './AlertTypes';
7
7
 
8
8
  interface AlertState {
9
9
  alerts: Alert[];
10
+ _updateInProgress: boolean;
10
11
  }
11
12
 
12
13
  interface AlertActions {
@@ -19,13 +20,37 @@ export const useAlertStore = createStore<AlertState, AlertActions>({
19
20
  name: 'alert-store',
20
21
  initialState: {
21
22
  alerts: [],
23
+ _updateInProgress: false,
22
24
  },
23
25
  persist: false,
24
26
  actions: (set, get) => ({
25
- addAlert: (alert: Alert) => set({ alerts: [...get().alerts, alert] }),
26
- dismissAlert: (id: string) => set({
27
- alerts: get().alerts.filter((a: Alert) => a.id !== id)
28
- }),
27
+ addAlert: (alert: Alert) => {
28
+ const { _updateInProgress, alerts } = get();
29
+ if (_updateInProgress) return;
30
+
31
+ const existingIndex = alerts.findIndex((a: Alert) => a.id === alert.id);
32
+
33
+ if (existingIndex >= 0) {
34
+ // Replace existing alert
35
+ const updatedAlerts = [...alerts];
36
+ updatedAlerts[existingIndex] = alert;
37
+ set({ alerts: updatedAlerts });
38
+ } else {
39
+ // Add new alert
40
+ set({ alerts: [...alerts, alert] });
41
+ }
42
+ },
43
+ dismissAlert: (id: string) => {
44
+ const { _updateInProgress, alerts } = get();
45
+ if (_updateInProgress) return;
46
+
47
+ const updatedAlerts = alerts.filter((a: Alert) => a.id !== id);
48
+
49
+ // Only update if something actually changed
50
+ if (updatedAlerts.length !== alerts.length) {
51
+ set({ alerts: updatedAlerts });
52
+ }
53
+ },
29
54
  clearAlerts: () => set({ alerts: [] }),
30
55
  }),
31
56
  });
@@ -15,7 +15,7 @@ export function useAlertDismissHandler(alert: Alert) {
15
15
  const handleDismiss = useCallback(() => {
16
16
  dismissAlert(alert.id);
17
17
  alert.onDismiss?.();
18
- }, [alert.id, alert.onDismiss, dismissAlert]);
18
+ }, [alert, dismissAlert]);
19
19
 
20
20
  return handleDismiss;
21
21
  }
@@ -1,4 +1,3 @@
1
1
  export * from "./CircularMenu";
2
- export * from "./CircularMenuItem";
3
2
  export * from "./CircularMenuBackground";
4
3
  export * from "./CircularMenuCloseButton";
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { ViewStyle } from 'react-native';
8
- import { ConfirmationModalVariant, ConfirmationModalVariantConfig } from '../types/';
8
+ import { ConfirmationModalVariant, ConfirmationModalVariantConfig } from '../types';
9
9
  import type { DesignTokens } from '../../../theme';
10
10
 
11
11
  /**
@@ -8,62 +8,31 @@ export * from './avatar';
8
8
  export * from './bottom-sheet';
9
9
  export { FormField, type FormFieldProps } from './FormField';
10
10
  export { ListItem, type ListItemProps } from './ListItem';
11
-
12
-
13
11
  export { SearchBar, type SearchBarProps } from './SearchBar';
14
12
  export { IconContainer } from './IconContainer';
15
13
  export { BaseModal, type BaseModalProps } from './BaseModal';
16
14
  export { ConfirmationModal } from './ConfirmationModalMain';
17
15
  export { useConfirmationModal } from './confirmation-modal/useConfirmationModal';
18
16
 
19
- // Type exports
20
- export type {
21
- ConfirmationModalProps,
22
- ConfirmationModalVariant,
23
- } from './confirmation-modal/types/';
24
-
25
- // Divider
26
- export * from './Divider';
27
- export * from "./StepProgress";
28
-
29
- // Responsive Components
30
- export { List, type ListProps } from './List';
31
-
32
- // Alerts
17
+ // Other components
18
+ export * from './Divider/Divider';
19
+ export * from './Divider/types';
20
+ export * from './StepProgress';
21
+ export * from './List';
33
22
  export * from './alerts';
34
-
35
- // Calendar
36
23
  export * from './calendar';
37
-
38
- // Swipe Actions
39
24
  export * from './swipe-actions';
40
-
41
- // Navigation
42
25
  export * from './navigation';
43
-
44
- // Long Press Menu
45
26
  export * from './long-press-menu';
46
-
47
- // Step Header
48
27
  export * from './StepHeader';
49
-
50
- // Emoji
51
28
  export * from './emoji';
52
-
53
- // Countdown
54
29
  export * from './countdown';
55
-
56
- // Splash
57
30
  export * from './splash';
58
-
59
-
60
31
  export * from './filter-group';
61
- export * from './action-footer';
62
-
63
- export * from './hero-section';
32
+ export * from './action-footer/ActionFooter';
33
+ export * from './action-footer/types';
34
+ export * from './hero-section/HeroSection';
35
+ export * from './hero-section/types';
64
36
  export * from './info-grid';
65
-
66
-
67
37
  export * from './circular-menu';
68
-
69
38
  export * from './icon-grid';
@@ -31,6 +31,7 @@ export function useAppNavigation(): AppNavigationResult {
31
31
 
32
32
  const navigate = useCallback(
33
33
  (screen: string, params?: Record<string, unknown>) => {
34
+ // Dynamic navigation: bypass ParamListBase constraint to allow arbitrary screen names
34
35
  (navigation as any).navigate(screen, params);
35
36
  },
36
37
  [navigation]
@@ -11,6 +11,7 @@ const DEFAULT_TIMEOUT = 5000;
11
11
  export class HealthCheck {
12
12
  private intervalId: ReturnType<typeof setInterval> | null = null;
13
13
  private isChecking = false;
14
+ private pendingCheck: Promise<boolean> | null = null;
14
15
  private config: Required<OfflineConfig>;
15
16
 
16
17
  constructor(config: OfflineConfig = {}) {
@@ -27,37 +28,47 @@ export class HealthCheck {
27
28
  * Perform a single health check
28
29
  */
29
30
  async check(): Promise<boolean> {
30
- if (this.isChecking) {
31
- return false;
31
+ // Return existing promise if check is in progress
32
+ if (this.pendingCheck) {
33
+ return this.pendingCheck;
32
34
  }
33
35
 
34
- this.isChecking = true;
36
+ this.pendingCheck = (async () => {
37
+ if (this.isChecking) {
38
+ return false;
39
+ }
35
40
 
36
- try {
37
- const controller = new AbortController();
38
- const timeoutId = setTimeout(() => controller.abort(), this.config.healthCheckTimeout);
41
+ this.isChecking = true;
39
42
 
40
- const response = await fetch(this.config.healthCheckUrl, {
41
- method: 'HEAD',
42
- signal: controller.signal,
43
- });
43
+ try {
44
+ const controller = new AbortController();
45
+ const timeoutId = setTimeout(() => controller.abort(), this.config.healthCheckTimeout);
44
46
 
45
- clearTimeout(timeoutId);
47
+ const response = await fetch(this.config.healthCheckUrl, {
48
+ method: 'HEAD',
49
+ signal: controller.signal,
50
+ });
46
51
 
47
- const isHealthy = response.ok;
52
+ clearTimeout(timeoutId);
48
53
 
49
- if (this.config.debug) {
50
- }
54
+ const isHealthy = response.ok;
51
55
 
52
- return isHealthy;
53
- } catch (_error) {
54
- if (this.config.debug) {
56
+ if (this.config.debug) {
57
+ }
58
+
59
+ return isHealthy;
60
+ } catch (_error) {
61
+ if (this.config.debug) {
62
+ }
63
+
64
+ return false;
65
+ } finally {
66
+ this.isChecking = false;
67
+ this.pendingCheck = null;
55
68
  }
69
+ })();
56
70
 
57
- return false;
58
- } finally {
59
- this.isChecking = false;
60
- }
71
+ return this.pendingCheck;
61
72
  }
62
73
 
63
74
  /**
@@ -101,5 +112,6 @@ export class HealthCheck {
101
112
  */
102
113
  destroy(): void {
103
114
  this.stop();
115
+ this.pendingCheck = null;
104
116
  }
105
117
  }
@@ -76,7 +76,7 @@ export const useOffline = (config?: OfflineConfig) => {
76
76
 
77
77
  networkEvents.emit('change', networkState);
78
78
  previousStateRef.current = networkState;
79
- }, [store, previousStateRef]);
79
+ }, [store]);
80
80
 
81
81
  useEffect(() => {
82
82
  if (isInitialized.current) return;
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
1
  /**
3
2
  * useStorageState Hook Tests
4
3
  *
@@ -107,7 +106,7 @@ describe('useStorageState Hook', () => {
107
106
  const newValue = 'new-value';
108
107
 
109
108
  // Mock slow storage
110
- let resolveStorage: (value: string) => void;
109
+ let _resolveStorage: (value: string) => void;
111
110
  (AsyncStorage.getItem as jest.Mock).mockImplementation(() =>
112
111
  new Promise(resolve => {
113
112
  resolveStorage = resolve;
@@ -133,7 +132,7 @@ describe('useStorageState Hook', () => {
133
132
  const defaultValue = 'default';
134
133
 
135
134
  // Mock slow storage
136
- let resolveStorage: (value: string) => void;
135
+ let _resolveStorage: (value: string) => void;
137
136
  (AsyncStorage.getItem as jest.Mock).mockImplementation(() =>
138
137
  new Promise(resolve => {
139
138
  resolveStorage = resolve;
@@ -198,7 +197,7 @@ describe('useStorageState Hook', () => {
198
197
 
199
198
  await AsyncStorage.setItem(key, JSON.stringify('stored-value'));
200
199
 
201
- const { result, rerender, waitForNextUpdate } = renderHook(
200
+ const { result: _result, rerender, waitForNextUpdate } = renderHook(
202
201
  ({ defaultValue }) => useStorageState(key, defaultValue),
203
202
  { initialProps: { defaultValue: defaultValue1 } }
204
203
  );
@@ -52,7 +52,7 @@ export const useStorageState = <T>(
52
52
  return () => {
53
53
  isMountedRef.current = false;
54
54
  };
55
- }, [keyString]);
55
+ }, [keyString, defaultValue]);
56
56
 
57
57
  // Update state and persist to storage
58
58
  const updateState = useCallback(
@@ -22,6 +22,7 @@ interface ThemeState {
22
22
  isInitialized: boolean;
23
23
  _updateInProgress: boolean;
24
24
  _initInProgress: boolean;
25
+ _lastUpdateId?: number;
25
26
  }
26
27
 
27
28
  interface ThemeActions {
@@ -46,6 +47,7 @@ export const useTheme = createStore<ThemeState, ThemeActions>({
46
47
  isInitialized: false,
47
48
  _updateInProgress: false,
48
49
  _initInProgress: false,
50
+ _lastUpdateId: undefined,
49
51
  },
50
52
  persist: false,
51
53
  actions: (set, get) => ({
@@ -88,30 +90,44 @@ export const useTheme = createStore<ThemeState, ThemeActions>({
88
90
  setThemeMode: async (mode: ThemeMode) => {
89
91
  const { _updateInProgress } = get();
90
92
  if (_updateInProgress) return;
91
- set({ _updateInProgress: true });
93
+
94
+ const updateId = Date.now();
95
+ set({ _updateInProgress: true, _lastUpdateId: updateId });
92
96
 
93
97
  try {
94
98
  const theme = mode === 'light' ? lightTheme : darkTheme;
95
99
  set({ themeMode: mode, theme, isDark: mode === 'dark' });
96
100
  await ThemeStorage.setThemeMode(mode);
97
101
  useDesignSystemTheme.getState().setThemeMode(mode);
98
- } catch {
99
- // Silent failure
102
+ } catch (error) {
103
+ // Revert state on error
104
+ set({ _lastUpdateId: undefined });
105
+ if (__DEV__) {
106
+ console.error('[ThemeStore] Failed to set theme mode:', error);
107
+ }
108
+ throw error;
100
109
  } finally {
101
110
  set({ _updateInProgress: false });
102
111
  }
103
112
  },
104
113
 
105
114
  setCustomColors: async (colors?: CustomThemeColors) => {
106
- const { _updateInProgress } = get();
115
+ const { _updateInProgress, customColors: currentColors } = get();
107
116
  if (_updateInProgress) return;
108
- set({ _updateInProgress: true, customColors: colors });
117
+
118
+ const updateId = Date.now();
119
+ set({ _updateInProgress: true, _lastUpdateId: updateId, customColors: colors });
109
120
 
110
121
  try {
111
122
  await ThemeStorage.setCustomColors(colors);
112
123
  useDesignSystemTheme.getState().setCustomColors(colors);
113
- } catch {
114
- // Silent failure
124
+ } catch (error) {
125
+ // Revert to previous colors on error
126
+ set({ customColors: currentColors, _lastUpdateId: undefined });
127
+ if (__DEV__) {
128
+ console.error('[ThemeStore] Failed to set custom colors:', error);
129
+ }
130
+ throw error;
115
131
  } finally {
116
132
  set({ _updateInProgress: false });
117
133
  }
@@ -15,6 +15,7 @@ export class SimpleCache<T> {
15
15
  private defaultTTL: number;
16
16
  private cleanupTimeout: ReturnType<typeof setTimeout> | null = null;
17
17
  private destroyed = false;
18
+ private cleanupScheduleLock = false;
18
19
 
19
20
  constructor(defaultTTL: number = 60000) {
20
21
  this.defaultTTL = defaultTTL;
@@ -26,6 +27,7 @@ export class SimpleCache<T> {
26
27
  */
27
28
  destroy(): void {
28
29
  this.destroyed = true;
30
+ this.cleanupScheduleLock = false;
29
31
  if (this.cleanupTimeout) {
30
32
  clearTimeout(this.cleanupTimeout);
31
33
  this.cleanupTimeout = null;
@@ -76,16 +78,26 @@ export class SimpleCache<T> {
76
78
  }
77
79
 
78
80
  private scheduleCleanup(): void {
79
- if (this.destroyed) return;
81
+ if (this.destroyed || this.cleanupScheduleLock) return;
80
82
 
81
- if (this.cleanupTimeout) {
82
- clearTimeout(this.cleanupTimeout);
83
- }
83
+ this.cleanupScheduleLock = true;
84
84
 
85
- this.cleanup();
85
+ try {
86
+ if (this.cleanupTimeout) {
87
+ clearTimeout(this.cleanupTimeout);
88
+ }
89
+
90
+ this.cleanup();
86
91
 
87
- this.cleanupTimeout = setTimeout(() => {
88
- this.scheduleCleanup();
89
- }, 60000);
92
+ if (!this.destroyed) {
93
+ this.cleanupTimeout = setTimeout(() => {
94
+ this.cleanupScheduleLock = false;
95
+ this.scheduleCleanup();
96
+ }, 60000);
97
+ }
98
+ } catch (error) {
99
+ this.cleanupScheduleLock = false;
100
+ throw error;
101
+ }
90
102
  }
91
103
  }
@@ -1 +0,0 @@
1
- export * from './AppHeader';
@@ -1 +0,0 @@
1
- export * from './ScreenHeader';
@@ -1,2 +0,0 @@
1
- export * from './Divider';
2
- export * from './types';
@@ -1,3 +0,0 @@
1
-
2
- export * from './ActionFooter';
3
- export * from './types';
@@ -1,3 +0,0 @@
1
-
2
- export * from './HeroSection';
3
- export * from './types';