@umituz/react-native-design-system 2.6.107 → 2.6.111

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 (35) hide show
  1. package/package.json +2 -2
  2. package/src/exception/domain/entities/ExceptionEntity.ts +115 -0
  3. package/src/exception/domain/repositories/IExceptionRepository.ts +37 -0
  4. package/src/exception/index.ts +65 -0
  5. package/src/exception/infrastructure/services/ExceptionHandler.ts +93 -0
  6. package/src/exception/infrastructure/services/ExceptionLogger.ts +142 -0
  7. package/src/exception/infrastructure/services/ExceptionReporter.ts +134 -0
  8. package/src/exception/infrastructure/services/ExceptionService.ts +154 -0
  9. package/src/exception/infrastructure/storage/ExceptionStore.ts +44 -0
  10. package/src/exception/presentation/components/ErrorBoundary.tsx +129 -0
  11. package/src/exception/presentation/components/ExceptionEmptyState.tsx +123 -0
  12. package/src/exception/presentation/components/ExceptionErrorState.tsx +118 -0
  13. package/src/exports/exception.ts +7 -0
  14. package/src/exports/infinite-scroll.ts +7 -0
  15. package/src/exports/uuid.ts +7 -0
  16. package/src/index.ts +15 -0
  17. package/src/infinite-scroll/domain/interfaces/infinite-scroll-list-props.ts +67 -0
  18. package/src/infinite-scroll/domain/types/infinite-scroll-config.ts +108 -0
  19. package/src/infinite-scroll/domain/types/infinite-scroll-return.ts +40 -0
  20. package/src/infinite-scroll/domain/types/infinite-scroll-state.ts +58 -0
  21. package/src/infinite-scroll/domain/utils/pagination-utils.ts +63 -0
  22. package/src/infinite-scroll/domain/utils/type-guards.ts +53 -0
  23. package/src/infinite-scroll/index.ts +62 -0
  24. package/src/infinite-scroll/presentation/components/empty.tsx +44 -0
  25. package/src/infinite-scroll/presentation/components/error.tsx +66 -0
  26. package/src/infinite-scroll/presentation/components/infinite-scroll-list.tsx +120 -0
  27. package/src/infinite-scroll/presentation/components/loading-more.tsx +38 -0
  28. package/src/infinite-scroll/presentation/components/loading.tsx +40 -0
  29. package/src/infinite-scroll/presentation/components/types.ts +124 -0
  30. package/src/infinite-scroll/presentation/hooks/pagination.helper.ts +83 -0
  31. package/src/infinite-scroll/presentation/hooks/useInfiniteScroll.ts +327 -0
  32. package/src/uuid/index.ts +15 -0
  33. package/src/uuid/infrastructure/utils/UUIDUtils.ts +75 -0
  34. package/src/uuid/package-lock.json +14255 -0
  35. package/src/uuid/types/UUID.ts +36 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Component Types
