@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "4.23.113",
3
+ "version": "4.23.115",
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": "./src/index.ts",
@@ -16,6 +16,18 @@ export interface AtomicTouchableProps {
16
16
  activeOpacity?: number;
17
17
  style?: StyleProp<ViewStyle>;
18
18
  testID?: string;
19
+ // Accessibility props
20
+ accessibilityLabel?: string;
21
+ accessibilityHint?: string;
22
+ accessibilityRole?: 'button' | 'link' | 'none' | 'text' | 'search' | 'image' | 'adjustable' | 'imagebutton' | 'header' | 'summary' | 'alert' | 'checkbox' | 'combobox' | 'menu' | 'menubar' | 'menuitem' | 'progressbar' | 'radio' | 'radiogroup' | 'scrollbar' | 'spinbutton' | 'switch' | 'tab' | 'tablist' | 'timer' | 'toolbar';
23
+ accessibilityState?: {
24
+ disabled?: boolean;
25
+ selected?: boolean;
26
+ checked?: boolean | 'mixed';
27
+ busy?: boolean;
28
+ expanded?: boolean;
29
+ };
30
+ accessible?: boolean;
19
31
  }
20
32
 
21
33
  export const AtomicTouchable: React.FC<AtomicTouchableProps> = ({
@@ -26,6 +38,11 @@ export const AtomicTouchable: React.FC<AtomicTouchableProps> = ({
26
38
  activeOpacity = 0.7,
27
39
  style,
28
40
  testID,
41
+ accessibilityLabel,
42
+ accessibilityHint,
43
+ accessibilityRole = 'button',
44
+ accessibilityState,
45
+ accessible = true,
29
46
  }) => {
30
47
  return (
31
48
  <TouchableOpacity
@@ -35,6 +52,11 @@ export const AtomicTouchable: React.FC<AtomicTouchableProps> = ({
35
52
  activeOpacity={activeOpacity}
36
53
  style={style}
37
54
  testID={testID}
55
+ accessibilityLabel={accessibilityLabel}
56
+ accessibilityHint={accessibilityHint}
57
+ accessibilityRole={accessibilityRole}
58
+ accessibilityState={{ ...accessibilityState, disabled }}
59
+ accessible={accessible}
38
60
  >
39
61
  {children}
40
62
  </TouchableOpacity>
@@ -3,7 +3,7 @@
3
3
  * Reusable badge for labels, status indicators, and tags
4
4
  */
5
5
 
6
- import React from "react";
6
+ import React, { useMemo } from "react";
7
7
  import { View, StyleSheet, type StyleProp, type ViewStyle, type TextStyle } from "react-native";
8
8
  import { AtomicText } from "../AtomicText";
9
9
  import { AtomicIcon, type IconName } from "../icon";
@@ -43,34 +43,34 @@ export const AtomicBadge: React.FC<AtomicBadgeProps> = React.memo(({
43
43
  }) => {
44
44
  const tokens = useAppDesignTokens();
45
45
 
46
- const sizeConfig = {
47
- sm: {
48
- paddingH: 6 * tokens.spacingMultiplier,
49
- paddingV: 2 * tokens.spacingMultiplier,
50
- fontSize: 10 * tokens.spacingMultiplier,
51
- iconSize: 10 * tokens.spacingMultiplier,
52
- gap: 3 * tokens.spacingMultiplier,
53
- radius: 4 * tokens.spacingMultiplier
46
+ const sizeConfig = useMemo(() => ({
47
+ sm: {
48
+ paddingH: 6 * tokens.spacingMultiplier,
49
+ paddingV: 2 * tokens.spacingMultiplier,
50
+ fontSize: 10 * tokens.spacingMultiplier,
51
+ iconSize: 10 * tokens.spacingMultiplier,
52
+ gap: 3 * tokens.spacingMultiplier,
53
+ radius: 4 * tokens.spacingMultiplier
54
54
  },
55
- md: {
56
- paddingH: 8 * tokens.spacingMultiplier,
57
- paddingV: 4 * tokens.spacingMultiplier,
58
- fontSize: 11 * tokens.spacingMultiplier,
59
- iconSize: 12 * tokens.spacingMultiplier,
60
- gap: 4 * tokens.spacingMultiplier,
61
- radius: 6 * tokens.spacingMultiplier
55
+ md: {
56
+ paddingH: 8 * tokens.spacingMultiplier,
57
+ paddingV: 4 * tokens.spacingMultiplier,
58
+ fontSize: 11 * tokens.spacingMultiplier,
59
+ iconSize: 12 * tokens.spacingMultiplier,
60
+ gap: 4 * tokens.spacingMultiplier,
61
+ radius: 6 * tokens.spacingMultiplier
62
62
  },
63
- lg: {
64
- paddingH: 12 * tokens.spacingMultiplier,
65
- paddingV: 6 * tokens.spacingMultiplier,
66
- fontSize: 13 * tokens.spacingMultiplier,
67
- iconSize: 14 * tokens.spacingMultiplier,
68
- gap: 5 * tokens.spacingMultiplier,
69
- radius: 8 * tokens.spacingMultiplier
63
+ lg: {
64
+ paddingH: 12 * tokens.spacingMultiplier,
65
+ paddingV: 6 * tokens.spacingMultiplier,
66
+ fontSize: 13 * tokens.spacingMultiplier,
67
+ iconSize: 14 * tokens.spacingMultiplier,
68
+ gap: 5 * tokens.spacingMultiplier,
69
+ radius: 8 * tokens.spacingMultiplier
70
70
  },
71
- }[size];
71
+ }[size]), [size, tokens.spacingMultiplier]);
72
72
 
73
- const getVariantColors = () => {
73
+ const colors = useMemo(() => {
74
74
  switch (variant) {
75
75
  case "primary":
76
76
  return { bg: tokens.colors.primaryLight, text: tokens.colors.primary };
@@ -87,9 +87,7 @@ export const AtomicBadge: React.FC<AtomicBadgeProps> = React.memo(({
87
87
  default:
88
88
  return { bg: tokens.colors.primaryLight, text: tokens.colors.primary };
89
89
  }
90
- };
91
-
92
- const colors = getVariantColors();
90
+ }, [variant, tokens.colors]);
93
91
 
94
92
  const containerStyle: StyleProp<ViewStyle> = [
95
93
  styles.container,
@@ -3,7 +3,7 @@
3
3
  * Refactored: Extracted configs, styles, and types
4
4
  */
5
5
 
6
- import React from 'react';
6
+ import React, { useMemo } from 'react';
7
7
  import { View, ViewStyle, TouchableOpacity } from 'react-native';
8
8
  import { AtomicText } from '../AtomicText';
9
9
  import { AtomicIcon } from '../icon';
@@ -33,10 +33,10 @@ export const AtomicChip: React.FC<AtomicChipProps> = React.memo(({
33
33
  }) => {
34
34
  const tokens = useAppDesignTokens();
35
35
 
36
- const sizeConfig = getChipSizeConfig(size, tokens);
37
- const colorConfig = getChipColorConfig(color, variant, tokens);
38
- const borderStyle = getChipBorderStyle(variant, tokens);
39
- const selectedStyle = getChipSelectedStyle(selected, tokens);
36
+ const sizeConfig = useMemo(() => getChipSizeConfig(size, tokens), [size, tokens]);
37
+ const colorConfig = useMemo(() => getChipColorConfig(color, variant, tokens), [color, variant, tokens]);
38
+ const borderStyle = useMemo(() => getChipBorderStyle(variant, tokens), [variant, tokens]);
39
+ const selectedStyle = useMemo(() => getChipSelectedStyle(selected, tokens), [selected, tokens]);
40
40
 
41
41
  // Apply custom colors if provided
42
42
  const finalBackgroundColor = backgroundColor || colorConfig.bg;
@@ -5,7 +5,7 @@
5
5
  * Extracted from AtomicDatePicker for better separation of concerns.
6
6
  */
7
7
 
8
- import React from 'react';
8
+ import React, { useMemo } from 'react';
9
9
  import {
10
10
  View,
11
11
  Modal,
@@ -52,7 +52,7 @@ export const DatePickerModal: React.FC<DatePickerModalProps> = ({
52
52
  const tokens = useAppDesignTokens();
53
53
  const insets = useSafeAreaInsets();
54
54
 
55
- const modalStyles = StyleSheet.create({
55
+ const modalStyles = useMemo(() => StyleSheet.create({
56
56
  overlay: {
57
57
  flex: 1,
58
58
  backgroundColor: `rgba(0, 0, 0, ${overlayOpacity})`,
@@ -89,7 +89,7 @@ export const DatePickerModal: React.FC<DatePickerModalProps> = ({
89
89
  fontWeight: '500',
90
90
  color: tokens.colors.onPrimary,
91
91
  },
92
- });
92
+ }), [overlayOpacity, tokens, insets.bottom]);
93
93
 
94
94
  if (Platform.OS !== 'ios') {
95
95
  return null;
@@ -102,6 +102,7 @@ export const DatePickerModal: React.FC<DatePickerModalProps> = ({
102
102
  animationType="none"
103
103
  onRequestClose={onClose}
104
104
  testID={`${testID}-modal`}
105
+ accessibilityViewIsModal={true}
105
106
  >
106
107
  <View style={modalStyles.overlay}>
107
108
  <View style={modalStyles.container}>
@@ -54,5 +54,5 @@ export const useInputState = ({
54
54
  setIsFocused,
55
55
  handleTextChange,
56
56
  togglePasswordVisibility,
57
- }), [localValue, isFocused, isPasswordVisible, characterCount, isAtMaxLength, handleTextChange, togglePasswordVisibility]);
57
+ }), [localValue, isFocused, isPasswordVisible, characterCount, isAtMaxLength, setIsFocused, handleTextChange, togglePasswordVisibility]);
58
58
  };
@@ -95,7 +95,7 @@ export const PickerModal: React.FC<PickerModalProps> = React.memo(({
95
95
  };
96
96
 
97
97
  return (
98
- <Modal visible={visible} animationType="none" transparent onRequestClose={onClose} testID={`${testID}-modal`}>
98
+ <Modal visible={visible} animationType="none" transparent onRequestClose={onClose} testID={`${testID}-modal`} accessibilityViewIsModal={true}>
99
99
  <View style={styles.overlay}>
100
100
  <View style={[styles.container, { paddingBottom: insets.bottom + tokens.spacing.md }]}>
101
101
  <View style={styles.header}>
@@ -1,4 +1,4 @@
1
- import { useState, useMemo } from 'react';
1
+ import { useState, useMemo, useCallback } from 'react';
2
2
  import type { PickerOption } from '../../picker/types';
3
3
 
4
4
  interface UsePickerStateProps {
@@ -71,23 +71,23 @@ export const usePickerState = ({
71
71
  /**
72
72
  * Handle modal open
73
73
  */
74
- const openModal = () => {
74
+ const openModal = useCallback(() => {
75
75
  setModalVisible(true);
76
76
  setSearchQuery('');
77
- };
77
+ }, []);
78
78
 
79
79
  /**
80
80
  * Handle modal close
81
81
  */
82
- const closeModal = () => {
82
+ const closeModal = useCallback(() => {
83
83
  setModalVisible(false);
84
84
  setSearchQuery('');
85
- };
85
+ }, []);
86
86
 
87
87
  /**
88
88
  * Handle option selection
89
89
  */
90
- const handleSelect = (optionValue: string) => {
90
+ const handleSelect = useCallback((optionValue: string) => {
91
91
  if (multiple) {
92
92
  const newValues = selectedValues.includes(optionValue)
93
93
  ? selectedValues.filter((v) => v !== optionValue)
@@ -99,30 +99,30 @@ export const usePickerState = ({
99
99
  closeModal();
100
100
  }
101
101
  }
102
- };
102
+ }, [multiple, selectedValues, onChange, autoClose, closeModal]);
103
103
 
104
104
  /**
105
105
  * Handle clear selection
106
106
  */
107
- const handleClear = () => {
107
+ const handleClear = useCallback(() => {
108
108
  onChange(multiple ? [] : '');
109
- };
109
+ }, [onChange, multiple]);
110
110
 
111
111
  /**
112
112
  * Handle search query change
113
113
  */
114
- const handleSearch = (query: string) => {
114
+ const handleSearch = useCallback((query: string) => {
115
115
  setSearchQuery(query);
116
- };
116
+ }, []);
117
117
 
118
118
  /**
119
119
  * Handle chip removal
120
120
  */
121
- const handleChipRemove = (value: string) => {
121
+ const handleChipRemove = useCallback((value: string) => {
122
122
  handleSelect(value);
123
- };
123
+ }, [handleSelect]);
124
124
 
125
- return {
125
+ return useMemo(() => ({
126
126
  modalVisible,
127
127
  searchQuery,
128
128
  selectedValues,
@@ -135,5 +135,18 @@ export const usePickerState = ({
135
135
  handleClear,
136
136
  handleSearch,
137
137
  handleChipRemove,
138
- };
138
+ }), [
139
+ modalVisible,
140
+ searchQuery,
141
+ selectedValues,
142
+ selectedOptions,
143
+ filteredOptions,
144
+ displayText,
145
+ openModal,
146
+ closeModal,
147
+ handleSelect,
148
+ handleClear,
149
+ handleSearch,
150
+ handleChipRemove,
151
+ ]);
139
152
  };
@@ -22,7 +22,7 @@
22
22
  * ```
23
23
  */
24
24
 
25
- import React from 'react';
25
+ import React, { useMemo } from 'react';
26
26
  import { View, StyleSheet, type StyleProp, type ViewStyle, type DimensionValue } from 'react-native';
27
27
  import { useAppDesignTokens } from '../../theme';
28
28
  import type { SkeletonPattern, SkeletonConfig } from './AtomicSkeleton.types';
@@ -50,8 +50,8 @@ const SkeletonItem: React.FC<{
50
50
  config: SkeletonConfig;
51
51
  baseColor: string;
52
52
  multiplier: number;
53
- }> = ({ config, baseColor, multiplier }) => {
54
- const itemStyles = StyleSheet.create({
53
+ }> = React.memo(({ config, baseColor, multiplier }) => {
54
+ const itemStyles = useMemo(() => StyleSheet.create({
55
55
  item: {
56
56
  ...styles.skeleton,
57
57
  width: (typeof config.width === 'number' ? config.width * multiplier : config.width) as DimensionValue,
@@ -60,10 +60,10 @@ const SkeletonItem: React.FC<{
60
60
  marginBottom: config.marginBottom ? config.marginBottom * multiplier : undefined,
61
61
  backgroundColor: baseColor,
62
62
  },
63
- });
63
+ }), [config, baseColor, multiplier]);
64
64
 
65
65
  return <View style={itemStyles.item} />;
66
- };
66
+ });
67
67
 
68
68
  export const AtomicSkeleton: React.FC<AtomicSkeletonProps> = ({
69
69
  pattern = 'list',
@@ -54,19 +54,8 @@ export class DeviceCapabilityService {
54
54
  */
55
55
  static async hasNotch(): Promise<boolean> {
56
56
  try {
57
- if (Platform.OS !== 'ios') {
58
- return false;
59
- }
60
-
61
57
  const info = await DeviceInfoService.getDeviceInfo();
62
- const modelName = info.modelName?.toLowerCase() ?? '';
63
-
64
- // iPhone X and newer (with notch or dynamic island)
65
- return (
66
- modelName.includes('iphone x') ||
67
- modelName.includes('iphone 1') || // 11, 12, 13, 14, 15
68
- modelName.includes('pro')
69
- );
58
+ return this.hasNotchFromInfo(info);
70
59
  } catch {
71
60
  return false;
72
61
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { Directory, Paths } from "expo-file-system";
7
7
  import type { DirectoryType } from "../../domain/entities/File";
8
+ import { ErrorHandler, ErrorCodes } from "../../../utils/errors";
8
9
 
9
10
  /**
10
11
  * Create directory
@@ -12,10 +13,19 @@ import type { DirectoryType } from "../../domain/entities/File";
12
13
  export async function createDirectory(uri: string): Promise<boolean> {
13
14
  try {
14
15
  const dir = new Directory(uri);
15
- dir.create({ intermediates: true, idempotent: true });
16
+ await dir.create({ intermediates: true, idempotent: true });
16
17
  return true;
17
- } catch {
18
- return false;
18
+ } catch (error) {
19
+ const handled = ErrorHandler.handleAndLog(
20
+ error,
21
+ 'createDirectory',
22
+ { uri }
23
+ );
24
+ throw ErrorHandler.create(
25
+ `Failed to create directory: ${handled.message}`,
26
+ ErrorCodes.DIRECTORY_CREATE_ERROR,
27
+ { uri, originalError: handled }
28
+ );
19
29
  }
20
30
  }
21
31
 
@@ -25,10 +35,19 @@ export async function createDirectory(uri: string): Promise<boolean> {
25
35
  export async function listDirectory(uri: string): Promise<string[]> {
26
36
  try {
27
37
  const dir = new Directory(uri);
28
- const items = dir.list();
38
+ const items = await dir.list();
29
39
  return items.map((item) => item.uri);
30
- } catch {
31
- return [];
40
+ } catch (error) {
41
+ const handled = ErrorHandler.handleAndLog(
42
+ error,
43
+ 'listDirectory',
44
+ { uri }
45
+ );
46
+ throw ErrorHandler.create(
47
+ `Failed to list directory contents: ${handled.message}`,
48
+ ErrorCodes.FILE_READ_ERROR,
49
+ { uri, originalError: handled }
50
+ );
32
51
  }
33
52
  }
34
53
 
@@ -43,10 +62,19 @@ export function getDirectoryPath(type: DirectoryType): string {
43
62
  case "cacheDirectory":
44
63
  return Paths.cache.uri;
45
64
  default:
46
- return "";
65
+ throw ErrorHandler.create(
66
+ `Unknown directory type: ${type}`,
67
+ ErrorCodes.INVALID_INPUT,
68
+ { type }
69
+ );
47
70
  }
48
- } catch {
49
- return "";
71
+ } catch (error) {
72
+ const handled = ErrorHandler.handleAndLog(
73
+ error,
74
+ 'getDirectoryPath',
75
+ { type }
76
+ );
77
+ throw handled;
50
78
  }
51
79
  }
52
80
 
@@ -7,6 +7,8 @@ import type { FileOperationResult } from "../../domain/entities/File";
7
7
  import { FileUtils } from "../../domain/entities/File";
8
8
  import { SUPPORTED_DOWNLOAD_EXTENSIONS, DEFAULT_DOWNLOAD_EXTENSION } from "./download.constants";
9
9
  import type { DownloadProgressCallback, DownloadWithProgressResult } from "./download.types";
10
+ import { ErrorHandler, ErrorCodes } from "../../../utils/errors";
11
+ import { retryWithBackoff, isNetworkError } from "../../../utils/async";
10
12
 
11
13
  const hashUrl = (url: string) => {
12
14
  let hash = 0;
@@ -25,9 +27,29 @@ const getCacheUri = (url: string, dir: string) => FileUtils.joinPaths(dir, `cach
25
27
  export async function downloadFile(url: string, dest?: string): Promise<FileOperationResult> {
26
28
  try {
27
29
  const destination = dest ? new File(dest) : new File(Paths.document, FileUtils.generateUniqueFilename("download"));
28
- const res = await File.downloadFileAsync(url, destination, { idempotent: true });
30
+
31
+ // Retry download with exponential backoff
32
+ const res = await retryWithBackoff(
33
+ () => File.downloadFileAsync(url, destination, { idempotent: true }),
34
+ {
35
+ maxRetries: 3,
36
+ baseDelay: 1000,
37
+ shouldRetry: (error) => isNetworkError(error as Error),
38
+ }
39
+ );
40
+
29
41
  return { success: true, uri: res.uri };
30
- } catch (e: unknown) { return { success: false, error: e instanceof Error ? e.message : String(e) }; }
42
+ } catch (error) {
43
+ const handled = ErrorHandler.handleAndLog(
44
+ error,
45
+ 'downloadFile',
46
+ { url, dest }
47
+ );
48
+ return {
49
+ success: false,
50
+ error: handled.getUserMessage(),
51
+ };
52
+ }
31
53
  }
32
54
 
33
55
  export async function downloadFileWithProgress(
@@ -38,13 +60,19 @@ export async function downloadFileWithProgress(
38
60
  ): Promise<DownloadWithProgressResult> {
39
61
  try {
40
62
  const dir = new Directory(cacheDir);
41
- if (!dir.exists) dir.create({ intermediates: true, idempotent: true });
63
+ if (!dir.exists) await dir.create({ intermediates: true, idempotent: true });
42
64
 
43
65
  const destUri = getCacheUri(url, cacheDir);
44
66
  if (new File(destUri).exists) return { success: true, uri: destUri, fromCache: true };
45
67
 
46
68
  const response = await fetch(url, { signal });
47
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
69
+ if (!response.ok) {
70
+ throw ErrorHandler.create(
71
+ `HTTP ${response.status}: ${response.statusText}`,
72
+ ErrorCodes.NETWORK_ERROR,
73
+ { url, status: response.status }
74
+ );
75
+ }
48
76
 
49
77
  const totalBytes = parseInt(response.headers.get("content-length") || "0", 10);
50
78
  if (!response.body) return { ...(await downloadFile(url, destUri)), fromCache: false };
@@ -57,7 +85,11 @@ export async function downloadFileWithProgress(
57
85
  while (true) {
58
86
  if (signal?.aborted) {
59
87
  await reader.cancel();
60
- throw new Error("Download aborted");
88
+ throw ErrorHandler.create(
89
+ 'Download aborted by user',
90
+ ErrorCodes.NETWORK_ERROR,
91
+ { url }
92
+ );
61
93
  }
62
94
 
63
95
  const { done, value } = await reader.read();
@@ -70,19 +102,38 @@ export async function downloadFileWithProgress(
70
102
  const all = new Uint8Array(received);
71
103
  let pos = 0;
72
104
  for (const c of chunks) { all.set(c, pos); pos += c.length; }
73
- new File(destUri).write(all);
105
+ await new File(destUri).write(all);
74
106
 
75
107
  return { success: true, uri: destUri, fromCache: false };
76
108
  } finally {
77
109
  reader.releaseLock();
78
110
  }
79
- } catch (e: unknown) { return { success: false, error: e instanceof Error ? e.message : String(e) }; }
111
+ } catch (error) {
112
+ const handled = ErrorHandler.handleAndLog(
113
+ error,
114
+ 'downloadFileWithProgress',
115
+ { url, cacheDir }
116
+ );
117
+ return {
118
+ success: false,
119
+ error: handled.getUserMessage(),
120
+ };
121
+ }
80
122
  }
81
123
 
82
124
  export const isUrlCached = (url: string, dir: string) => new File(getCacheUri(url, dir)).exists;
83
125
  export const getCachedFileUri = (url: string, dir: string) => isUrlCached(url, dir) ? getCacheUri(url, dir) : null;
84
- export const deleteCachedFile = (url: string, dir: string) => {
85
- const f = new File(getCacheUri(url, dir));
86
- if (f.exists) f.delete();
87
- return true;
126
+ export const deleteCachedFile = async (url: string, dir: string): Promise<boolean> => {
127
+ try {
128
+ const f = new File(getCacheUri(url, dir));
129
+ if (f.exists) await f.delete();
130
+ return true;
131
+ } catch (error) {
132
+ ErrorHandler.handleAndLog(
133
+ error,
134
+ 'deleteCachedFile',
135
+ { url, dir }
136
+ );
137
+ return false;
138
+ }
88
139
  };
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { File, Directory } from "expo-file-system";
7
7
  import type { FileOperationResult } from "../../domain/entities/File";
8
+ import { ErrorHandler, ErrorCodes } from "../../../utils/errors";
8
9
 
9
10
  /**
10
11
  * Delete file or directory
@@ -15,27 +16,47 @@ export async function deleteFile(uri: string): Promise<boolean> {
15
16
  try {
16
17
  const file = new File(uri);
17
18
  if (file.exists) {
18
- file.delete();
19
+ await file.delete();
19
20
  return true;
20
21
  }
21
- } catch {
22
+ } catch (fileError) {
22
23
  // Not a file, try as directory
24
+ if (__DEV__) {
25
+ console.log('[deleteFile] Not a file, trying as directory:', uri);
26
+ }
23
27
  }
24
28
 
25
29
  // Try as directory
26
30
  try {
27
31
  const dir = new Directory(uri);
28
32
  if (dir.exists) {
29
- dir.delete();
33
+ await dir.delete();
30
34
  return true;
31
35
  }
32
- } catch {
36
+ } catch (dirError) {
33
37
  // Not a directory either
38
+ if (__DEV__) {
39
+ console.log('[deleteFile] Not a directory:', uri);
40
+ }
34
41
  }
35
42
 
36
- return false;
37
- } catch {
38
- return false;
43
+ // File/directory doesn't exist
44
+ throw ErrorHandler.create(
45
+ `File or directory not found: ${uri}`,
46
+ ErrorCodes.FILE_NOT_FOUND,
47
+ { uri }
48
+ );
49
+ } catch (error) {
50
+ const handled = ErrorHandler.handleAndLog(
51
+ error,
52
+ 'deleteFile',
53
+ { uri }
54
+ );
55
+ throw ErrorHandler.create(
56
+ `Failed to delete file: ${handled.message}`,
57
+ ErrorCodes.FILE_DELETE_ERROR,
58
+ { uri, originalError: handled }
59
+ );
39
60
  }
40
61
  }
41
62
 
@@ -49,12 +70,17 @@ export async function copyFile(
49
70
  try {
50
71
  const sourceFile = new File(sourceUri);
51
72
  const destination = new File(destinationUri);
52
- sourceFile.copy(destination);
73
+ await sourceFile.copy(destination);
53
74
  return { success: true, uri: destinationUri };
54
75
  } catch (error) {
76
+ const handled = ErrorHandler.handleAndLog(
77
+ error,
78
+ 'copyFile',
79
+ { sourceUri, destinationUri }
80
+ );
55
81
  return {
56
82
  success: false,
57
- error: error instanceof Error ? error.message : "Unknown error",
83
+ error: handled.getUserMessage(),
58
84
  };
59
85
  }
60
86
  }
@@ -69,12 +95,17 @@ export async function moveFile(
69
95
  try {
70
96
  const sourceFile = new File(sourceUri);
71
97
  const destination = new File(destinationUri);
72
- sourceFile.move(destination);
98
+ await sourceFile.move(destination);
73
99
  return { success: true, uri: destinationUri };
74
100
  } catch (error) {
101
+ const handled = ErrorHandler.handleAndLog(
102
+ error,
103
+ 'moveFile',
104
+ { sourceUri, destinationUri }
105
+ );
75
106
  return {
76
107
  success: false,
77
- error: error instanceof Error ? error.message : "Unknown error",
108
+ error: handled.getUserMessage(),
78
109
  };
79
110
  }
80
111
  }