@umituz/react-native-design-system 4.27.0 → 4.27.2

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.27.0",
3
+ "version": "4.27.2",
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 - TanStack persistence and expo-image-manipulator now lazy loaded",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -46,7 +46,9 @@ export const isTablet = (): boolean => {
46
46
  return Device.deviceType === Device.DeviceType.TABLET;
47
47
  }
48
48
  // Fallback: Platform.isPad (iOS) or screen width >= 600dp (Android)
49
- if (Platform.OS === 'ios' && (Platform as any).isPad) return true;
49
+ // Platform.isPad is not in React Native types but exists on iOS
50
+ const platformWithIsPad = Platform as { isPad?: boolean };
51
+ if (Platform.OS === 'ios' && platformWithIsPad.isPad) return true;
50
52
  const { width, height } = getScreenDimensions();
51
53
  return Math.min(width, height) >= 600;
52
54
  };
@@ -46,7 +46,6 @@ export const useAnonymousUser = (
46
46
  ): UseAnonymousUserResult => {
47
47
  const {
48
48
  anonymousDisplayName = 'Anonymous',
49
- fallbackUserId = 'anonymous_fallback',
50
49
  } = options || {};
51
50
 
52
51
  const { data: anonymousUser, isLoading, error, execute } = useAsyncOperation<AnonymousUser, string>(
@@ -56,9 +55,14 @@ export const useAnonymousUser = (
56
55
  DeviceService.getUserFriendlyId(),
57
56
  ]);
58
57
 
58
+ // No fallback - if we can't get ID, let it error
59
+ if (!userId) {
60
+ throw new Error('Failed to generate device ID');
61
+ }
62
+
59
63
  return {
60
- userId: userId || fallbackUserId,
61
- deviceName: deviceName || 'Unknown Device',
64
+ userId,
65
+ deviceName: deviceName ?? 'Device',
62
66
  displayName: anonymousDisplayName,
63
67
  isAnonymous: true,
64
68
  };
@@ -66,16 +70,7 @@ export const useAnonymousUser = (
66
70
  {
67
71
  immediate: true,
68
72
  initialData: null,
69
- errorHandler: () => 'Failed to generate device ID',
70
- onError: () => {
71
- // Fallback on error - set default anonymous user
72
- return {
73
- userId: fallbackUserId,
74
- deviceName: 'Unknown Device',
75
- displayName: anonymousDisplayName,
76
- isAnonymous: true,
77
- };
78
- },
73
+ errorHandler: (err) => `Failed to generate device ID: ${err instanceof Error ? err.message : String(err)}`,
79
74
  }
80
75
  );
81
76
 
@@ -27,7 +27,13 @@ export const useCalendar = () => {
27
27
  const view = useCalendarView();
28
28
 
29
29
  // Utility functions - memoized to prevent recreating on every render
30
- const getEventsForDate = useCallback((date: Date) => {
30
+ const getEventsForDate = useCallback((date: Date | null | undefined) => {
31
+ if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
32
+ if (__DEV__) {
33
+ console.warn('[CalendarStore] getEventsForDate called with invalid date:', date);
34
+ }
35
+ return [];
36
+ }
31
37
  return events.events.filter(event => {
32
38
  const eventDate = new Date(event.date);
33
39
  return eventDate.toDateString() === date.toDateString();
@@ -35,6 +41,13 @@ export const useCalendar = () => {
35
41
  }, [events.events]);
36
42
 
37
43
  const getEventsForMonth = useCallback((year: number, month: number) => {
44
+ if (typeof year !== 'number' || typeof month !== 'number' ||
45
+ isNaN(year) || isNaN(month) || month < 0 || month > 11) {
46
+ if (__DEV__) {
47
+ console.warn('[CalendarStore] getEventsForMonth called with invalid year/month:', { year, month });
48
+ }
49
+ return [];
50
+ }
38
51
  return events.events.filter(event => {
39
52
  const eventDate = new Date(event.date);
40
53
  return eventDate.getFullYear() === year && eventDate.getMonth() === month;
@@ -1,4 +1,4 @@
1
- import { useNavigation, StackActions } from "@react-navigation/native";
1
+ import { useNavigation, StackActions, CommonActions } from "@react-navigation/native";
2
2
  import type { NavigationProp, ParamListBase } from "@react-navigation/native";
3
3
  import { useCallback, useMemo } from "react";
4
4
 
@@ -31,8 +31,13 @@ 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
35
- (navigation as any).navigate(screen, params);
34
+ // Dynamic navigation: use CommonActions for type-safe arbitrary screen navigation
35
+ navigation.dispatch(
36
+ CommonActions.navigate({
37
+ name: screen,
38
+ params,
39
+ })
40
+ );
36
41
  },
37
42
  [navigation]
38
43
  );
@@ -46,7 +46,7 @@ export type { NavigationCleanup } from "./utils/NavigationCleanup";
46
46
  export { AppNavigation } from "./utils/AppNavigation";
47
47
 
48
48
  export { TabLabel, type TabLabelProps } from "./components/TabLabel";
49
- export * from "./components/NavigationHeader";
49
+ export { NavigationHeader, type NavigationHeaderProps } from "./components/NavigationHeader";
50
50
  export { useTabBarStyles, type TabBarConfig } from "./hooks/useTabBarStyles";
51
51
  export { useTabConfig, type UseTabConfigProps } from "./hooks/useTabConfig";
52
52
 
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import React, { useMemo } from "react";
8
- import { View, Image as RNImage, StyleSheet } from "react-native";
8
+ import { View, Image as RNImage, StyleSheet, type ImageURISource, type ImageStyle } from "react-native";
9
9
  import { useSafeAreaInsets } from "../../../safe-area/hooks/useSafeAreaInsets";
10
10
  import {
11
11
  generateGridLayout,
@@ -93,8 +93,8 @@ export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
93
93
  return (
94
94
  <RNImage
95
95
  key={String(item.source)}
96
- source={item.source as any}
97
- style={item.style as any}
96
+ source={item.source as ImageURISource | number}
97
+ style={item.style as ImageStyle}
98
98
  resizeMode="cover"
99
99
  />
100
100
  );
@@ -52,7 +52,7 @@ export const OnboardingFooter = React.memo<OnboardingFooterProps>(({
52
52
  const progressFillStyle = useMemo(
53
53
  () => ({
54
54
  ...styles.progressFill,
55
- width: `${progressPercent}%` as any,
55
+ width: `${progressPercent}%` as `${number}%`,
56
56
  backgroundColor: colors.progressFillColor,
57
57
  }),
58
58
  [progressPercent, colors.progressFillColor]
@@ -20,10 +20,12 @@ export class CacheManager {
20
20
  }
21
21
 
22
22
  getCache<T>(name: string, config?: CacheConfig): Cache<T> {
23
- if (!this.caches.has(name)) {
24
- this.caches.set(name, new Cache<T>(config));
23
+ let cache = this.caches.get(name);
24
+ if (!cache) {
25
+ cache = new Cache<T>(config);
26
+ this.caches.set(name, cache);
25
27
  }
26
- return this.caches.get(name)!;
28
+ return cache;
27
29
  }
28
30
 
29
31
  deleteCache(name: string): boolean {
@@ -25,7 +25,12 @@ export function useCachedValue<T>(
25
25
  return cached;
26
26
  }
27
27
 
28
- const data = await fetcherRef.current!();
28
+ const fetcherFn = fetcherRef.current;
29
+ if (!fetcherFn) {
30
+ throw new Error('Fetcher function is not defined');
31
+ }
32
+
33
+ const data = await fetcherFn();
29
34
  cache.set(key, data, configRef.current?.ttl);
30
35
  return data;
31
36
  },
@@ -142,10 +142,25 @@ export function usePersistentCache<T>(
142
142
 
143
143
  const setData = useCallback(
144
144
  async (value: T) => {
145
- await cacheOps.saveToStorage(key, value, { ttl, version, enabled });
145
+ // Optimistic update
146
+ const previousData = state.data;
146
147
  stableActionsRef.current.setData(value);
148
+
149
+ try {
150
+ await cacheOps.saveToStorage(key, value, { ttl, version, enabled });
151
+ } catch (error) {
152
+ // Rollback on error
153
+ if (previousData !== null) {
154
+ stableActionsRef.current.setData(previousData);
155
+ } else {
156
+ stableActionsRef.current.clearData();
157
+ }
158
+ if (__DEV__) {
159
+ console.warn('[usePersistentCache] Failed to save to storage, rolling back:', error);
160
+ }
161
+ }
147
162
  },
148
- [key, ttl, version, enabled, cacheOps],
163
+ [key, ttl, version, enabled, cacheOps, state.data],
149
164
  );
150
165
 
151
166
  const clearData = useCallback(async () => {
@@ -57,10 +57,21 @@ export const useStorageState = <T>(
57
57
  // Update state and persist to storage
58
58
  const updateState = useCallback(
59
59
  async (value: T) => {
60
+ // Optimistic update
61
+ const previousValue = state;
60
62
  setState(value);
61
- await storageRepository.setItem(keyString, value);
63
+
64
+ try {
65
+ await storageRepository.setItem(keyString, value);
66
+ } catch (error) {
67
+ // Rollback on error
68
+ setState(previousValue);
69
+ if (__DEV__) {
70
+ console.warn('[useStorageState] Failed to persist state, rolling back:', error);
71
+ }
72
+ }
62
73
  },
63
- [keyString]
74
+ [keyString, state]
64
75
  );
65
76
 
66
77
  return [state, updateState, isLoading];
@@ -188,8 +188,6 @@ class DevMonitorClass {
188
188
  reset(): void {
189
189
  if (!this.isEnabled) return;
190
190
  this.detach();
191
- this.stopStatsLogging();
192
- this.clear();
193
191
  if (this.options.enableLogging) {
194
192
  DevMonitorLogger.logReset();
195
193
  }
@@ -9,6 +9,7 @@ import type { CustomThemeColors } from '../../core/CustomColors';
9
9
  import type { SplashScreenProps } from '../../../molecules/splash/types';
10
10
  import { useIconStore } from '../../../atoms/icon/iconStore';
11
11
  import type { IconRenderer, IconNames } from '../../../atoms/icon/iconStore';
12
+ import { FIVE_SECONDS_MS } from '../../../utils/constants/TimeConstants';
12
13
 
13
14
  // Lazy load SplashScreen to avoid circular dependency
14
15
  const SplashScreen = lazy(() => import('../../../molecules/splash').then(m => ({ default: m.SplashScreen })));
@@ -82,7 +83,7 @@ export const DesignSystemProvider: React.FC<DesignSystemProviderProps> = ({
82
83
  if (!prev) onError?.(new Error('DesignSystemProvider initialization timed out'));
83
84
  return true;
84
85
  });
85
- }, 5000);
86
+ }, FIVE_SECONDS_MS);
86
87
 
87
88
  initialize()
88
89
  .then(() => {
@@ -12,6 +12,26 @@ import { useDesignSystemTheme } from '../globalThemeStore';
12
12
  import type { ThemeMode } from '../../core/ColorPalette';
13
13
  import type { CustomThemeColors } from '../../core/CustomColors';
14
14
 
15
+ /**
16
+ * Shallow equality check for CustomThemeColors
17
+ * Compares all defined properties without deep object traversal
18
+ */
19
+ function areCustomColorsEqual(a?: CustomThemeColors, b?: CustomThemeColors): boolean {
20
+ if (a === b) return true;
21
+ if (!a || !b) return false;
22
+
23
+ const keysA = Object.keys(a) as (keyof CustomThemeColors)[];
24
+ const keysB = Object.keys(b) as (keyof CustomThemeColors)[];
25
+
26
+ if (keysA.length !== keysB.length) return false;
27
+
28
+ for (const key of keysA) {
29
+ if (a[key] !== b[key]) return false;
30
+ }
31
+
32
+ return true;
33
+ }
34
+
15
35
  interface ThemeState {
16
36
  theme: Theme;
17
37
  themeMode: ThemeMode;
@@ -118,8 +138,8 @@ export const useTheme = createStore<ThemeState, ThemeActions>({
118
138
  const { _updateInProgress, customColors: currentColors } = get();
119
139
  if (_updateInProgress) return;
120
140
 
121
- // Deep comparison to avoid redundant updates from new object references
122
- if (JSON.stringify(colors) === JSON.stringify(currentColors)) return;
141
+ // Shallow comparison to avoid redundant updates from new object references
142
+ if (areCustomColorsEqual(colors, currentColors)) return;
123
143
 
124
144
  const updateId = Date.now();
125
145
  set({ _updateInProgress: true, _lastUpdateId: updateId, customColors: colors });
@@ -5,6 +5,8 @@
5
5
  * No external dependencies - pure TypeScript implementation
6
6
  */
7
7
 
8
+ import { ONE_MINUTE_MS } from '../../../utils/constants/TimeConstants';
9
+
8
10
  interface CacheEntry<T> {
9
11
  value: T;
10
12
  expires: number;
@@ -17,7 +19,7 @@ export class SimpleCache<T> {
17
19
  private destroyed = false;
18
20
  private cleanupScheduleLock = false;
19
21
 
20
- constructor(defaultTTL: number = 60000) {
22
+ constructor(defaultTTL: number = ONE_MINUTE_MS) {
21
23
  this.defaultTTL = defaultTTL;
22
24
  this.scheduleCleanup();
23
25
  }
@@ -91,9 +93,13 @@ export class SimpleCache<T> {
91
93
 
92
94
  if (!this.destroyed) {
93
95
  this.cleanupTimeout = setTimeout(() => {
94
- this.cleanupScheduleLock = false;
95
- this.scheduleCleanup();
96
- }, 60000);
96
+ if (!this.destroyed) {
97
+ this.cleanupScheduleLock = false;
98
+ this.scheduleCleanup();
99
+ }
100
+ }, ONE_MINUTE_MS);
101
+ } else {
102
+ this.cleanupScheduleLock = false;
97
103
  }
98
104
  } catch (error) {
99
105
  this.cleanupScheduleLock = false;
@@ -53,7 +53,10 @@ export function getTextColor(
53
53
 
54
54
  const cacheKey = `${color}_${Object.keys(tokens.colors).length}_${tokens.colors.textPrimary}`;
55
55
 
56
- if (colorCache.has(cacheKey)) return colorCache.get(cacheKey)!;
56
+ if (colorCache.has(cacheKey)) {
57
+ const cached = colorCache.get(cacheKey);
58
+ if (cached) return cached;
59
+ }
57
60
 
58
61
  const colorKey = COLOR_MAP[color as ColorVariant] ?? 'textPrimary';
59
62
  const resolvedColor = tokens.colors[colorKey];
@@ -112,7 +112,8 @@ export function getTextStyle(
112
112
 
113
113
  // Check cache first
114
114
  if (typographyCache.has(cacheKey)) {
115
- return typographyCache.get(cacheKey)!;
115
+ const cached = typographyCache.get(cacheKey);
116
+ if (cached) return cached;
116
117
  }
117
118
 
118
119
  // Resolve style and cache it
@@ -5,6 +5,8 @@
5
5
  * Useful for network requests, file operations, etc.
6
6
  */
7
7
 
8
+ import { DEFAULT_LONG_TIMEOUT_MS, ONE_SECOND_MS, TEN_SECONDS_MS } from '../constants/TimeConstants';
9
+
8
10
  export interface RetryOptions {
9
11
  /**
10
12
  * Maximum number of retry attempts
@@ -59,14 +61,14 @@ export async function retryWithBackoff<T>(
59
61
  ): Promise<T> {
60
62
  const {
61
63
  maxRetries = 3,
62
- baseDelay = 1000,
63
- maxDelay = 10000,
64
+ baseDelay = ONE_SECOND_MS,
65
+ maxDelay = TEN_SECONDS_MS,
64
66
  backoffMultiplier = 2,
65
67
  shouldRetry = () => true,
66
68
  onRetry,
67
69
  } = options;
68
70
 
69
- let lastError: Error;
71
+ let lastError: Error | undefined;
70
72
 
71
73
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
72
74
  try {
@@ -106,7 +108,7 @@ export async function retryWithBackoff<T>(
106
108
  }
107
109
 
108
110
  // This should never be reached, but TypeScript needs it
109
- throw lastError!;
111
+ throw lastError ?? new Error('Retry operation failed with unknown error');
110
112
  }
111
113
 
112
114
  /**
@@ -125,7 +127,7 @@ export async function retryWithTimeout<T>(
125
127
  fn: () => Promise<T>,
126
128
  options: RetryOptions & { timeout?: number } = {}
127
129
  ): Promise<T> {
128
- const { timeout = 30000, ...retryOptions } = options;
130
+ const { timeout = DEFAULT_LONG_TIMEOUT_MS, ...retryOptions } = options;
129
131
 
130
132
  return retryWithBackoff(
131
133
  () => withTimeout(fn(), timeout),
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Time Constants
3
+ *
4
+ * Centralized time-related constants to replace magic numbers
5
+ */
6
+
7
+ export const MILLISECONDS_PER_SECOND = 1000;
8
+ export const MILLISECONDS_PER_MINUTE = 60 * 1000; // 60000
9
+ export const MILLISECONDS_PER_HOUR = 60 * 60 * 1000; // 3600000
10
+ export const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; // 86400000
11
+
12
+ export const SECONDS_PER_MINUTE = 60;
13
+ export const SECONDS_PER_HOUR = 60 * 60;
14
+ export const SECONDS_PER_DAY = 24 * 60 * 60;
15
+
16
+ export const MINUTES_PER_HOUR = 60;
17
+ export const MINUTES_PER_DAY = 24 * 60;
18
+
19
+ // Common time intervals
20
+ export const ONE_SECOND_MS = MILLISECONDS_PER_SECOND;
21
+ export const FIVE_SECONDS_MS = 5 * MILLISECONDS_PER_SECOND;
22
+ export const TEN_SECONDS_MS = 10 * MILLISECONDS_PER_SECOND;
23
+ export const THIRTY_SECONDS_MS = 30 * MILLISECONDS_PER_SECOND;
24
+ export const ONE_MINUTE_MS = MILLISECONDS_PER_MINUTE;
25
+ export const FIVE_MINUTES_MS = 5 * MILLISECONDS_PER_MINUTE;
26
+ export const TEN_MINUTES_MS = 10 * MILLISECONDS_PER_MINUTE;
27
+ export const THIRTY_MINUTES_MS = 30 * MILLISECONDS_PER_MINUTE;
28
+ export const ONE_HOUR_MS = MILLISECONDS_PER_HOUR;
29
+ export const ONE_DAY_MS = MILLISECONDS_PER_DAY;
30
+
31
+ // Default timeouts
32
+ export const DEFAULT_TIMEOUT_MS = FIVE_SECONDS_MS;
33
+ export const DEFAULT_LONG_TIMEOUT_MS = THIRTY_SECONDS_MS;
34
+ export const DEFAULT_CACHE_TTL_MS = ONE_MINUTE_MS;
@@ -60,8 +60,9 @@ export class DesignSystemError extends Error {
60
60
  this.retryable = metadata?.retryable ?? false;
61
61
 
62
62
  // Maintains proper stack trace for where our error was thrown (only available on V8)
63
- if (typeof (Error as any).captureStackTrace === 'function') {
64
- (Error as any).captureStackTrace(this, DesignSystemError);
63
+ const ErrorConstructor = Error as { captureStackTrace?: (error: Error, constructor: typeof DesignSystemError) => void };
64
+ if (typeof ErrorConstructor.captureStackTrace === 'function') {
65
+ ErrorConstructor.captureStackTrace(this, DesignSystemError);
65
66
  }
66
67
  }
67
68
 
@@ -100,8 +100,11 @@ class Logger {
100
100
  * Use for performance measurements
101
101
  */
102
102
  time(label: string): void {
103
- if (this.isDev && (console as any).time) {
104
- (console as any).time(label);
103
+ if (this.isDev) {
104
+ const consoleWithTime = console as { time?: (label: string) => void };
105
+ if (typeof consoleWithTime.time === 'function') {
106
+ consoleWithTime.time(label);
107
+ }
105
108
  }
106
109
  }
107
110
 
@@ -109,8 +112,11 @@ class Logger {
109
112
  * End time measurement - only in development
110
113
  */
111
114
  timeEnd(label: string): void {
112
- if (this.isDev && (console as any).timeEnd) {
113
- (console as any).timeEnd(label);
115
+ if (this.isDev) {
116
+ const consoleWithTimeEnd = console as { timeEnd?: (label: string) => void };
117
+ if (typeof consoleWithTimeEnd.timeEnd === 'function') {
118
+ consoleWithTimeEnd.timeEnd(label);
119
+ }
114
120
  }
115
121
  }
116
122