3
+ *
4
+ * Shared types for presentation components
5
+ */
6
+
7
+ import type { TextStyle, ViewStyle, AccessibilityProps } from "react-native";
8
+
9
+ export interface EmptyProps extends AccessibilityProps {
10
+ /**
11
+ * Optional text to display
12
+ * Default: "No items found"
13
+ */
14
+ text?: string;
15
+
16
+ /**
17
+ * Optional accessibility label
18
+ * Default: "Empty list"
19
+ */
20
+ accessibilityLabel?: string;
21
+
22
+ /**
23
+ * Optional custom style for container
24
+ */
25
+ containerStyle?: ViewStyle;
26
+
27
+ /**
28
+ * Optional custom style for text
29
+ */
30
+ textStyle?: TextStyle;
31
+ }
32
+
33
+ export interface ErrorProps extends AccessibilityProps {
34
+ /**
35
+ * Error message to display
36
+ */
37
+ error: string;
38
+
39
+ /**
40
+ * Retry callback
41
+ */
42
+ onRetry: () => void;
43
+
44
+ /**
45
+ * Optional text for retry button
46
+ * Default: "Tap to retry"
47
+ */
48
+ retryText?: string;
49
+
50
+ /**
51
+ * Optional accessibility label for error
52
+ * Default: "Error: {error}"
53
+ */
54
+ accessibilityLabel?: string;
55
+
56
+ /**
57
+ * Optional accessibility hint for retry button
58
+ * Default: "Double tap to retry"
59
+ */
60
+ retryAccessibilityHint?: string;
61
+
62
+ /**
63
+ * Optional custom style for container
64
+ */
65
+ containerStyle?: ViewStyle;
66
+
67
+ /**
68
+ * Optional custom style for error text
69
+ */
70
+ errorTextStyle?: TextStyle;
71
+
72
+ /**
73
+ * Optional custom style for retry text
74
+ */
75
+ retryTextStyle?: TextStyle;
76
+ }
77
+
78
+ export interface LoadingProps extends AccessibilityProps {
79
+ /**
80
+ * Optional custom style for container
81
+ */
82
+ containerStyle?: ViewStyle;
83
+
84
+ /**
85
+ * Optional size for indicator
86
+ * Default: "large"
87
+ */
88
+ size?: "small" | "large" | number;
89
+
90
+ /**
91
+ * Optional color for indicator
92
+ */
93
+ color?: string;
94
+
95
+ /**
96
+ * Optional accessibility label
97
+ * Default: "Loading"
98
+ */
99
+ accessibilityLabel?: string;
100
+ }
101
+
102
+ export interface LoadingMoreProps extends AccessibilityProps {
103
+ /**
104
+ * Optional custom style for container
105
+ */
106
+ containerStyle?: ViewStyle;
107
+
108
+ /**
109
+ * Optional size for indicator
110
+ * Default: "small"
111
+ */
112
+ size?: "small" | "large" | number;
113
+
114
+ /**
115
+ * Optional color for indicator
116
+ */
117
+ color?: string;
118
+
119
+ /**
120
+ * Optional accessibility label
121
+ * Default: "Loading more"
122
+ */
123
+ accessibilityLabel?: string;
124
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Pagination Helper
3
+ * SOLID: Single Responsibility - Handle pagination operations
4
+ */
5
+
6
+ import type { InfiniteScrollConfig, PaginatedResult } from "../../domain/types/infinite-scroll-config";
7
+ import type { InfiniteScrollState } from "../../domain/types/infinite-scroll-state";
8
+
9
+ export function isCursorMode<T>(
10
+ config: InfiniteScrollConfig<T>,
11
+ ): config is Extract<InfiniteScrollConfig<T>, { paginationMode: "cursor" }> {
12
+ return "paginationMode" in config && config.paginationMode === "cursor";
13
+ }
14
+
15
+ export async function loadData<T>(
16
+ config: InfiniteScrollConfig<T>,
17
+ pageOrCursor: number | string | undefined,
18
+ pageSize: number,
19
+ totalItems?: number,
20
+ ): Promise<InfiniteScrollState<T>> {
21
+ if (isCursorMode(config)) {
22
+ const result = await config.fetchCursor(pageOrCursor as string | undefined, pageSize);
23
+ return {
24
+ items: result.items,
25
+ pages: [result.items],
26
+ currentPage: 0,
27
+ cursor: result.nextCursor,
28
+ hasMore: result.hasMore,
29
+ isLoading: false,
30
+ isLoadingMore: false,
31
+ isRefreshing: false,
32
+ error: null,
33
+ totalItems,
34
+ };
35
+ } else {
36
+ const data = await config.fetchData(pageOrCursor as number, pageSize);
37
+ const hasMore = data.length >= pageSize;
38
+ return {
39
+ items: data,
40
+ pages: [data],
41
+ currentPage: pageOrCursor as number,
42
+ cursor: null,
43
+ hasMore,
44
+ isLoading: false,
45
+ isLoadingMore: false,
46
+ isRefreshing: false,
47
+ error: null,
48
+ totalItems,
49
+ };
50
+ }
51
+ }
52
+
53
+ export async function loadMoreData<T>(
54
+ config: InfiniteScrollConfig<T>,
55
+ state: InfiniteScrollState<T>,
56
+ pageSize: number,
57
+ ): Promise<Partial<InfiniteScrollState<T>>> {
58
+ if (isCursorMode(config)) {
59
+ if (!state.cursor) throw new Error("No cursor available");
60
+ const result = await config.fetchCursor(state.cursor, pageSize);
61
+ return {
62
+ items: [...state.items, ...result.items],
63
+ pages: [...state.pages, result.items],
64
+ cursor: result.nextCursor,
65
+ hasMore: result.hasMore,
66
+ isLoadingMore: false,
67
+ error: null,
68
+ };
69
+ } else {
70
+ const nextPage = state.currentPage + 1;
71
+ const data = await config.fetchData(nextPage, pageSize);
72
+ const newPages = [...state.pages, data];
73
+ const hasMore = data.length >= pageSize;
74
+ return {
75
+ items: newPages.flat(),
76
+ pages: newPages,
77
+ currentPage: nextPage,
78
+ hasMore,
79
+ isLoadingMore: false,
80
+ error: null,
81
+ };
82
+ }
83
+ }
@@ -0,0 +1,327 @@
1
+ /**
2
+ * useInfiniteScroll Hook
3
+ *
4
+ * Supports page-based and cursor-based pagination
5
+ * Features:
6
+ * - AbortController for request cancellation
7
+ * - Automatic retry with exponential backoff
8
+ * - Performance monitoring in __DEV__ mode
9
+ * - Memory leak prevention
10
+ */
11
+
12
+ import { useState, useCallback, useEffect, useRef } from "react";
13
+ import type { InfiniteScrollConfig } from "../../domain/types/infinite-scroll-config";
14
+ import type { InfiniteScrollState } from "../../domain/types/infinite-scroll-state";
15
+ import type { UseInfiniteScrollReturn } from "../../domain/types/infinite-scroll-return";
16
+ import { loadData, loadMoreData, isCursorMode } from "./pagination.helper";
17
+
18
+ const DEFAULT_CONFIG = {
19
+ pageSize: 20,
20
+ threshold: 5,
21
+ autoLoad: true,
22
+ initialPage: 0,
23
+ maxRetries: 3,
24
+ retryDelay: 1000,
25
+ };
26
+
27
+ function createInitialState<T>(
28
+ initialPage: number,
29
+ totalItems?: number,
30
+ ): InfiniteScrollState<T> {
31
+ return {
32
+ items: [],
33
+ pages: [],
34
+ currentPage: initialPage,
35
+ cursor: null,
36
+ hasMore: true,
37
+ isLoading: true,
38
+ isLoadingMore: false,
39
+ isRefreshing: false,
40
+ error: null,
41
+ totalItems,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Sleep utility for retry delay
47
+ */
48
+ function sleep(ms: number): Promise<void> {
49
+ return new Promise((resolve) => setTimeout(resolve, ms));
50
+ }
51
+
52
+ /**
53
+ * Retry logic with exponential backoff
54
+ */
55
+ async function retryWithBackoff<T>(
56
+ fn: () => Promise<T>,
57
+ maxRetries: number,
58
+ baseDelay: number,
59
+ ): Promise<T> {
60
+ let lastError: Error | undefined;
61
+
62
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
63
+ try {
64
+ return await fn();
65
+ } catch (error) {
66
+ lastError = error instanceof Error ? error : new Error(String(error));
67
+
68
+ if (attempt < maxRetries) {
69
+ const delay = baseDelay * Math.pow(2, attempt);
70
+ if (__DEV__) {
71
+ console.log(
72
+ `[useInfiniteScroll] Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`,
73
+ );
74
+ }
75
+ await sleep(delay);
76
+ }
77
+ }
78
+ }
79
+
80
+ throw lastError;
81
+ }
82
+
83
+ export function useInfiniteScroll<T>(
84
+ config: InfiniteScrollConfig<T>,
85
+ ): UseInfiniteScrollReturn<T> {
86
+ const {
87
+ pageSize = DEFAULT_CONFIG.pageSize,
88
+ autoLoad = DEFAULT_CONFIG.autoLoad,
89
+ totalItems,
90
+ } = config;
91
+
92
+ const initialPage =
93
+ "initialPage" in config ? config.initialPage || 0 : DEFAULT_CONFIG.initialPage;
94
+
95
+ const maxRetries = DEFAULT_CONFIG.maxRetries;
96
+ const retryDelay = DEFAULT_CONFIG.retryDelay;
97
+
98
+ const [state, setState] = useState<InfiniteScrollState<T>>(() =>
99
+ createInitialState<T>(initialPage, totalItems),
100
+ );
101
+
102
+ const isLoadingRef = useRef(false);
103
+ const isMountedRef = useRef(true);
104
+ const abortControllerRef = useRef<AbortController | null>(null);
105
+
106
+ // Cleanup on unmount
107
+ useEffect(() => {
108
+ isMountedRef.current = true;
109
+
110
+ return () => {
111
+ isMountedRef.current = false;
112
+ abortControllerRef.current?.abort();
113
+ if (__DEV__) {
114
+ console.log("[useInfiniteScroll] Cleanup: component unmounted");
115
+ }
116
+ };
117
+ }, []);
118
+
119
+ // Cancel pending requests
120
+ const cancelPendingRequests = useCallback(() => {
121
+ abortControllerRef.current?.abort();
122
+ abortControllerRef.current = new AbortController();
123
+ }, []);
124
+
125
+ const loadInitial = useCallback(async () => {
126
+ if (isLoadingRef.current) return;
127
+ isLoadingRef.current = true;
128
+
129
+ cancelPendingRequests();
130
+
131
+ if (isMountedRef.current) {
132
+ setState((prev) => ({ ...prev, isLoading: true, error: null }));
133
+ }
134
+
135
+ const startTime = __DEV__ ? performance.now() : 0;
136
+
137
+ try {
138
+ const newState = await retryWithBackoff(
139
+ async () => {
140
+ const result = await loadData(config, initialPage, pageSize, totalItems);
141
+ return result;
142
+ },
143
+ maxRetries,
144
+ retryDelay,
145
+ );
146
+
147
+ if (isMountedRef.current) {
148
+ setState(newState);
149
+
150
+ if (__DEV__) {
151
+ const duration = performance.now() - startTime;
152
+ console.log(
153
+ `[useInfiniteScroll] Initial load completed in ${duration.toFixed(2)}ms`,
154
+ `Loaded ${newState.items.length} items`,
155
+ );
156
+ }
157
+ }
158
+ } catch (error) {
159
+ if (isMountedRef.current) {
160
+ const errorMessage =
161
+ error instanceof Error ? error.message : "Failed to load data";
162
+
163
+ setState((prev) => ({
164
+ ...prev,
165
+ isLoading: false,
166
+ error: errorMessage,
167
+ }));
168
+
169
+ if (__DEV__) {
170
+ console.error("[useInfiniteScroll] Load initial failed:", errorMessage);
171
+ }
172
+ }
173
+ } finally {
174
+ isLoadingRef.current = false;
175
+ }
176
+ }, [config, initialPage, pageSize, totalItems, maxRetries, retryDelay, cancelPendingRequests]);
177
+
178
+ const loadMore = useCallback(async () => {
179
+ if (
180
+ isLoadingRef.current ||
181
+ !state.hasMore ||
182
+ state.isLoadingMore ||
183
+ state.isLoading
184
+ ) {
185
+ return;
186
+ }
187
+
188
+ if (isCursorMode(config) && !state.cursor) return;
189
+
190
+ isLoadingRef.current = true;
191
+
192
+ if (isMountedRef.current) {
193
+ setState((prev) => ({ ...prev, isLoadingMore: true, error: null }));
194
+ }
195
+
196
+ const startTime = __DEV__ ? performance.now() : 0;
197
+
198
+ try {
199
+ const updates = await retryWithBackoff(
200
+ async () => {
201
+ const result = await loadMoreData(config, state, pageSize);
202
+ return result;
203
+ },
204
+ maxRetries,
205
+ retryDelay,
206
+ );
207
+
208
+ if (isMountedRef.current) {
209
+ setState((prev) => ({ ...prev, ...updates }));
210
+
211
+ if (__DEV__) {
212
+ const duration = performance.now() - startTime;
213
+ const newItemsCount = updates.items?.length || 0;
214
+ console.log(
215
+ `[useInfiniteScroll] Load more completed in ${duration.toFixed(2)}ms`,
216
+ `Loaded ${newItemsCount} items, total: ${updates.items?.length || 0}`,
217
+ );
218
+ }
219
+ }
220
+ } catch (error) {
221
+ if (isMountedRef.current) {
222
+ const errorMessage =
223
+ error instanceof Error ? error.message : "Failed to load more items";
224
+
225
+ setState((prev) => ({
226
+ ...prev,
227
+ isLoadingMore: false,
228
+ error: errorMessage,
229
+ }));
230
+
231
+ if (__DEV__) {
232
+ console.error("[useInfiniteScroll] Load more failed:", errorMessage);
233
+ }
234
+ }
235
+ } finally {
236
+ isLoadingRef.current = false;
237
+ }
238
+ }, [config, state, pageSize, maxRetries, retryDelay]);
239
+
240
+ const refresh = useCallback(async () => {
241
+ if (isLoadingRef.current) return;
242
+ isLoadingRef.current = true;
243
+
244
+ cancelPendingRequests();
245
+
246
+ if (isMountedRef.current) {
247
+ setState((prev) => ({ ...prev, isRefreshing: true, error: null }));
248
+ }
249
+
250
+ const startTime = __DEV__ ? performance.now() : 0;
251
+
252
+ try {
253
+ const newState = await retryWithBackoff(
254
+ async () => {
255
+ const result = await loadData(config, initialPage, pageSize, totalItems);
256
+ return result;
257
+ },
258
+ maxRetries,
259
+ retryDelay,
260
+ );
261
+
262
+ if (isMountedRef.current) {
263
+ setState(newState);
264
+
265
+ if (__DEV__) {
266
+ const duration = performance.now() - startTime;
267
+ console.log(
268
+ `[useInfiniteScroll] Refresh completed in ${duration.toFixed(2)}ms`,
269
+ `Loaded ${newState.items.length} items`,
270
+ );
271
+ }
272
+ }
273
+ } catch (error) {
274
+ if (isMountedRef.current) {
275
+ const errorMessage =
276
+ error instanceof Error ? error.message : "Failed to refresh data";
277
+
278
+ setState((prev) => ({
279
+ ...prev,
280
+ isRefreshing: false,
281
+ error: errorMessage,
282
+ }));
283
+
284
+ if (__DEV__) {
285
+ console.error("[useInfiniteScroll] Refresh failed:", errorMessage);
286
+ }
287
+ }
288
+ } finally {
289
+ isLoadingRef.current = false;
290
+ }
291
+ }, [config, initialPage, pageSize, totalItems, maxRetries, retryDelay, cancelPendingRequests]);
292
+
293
+ const reset = useCallback(() => {
294
+ isLoadingRef.current = false;
295
+ cancelPendingRequests();
296
+ setState(createInitialState<T>(initialPage, totalItems));
297
+
298
+ if (__DEV__) {
299
+ console.log("[useInfiniteScroll] State reset");
300
+ }
301
+ }, [initialPage, totalItems, cancelPendingRequests]);
302
+
303
+ useEffect(() => {
304
+ if (autoLoad) {
305
+ loadInitial();
306
+ }
307
+
308
+ return () => {
309
+ // Cleanup on config change
310
+ if (__DEV__) {
311
+ console.log("[useInfiniteScroll] Config changed, cleaning up");
312
+ }
313
+ };
314
+ }, [autoLoad, loadInitial]);
315
+
316
+ const canLoadMore =
317
+ state.hasMore && !state.isLoadingMore && !state.isLoading;
318
+
319
+ return {
320
+ items: state.items,
321
+ state,
322
+ loadMore,
323
+ refresh,
324
+ reset,
325
+ canLoadMore,
326
+ };
327
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * React Native UUID
3
+ *
4
+ * Cross-platform UUID generation for React Native apps
5
+ */
6
+
7
+ export {
8
+ generateUUID,
9
+ generateUUID as uuidv4,
10
+ generateUUID as generateCreationId,
11
+ isValidUUID,
12
+ getUUIDVersion,
13
+ } from "./infrastructure/utils/UUIDUtils";
14
+ export type { UUID } from "./types/UUID";
15
+ export { UUIDVersion, UUID_CONSTANTS } from "./types/UUID";
@@ -0,0 +1,75 @@
1
+ /**
2
+ * UUID Generation Utility
3
+ *
4
+ * Provides cross-platform UUID generation using expo-crypto.
5
+ * Compatible with React Native (iOS, Android) and Web.
6
+ */
7
+
8
+ import * as Crypto from 'expo-crypto';
9
+ import type { UUID } from '../../types/UUID';
10
+ import { UUID_CONSTANTS } from '../../types/UUID';
11
+
12
+ /**
13
+ * Generate a v4 UUID
14
+ * Uses expo-crypto's randomUUID() for secure UUID generation
15
+ *
16
+ * @returns A v4 UUID string
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * import { generateUUID } from '@umituz/react-native-uuid';
21
+ *
22
+ * const id = generateUUID();
23
+ * // Returns: "550e8400-e29b-41d4-a716-446655440000"
24
+ * ```
25
+ */
26
+ export const generateUUID = (): UUID => {
27
+ return Crypto.randomUUID() as UUID;
28
+ };
29
+
30
+ /**
31
+ * Validate UUID format
32
+ * Checks if a string is a valid v4 UUID
33
+ *
34
+ * @param value - The value to validate
35
+ * @returns True if the value is a valid v4 UUID
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * import { isValidUUID } from '@umituz/react-native-uuid';
40
+ *
41
+ * isValidUUID('550e8400-e29b-41d4-a716-446655440000'); // true
42
+ * isValidUUID('invalid-uuid'); // false
43
+ * ```
44
+ */
45
+ export const isValidUUID = (value: string): value is UUID => {
46
+ return UUID_CONSTANTS.PATTERN.test(value);
47
+ };
48
+
49
+ /**
50
+ * Get version from UUID string
51
+ * Returns the UUID version number (1-5) or null for NIL/invalid
52
+ *
53
+ * @param value - The UUID string
54
+ * @returns UUID version number or null
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * import { getUUIDVersion } from '@umituz/react-native-uuid';
59
+ *
60
+ * getUUIDVersion('550e8400-e29b-41d4-a716-446655440000'); // 4
61
+ * getUUIDVersion('00000000-0000-0000-0000-000000000000'); // 0 (NIL)
62
+ * getUUIDVersion('invalid'); // null
63
+ * ```
64
+ */
65
+ export const getUUIDVersion = (value: string): number | null => {
66
+ if (value === UUID_CONSTANTS.NIL) {
67
+ return 0;
68
+ }
69
+
70
+ const versionChar = value.charAt(14);
71
+ const version = parseInt(versionChar, 10);
72
+
73
+ return (version >= 1 && version <= 5) ? version : null;
74
+ };
75
+