@umituz/react-native-design-system 4.23.113 → 4.23.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/atoms/AtomicTouchable.tsx +22 -0
  3. package/src/atoms/badge/AtomicBadge.tsx +26 -28
  4. package/src/atoms/chip/AtomicChip.tsx +5 -5
  5. package/src/atoms/datepicker/components/DatePickerModal.tsx +4 -3
  6. package/src/atoms/input/hooks/useInputState.ts +1 -1
  7. package/src/atoms/picker/components/PickerModal.tsx +1 -1
  8. package/src/atoms/picker/hooks/usePickerState.ts +28 -15
  9. package/src/atoms/skeleton/AtomicSkeleton.tsx +5 -5
  10. package/src/device/infrastructure/services/DeviceCapabilityService.ts +1 -12
  11. package/src/filesystem/infrastructure/services/directory.service.ts +37 -9
  12. package/src/filesystem/infrastructure/services/download.service.ts +62 -11
  13. package/src/filesystem/infrastructure/services/file-manager.service.ts +42 -11
  14. package/src/filesystem/infrastructure/services/file-writer.service.ts +8 -3
  15. package/src/media/infrastructure/services/MediaPickerService.ts +32 -8
  16. package/src/media/infrastructure/services/MediaSaveService.ts +7 -2
  17. package/src/media/presentation/hooks/useMedia.ts +60 -22
  18. package/src/molecules/BaseModal.tsx +1 -0
  19. package/src/molecules/ConfirmationModalMain.tsx +1 -0
  20. package/src/molecules/ListItem.tsx +15 -1
  21. package/src/molecules/avatar/Avatar.tsx +28 -11
  22. package/src/molecules/bottom-sheet/components/BottomSheet.tsx +1 -0
  23. package/src/molecules/calendar/presentation/components/AtomicCalendar.tsx +1 -1
  24. package/src/responsive/useResponsive.ts +1 -1
  25. package/src/services/api/ApiClient.ts +37 -6
  26. package/src/storage/presentation/hooks/usePersistentCache.ts +20 -12
  27. package/src/storage/presentation/hooks/useStore.ts +1 -0
  28. package/src/tanstack/presentation/hooks/usePrefetch.ts +14 -0
  29. package/src/theme/infrastructure/stores/themeStore.ts +13 -11
  30. package/src/timezone/infrastructure/services/BusinessCalendarManager.ts +1 -0
  31. package/src/timezone/infrastructure/services/CalendarManager.ts +2 -2
  32. package/src/timezone/infrastructure/services/DateComparisonUtils.ts +1 -0
  33. package/src/timezone/infrastructure/services/DateFormatter.ts +3 -2
  34. package/src/timezone/infrastructure/services/DateRangeUtils.ts +1 -0
  35. package/src/timezone/infrastructure/utils/TimezoneParsers.ts +27 -0
  36. package/src/utilities/sharing/presentation/hooks/useSharing.ts +44 -17
  37. package/src/utils/async/index.ts +12 -0
  38. package/src/utils/async/retryWithBackoff.ts +177 -0
  39. package/src/utils/errors/DesignSystemError.ts +117 -0
  40. package/src/utils/errors/ErrorHandler.ts +137 -0
  41. package/src/utils/errors/index.ts +7 -0
@@ -20,6 +20,8 @@ interface ThemeState {
20
20
  defaultThemeMode: ThemeMode;
21
21
  isDark: boolean;
22
22
  isInitialized: boolean;
23
+ _updateInProgress: boolean;
24
+ _initInProgress: boolean;
23
25
  }
24
26
 
25
27
  interface ThemeActions {
@@ -32,9 +34,6 @@ interface ThemeActions {
32
34
  initialize: () => Promise<void>;
33
35
  }
34
36
 
35
- let themeUpdateInProgress = false;
36
- let themeInitInProgress = false;
37
-
38
37
  export const useTheme = createStore<ThemeState, ThemeActions>({
39
38
  name: 'theme-store',
40
39
  initialState: {
@@ -45,14 +44,16 @@ export const useTheme = createStore<ThemeState, ThemeActions>({
45
44
  defaultThemeMode: 'dark',
46
45
  isDark: true,
47
46
  isInitialized: false,
47
+ _updateInProgress: false,
48
+ _initInProgress: false,
48
49
  },
49
50
  persist: false,
50
51
  actions: (set, get) => ({
51
52
  initialize: async () => {
52
- const { isInitialized, customColors: currentColors, defaultThemeMode } = get();
53
- if (isInitialized || themeInitInProgress) return;
53
+ const { isInitialized, _initInProgress, customColors: currentColors, defaultThemeMode } = get();
54
+ if (isInitialized || _initInProgress) return;
54
55
 
55
- themeInitInProgress = true;
56
+ set({ _initInProgress: true });
56
57
 
57
58
  try {
58
59
  const [savedMode, savedColors] = await Promise.all([
@@ -77,16 +78,17 @@ export const useTheme = createStore<ThemeState, ThemeActions>({
77
78
  dsTheme.setThemeMode(mode);
78
79
  dsTheme.setCustomColors(colors);
79
80
  } catch {
80
- set({ isInitialized: true });
81
+ set({ isInitialized: true, _initInProgress: false });
81
82
  useDesignSystemTheme.getState().setThemeMode(defaultThemeMode);
82
83
  } finally {
83
- themeInitInProgress = false;
84
+ set({ _initInProgress: false });
84
85
  }
85
86
  },
86
87
 
87
88
  setThemeMode: async (mode: ThemeMode) => {
88
- if (themeUpdateInProgress) return;
89
- themeUpdateInProgress = true;
89
+ const { _updateInProgress } = get();
90
+ if (_updateInProgress) return;
91
+ set({ _updateInProgress: true });
90
92
 
91
93
  try {
92
94
  const theme = mode === 'light' ? lightTheme : darkTheme;
@@ -96,7 +98,7 @@ export const useTheme = createStore<ThemeState, ThemeActions>({
96
98
  } catch {
97
99
  // Silent failure
98
100
  } finally {
99
- themeUpdateInProgress = false;
101
+ set({ _updateInProgress: false });
100
102
  }
101
103
  },
102
104
 
@@ -4,6 +4,7 @@
4
4
  * Business date utilities for work days and month boundaries
5
5
  * Handles weekend detection and business day calculations
6
6
  */
7
+ import { parseDate } from '../utils/TimezoneParsers';
7
8
 
8
9
  export class BusinessCalendarManager {
9
10
  /**
@@ -1,4 +1,5 @@
1
1
  import { TimezoneCalendarDay } from '../../domain/entities/Timezone';
2
+ import { parseDate } from '../utils/TimezoneParsers';
2
3
 
3
4
  /**
4
5
  * CalendarManager
@@ -123,8 +124,7 @@ export class CalendarManager {
123
124
  }
124
125
 
125
126
  parse(date: Date | string | number): Date {
126
- if (date instanceof Date) return new Date(date.getTime());
127
- return new Date(date);
127
+ return parseDate(date);
128
128
  }
129
129
 
130
130
  isValid(date: Date | string | number): boolean {
@@ -4,6 +4,7 @@
4
4
  * Precise date comparison utilities and relative time formatting
5
5
  * Handles hour/minute precision comparisons and "from now" formatting
6
6
  */
7
+ import { parseDate } from '../utils/TimezoneParsers';
7
8
 
8
9
  export class DateComparisonUtils {
9
10
  /**
@@ -2,6 +2,8 @@
2
2
  * DateFormatter
3
3
  * Handles locale-aware formatting of dates and times
4
4
  */
5
+ import { parseDate } from '../utils/TimezoneParsers';
6
+
5
7
  export class DateFormatter {
6
8
  formatDate(
7
9
  date: Date | string | number,
@@ -98,8 +100,7 @@ export class DateFormatter {
98
100
  }
99
101
 
100
102
  parse(date: Date | string | number): Date {
101
- if (date instanceof Date) return new Date(date.getTime());
102
- return new Date(date);
103
+ return parseDate(date);
103
104
  }
104
105
 
105
106
  formatDuration(milliseconds: number): string {
@@ -4,6 +4,7 @@
4
4
  * Date range utilities for working with date intervals
5
5
  * Handles range generation, overlap detection, and clamping
6
6
  */
7
+ import { parseDate } from '../utils/TimezoneParsers';
7
8
 
8
9
  export class DateRangeUtils {
9
10
  /**
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Timezone Parsers Utility
3
+ *
4
+ * Shared parsing functions for timezone services.
5
+ * Extracted from duplicate methods across DateFormatter, CalendarManager,
6
+ * BusinessCalendarManager, DateRangeUtils, and DateComparisonUtils.
7
+ */
8
+
9
+ /**
10
+ * Parse date from various input types
11
+ * Ensures a Date object is returned from Date, string, or number input
12
+ */
13
+ export function parseDate(date: Date | string | number): Date {
14
+ if (date instanceof Date) return new Date(date.getTime());
15
+ return new Date(date);
16
+ }
17
+
18
+ /**
19
+ * Parse timezone offset string to number
20
+ * @param offset - Offset string (e.g., "+05:30", "-08:00")
21
+ * @returns Offset in minutes
22
+ */
23
+ export function parseTimezoneOffset(offset: string): number {
24
+ const sign = offset[0] === '-' ? -1 : 1;
25
+ const [hours, minutes] = offset.slice(1).split(':').map(Number);
26
+ return sign * (hours * 60 + (minutes || 0));
27
+ }
@@ -8,7 +8,7 @@
8
8
  * @layer presentation/hooks
9
9
  */
10
10
 
11
- import { useState, useCallback, useEffect, useMemo } from 'react';
11
+ import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
12
12
  import { SharingService } from '../../infrastructure/services/SharingService';
13
13
  import type { ShareOptions } from '../../domain/entities/Share';
14
14
 
@@ -47,29 +47,40 @@ export const useSharing = () => {
47
47
  const [isSharing, setIsSharing] = useState(false);
48
48
  const [error, setError] = useState<string | null>(null);
49
49
 
50
+ // Track mounted state to prevent setState on unmounted component
51
+ const isMountedRef = useRef(true);
52
+
50
53
  /**
51
54
  * Check sharing availability on mount
52
55
  */
53
56
  useEffect(() => {
54
57
  const checkAvailability = async () => {
55
58
  const available = await SharingService.isAvailable();
56
- setIsAvailable(available);
59
+ if (isMountedRef.current) {
60
+ setIsAvailable(available);
61
+ }
57
62
  };
58
63
 
59
64
  checkAvailability();
65
+
66
+ return () => {
67
+ isMountedRef.current = false;
68
+ };
60
69
  }, []);
61
70
 
62
71
  /**
63
72
  * Share a file via system share sheet
64
73
  */
65
74
  const share = useCallback(async (uri: string, options?: ShareOptions): Promise<boolean> => {
66
- setIsSharing(true);
67
- setError(null);
75
+ if (isMountedRef.current) {
76
+ setIsSharing(true);
77
+ setError(null);
78
+ }
68
79
 
69
80
  try {
70
81
  const result = await SharingService.shareFile(uri, options);
71
82
 
72
- if (!result.success) {
83
+ if (!result.success && isMountedRef.current) {
73
84
  setError(result.error || 'Failed to share file');
74
85
  return false;
75
86
  }
@@ -77,10 +88,14 @@ export const useSharing = () => {
77
88
  return true;
78
89
  } catch (err) {
79
90
  const errorMessage = err instanceof Error ? err.message : 'Failed to share file';
80
- setError(errorMessage);
91
+ if (isMountedRef.current) {
92
+ setError(errorMessage);
93
+ }
81
94
  return false;
82
95
  } finally {
83
- setIsSharing(false);
96
+ if (isMountedRef.current) {
97
+ setIsSharing(false);
98
+ }
84
99
  }
85
100
  }, []);
86
101
 
@@ -89,13 +104,15 @@ export const useSharing = () => {
89
104
  */
90
105
  const shareWithAutoType = useCallback(
91
106
  async (uri: string, filename: string, dialogTitle?: string): Promise<boolean> => {
92
- setIsSharing(true);
93
- setError(null);
107
+ if (isMountedRef.current) {
108
+ setIsSharing(true);
109
+ setError(null);
110
+ }
94
111
 
95
112
  try {
96
113
  const result = await SharingService.shareWithAutoType(uri, filename, dialogTitle);
97
114
 
98
- if (!result.success) {
115
+ if (!result.success && isMountedRef.current) {
99
116
  setError(result.error || 'Failed to share file');
100
117
  return false;
101
118
  }
@@ -103,10 +120,14 @@ export const useSharing = () => {
103
120
  return true;
104
121
  } catch (err) {
105
122
  const errorMessage = err instanceof Error ? err.message : 'Failed to share file';
106
- setError(errorMessage);
123
+ if (isMountedRef.current) {
124
+ setError(errorMessage);
125
+ }
107
126
  return false;
108
127
  } finally {
109
- setIsSharing(false);
128
+ if (isMountedRef.current) {
129
+ setIsSharing(false);
130
+ }
110
131
  }
111
132
  },
112
133
  []
@@ -117,13 +138,15 @@ export const useSharing = () => {
117
138
  */
118
139
  const shareMultiple = useCallback(
119
140
  async (uris: string[], options?: ShareOptions): Promise<boolean> => {
120
- setIsSharing(true);
121
- setError(null);
141
+ if (isMountedRef.current) {
142
+ setIsSharing(true);
143
+ setError(null);
144
+ }
122
145
 
123
146
  try {
124
147
  const result = await SharingService.shareMultipleFiles(uris, options);
125
148
 
126
- if (!result.success) {
149
+ if (!result.success && isMountedRef.current) {
127
150
  setError(result.error || 'Failed to share files');
128
151
  return false;
129
152
  }
@@ -131,10 +154,14 @@ export const useSharing = () => {
131
154
  return true;
132
155
  } catch (err) {
133
156
  const errorMessage = err instanceof Error ? err.message : 'Failed to share files';
134
- setError(errorMessage);
157
+ if (isMountedRef.current) {
158
+ setError(errorMessage);
159
+ }
135
160
  return false;
136
161
  } finally {
137
- setIsSharing(false);
162
+ if (isMountedRef.current) {
163
+ setIsSharing(false);
164
+ }
138
165
  }
139
166
  },
140
167
  []
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Async Utilities
3
+ * Retry and timeout utilities for async operations
4
+ */
5
+
6
+ export {
7
+ retryWithBackoff,
8
+ retryWithTimeout,
9
+ isNetworkError,
10
+ isRetryableHttpStatus,
11
+ type RetryOptions,
12
+ } from './retryWithBackoff';
@@ -0,0 +1,177 @@
1
+ /**
2
+ * retryWithBackoff
3
+ *
4
+ * Retry utility with exponential backoff for async operations.
5
+ * Useful for network requests, file operations, etc.
6
+ */
7
+
8
+ import { ErrorHandler } from '../errors/ErrorHandler';
9
+
10
+ export interface RetryOptions {
11
+ /**
12
+ * Maximum number of retry attempts
13
+ * @default 3
14
+ */
15
+ maxRetries?: number;
16
+
17
+ /**
18
+ * Initial delay in milliseconds before first retry
19
+ * @default 1000
20
+ */
21
+ baseDelay?: number;
22
+
23
+ /**
24
+ * Maximum delay in milliseconds (caps exponential growth)
25
+ * @default 10000
26
+ */
27
+ maxDelay?: number;
28
+
29
+ /**
30
+ * Multiplier for exponential backoff
31
+ * @default 2
32
+ */
33
+ backoffMultiplier?: number;
34
+
35
+ /**
36
+ * Function to determine if error is retryable
37
+ * @default () => true (retry all errors)
38
+ */
39
+ shouldRetry?: (error: Error, attempt: number) => boolean;
40
+
41
+ /**
42
+ * Callback on each retry attempt
43
+ */
44
+ onRetry?: (error: Error, attempt: number, delay: number) => void;
45
+ }
46
+
47
+ /**
48
+ * Retry an async function with exponential backoff
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * const result = await retryWithBackoff(
53
+ * () => fetch('https://api.example.com'),
54
+ * { maxRetries: 3, baseDelay: 1000 }
55
+ * );
56
+ * ```
57
+ */
58
+ export async function retryWithBackoff<T>(
59
+ fn: () => Promise<T>,
60
+ options: RetryOptions = {}
61
+ ): Promise<T> {
62
+ const {
63
+ maxRetries = 3,
64
+ baseDelay = 1000,
65
+ maxDelay = 10000,
66
+ backoffMultiplier = 2,
67
+ shouldRetry = () => true,
68
+ onRetry,
69
+ } = options;
70
+
71
+ let lastError: Error;
72
+
73
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
74
+ try {
75
+ // Attempt the operation
76
+ return await fn();
77
+ } catch (error) {
78
+ lastError = error instanceof Error ? error : new Error(String(error));
79
+
80
+ // Check if we should retry
81
+ const isLastAttempt = attempt === maxRetries;
82
+ if (isLastAttempt || !shouldRetry(lastError, attempt)) {
83
+ throw lastError;
84
+ }
85
+
86
+ // Calculate delay with exponential backoff
87
+ const delay = Math.min(
88
+ baseDelay * Math.pow(backoffMultiplier, attempt),
89
+ maxDelay
90
+ );
91
+
92
+ // Call onRetry callback if provided
93
+ if (onRetry) {
94
+ onRetry(lastError, attempt + 1, delay);
95
+ }
96
+
97
+ // Log retry in development
98
+ if (__DEV__) {
99
+ console.log(
100
+ `[Retry] Attempt ${attempt + 1}/${maxRetries} failed. Retrying in ${delay}ms...`,
101
+ lastError.message
102
+ );
103
+ }
104
+
105
+ // Wait before retrying
106
+ await new Promise((resolve) => setTimeout(resolve, delay));
107
+ }
108
+ }
109
+
110
+ // This should never be reached, but TypeScript needs it
111
+ throw lastError!;
112
+ }
113
+
114
+ /**
115
+ * Retry with timeout
116
+ * Combines retry logic with a timeout
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * const result = await retryWithTimeout(
121
+ * () => fetch('https://api.example.com'),
122
+ * { timeout: 5000, maxRetries: 3 }
123
+ * );
124
+ * ```
125
+ */
126
+ export async function retryWithTimeout<T>(
127
+ fn: () => Promise<T>,
128
+ options: RetryOptions & { timeout?: number } = {}
129
+ ): Promise<T> {
130
+ const { timeout = 30000, ...retryOptions } = options;
131
+
132
+ return retryWithBackoff(
133
+ () => withTimeout(fn(), timeout),
134
+ retryOptions
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Add timeout to a promise
140
+ */
141
+ function withTimeout<T>(
142
+ promise: Promise<T>,
143
+ timeoutMs: number
144
+ ): Promise<T> {
145
+ return Promise.race([
146
+ promise,
147
+ new Promise<T>((_, reject) =>
148
+ setTimeout(
149
+ () => reject(new Error(`Operation timed out after ${timeoutMs}ms`)),
150
+ timeoutMs
151
+ )
152
+ ),
153
+ ]);
154
+ }
155
+
156
+ /**
157
+ * Check if error is a network error
158
+ * Useful for shouldRetry callback
159
+ */
160
+ export function isNetworkError(error: Error): boolean {
161
+ return (
162
+ error.message.includes('network') ||
163
+ error.message.includes('timeout') ||
164
+ error.message.includes('fetch') ||
165
+ error.message.includes('ECONNREFUSED') ||
166
+ error.message.includes('ETIMEDOUT')
167
+ );
168
+ }
169
+
170
+ /**
171
+ * Check if error is retryable HTTP status
172
+ * Useful for shouldRetry callback
173
+ */
174
+ export function isRetryableHttpStatus(status: number): boolean {
175
+ // Retry on 5xx server errors and 429 (rate limit)
176
+ return status >= 500 || status === 429 || status === 408;
177
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * DesignSystemError
3
+ *
4
+ * Unified error class for the design system package.
5
+ * Provides consistent error handling with error codes and context.
6
+ */
7
+
8
+ export class DesignSystemError extends Error {
9
+ /**
10
+ * Error code for categorization
11
+ */
12
+ public readonly code: string;
13
+
14
+ /**
15
+ * Additional context about the error
16
+ */
17
+ public readonly context?: Record<string, any>;
18
+
19
+ /**
20
+ * Timestamp when error was created
21
+ */
22
+ public readonly timestamp: Date;
23
+
24
+ constructor(
25
+ message: string,
26
+ code: string,
27
+ context?: Record<string, any>
28
+ ) {
29
+ super(message);
30
+ this.name = 'DesignSystemError';
31
+ this.code = code;
32
+ this.context = context;
33
+ this.timestamp = new Date();
34
+
35
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
36
+ if (Error.captureStackTrace) {
37
+ Error.captureStackTrace(this, DesignSystemError);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Convert error to JSON for logging/debugging
43
+ */
44
+ toJSON(): Record<string, any> {
45
+ return {
46
+ name: this.name,
47
+ message: this.message,
48
+ code: this.code,
49
+ context: this.context,
50
+ timestamp: this.timestamp.toISOString(),
51
+ stack: this.stack,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Get user-friendly error message
57
+ */
58
+ getUserMessage(): string {
59
+ // You can customize user-facing messages based on error codes
60
+ switch (this.code) {
61
+ case 'FILE_NOT_FOUND':
62
+ return 'The requested file could not be found.';
63
+ case 'PERMISSION_DENIED':
64
+ return 'Permission denied. Please check app permissions.';
65
+ case 'NETWORK_ERROR':
66
+ return 'Network error. Please check your connection.';
67
+ case 'STORAGE_FULL':
68
+ return 'Storage is full. Please free up some space.';
69
+ case 'INVALID_INPUT':
70
+ return 'Invalid input provided.';
71
+ default:
72
+ return this.message;
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Common error codes used across the design system
79
+ */
80
+ export const ErrorCodes = {
81
+ // File system errors
82
+ FILE_NOT_FOUND: 'FILE_NOT_FOUND',
83
+ FILE_READ_ERROR: 'FILE_READ_ERROR',
84
+ FILE_WRITE_ERROR: 'FILE_WRITE_ERROR',
85
+ FILE_DELETE_ERROR: 'FILE_DELETE_ERROR',
86
+ DIRECTORY_CREATE_ERROR: 'DIRECTORY_CREATE_ERROR',
87
+ PERMISSION_DENIED: 'PERMISSION_DENIED',
88
+ STORAGE_FULL: 'STORAGE_FULL',
89
+
90
+ // Network errors
91
+ NETWORK_ERROR: 'NETWORK_ERROR',
92
+ TIMEOUT_ERROR: 'TIMEOUT_ERROR',
93
+ API_ERROR: 'API_ERROR',
94
+
95
+ // Media errors
96
+ MEDIA_PICKER_ERROR: 'MEDIA_PICKER_ERROR',
97
+ MEDIA_SAVE_ERROR: 'MEDIA_SAVE_ERROR',
98
+ IMAGE_LOAD_ERROR: 'IMAGE_LOAD_ERROR',
99
+
100
+ // Storage errors
101
+ CACHE_ERROR: 'CACHE_ERROR',
102
+ STORAGE_ERROR: 'STORAGE_ERROR',
103
+
104
+ // Validation errors
105
+ INVALID_INPUT: 'INVALID_INPUT',
106
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
107
+
108
+ // Theme errors
109
+ THEME_LOAD_ERROR: 'THEME_LOAD_ERROR',
110
+ THEME_SAVE_ERROR: 'THEME_SAVE_ERROR',
111
+
112
+ // Generic errors
113
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR',
114
+ INITIALIZATION_ERROR: 'INITIALIZATION_ERROR',
115
+ } as const;
116
+
117
+ export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];