@umituz/react-native-design-system 4.23.112 → 4.23.114

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 (33) 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 +2 -2
  12. package/src/filesystem/infrastructure/services/download.service.ts +4 -4
  13. package/src/filesystem/infrastructure/services/file-manager.service.ts +4 -4
  14. package/src/filesystem/infrastructure/services/file-writer.service.ts +1 -1
  15. package/src/media/presentation/hooks/useMedia.ts +60 -22
  16. package/src/molecules/BaseModal.tsx +1 -0
  17. package/src/molecules/ConfirmationModalMain.tsx +1 -0
  18. package/src/molecules/ListItem.tsx +15 -1
  19. package/src/molecules/avatar/Avatar.tsx +28 -11
  20. package/src/molecules/bottom-sheet/components/BottomSheet.tsx +1 -0
  21. package/src/molecules/calendar/presentation/components/AtomicCalendar.tsx +1 -1
  22. package/src/responsive/useResponsive.ts +1 -1
  23. package/src/storage/presentation/hooks/usePersistentCache.ts +20 -12
  24. package/src/storage/presentation/hooks/useStore.ts +1 -0
  25. package/src/tanstack/presentation/hooks/usePrefetch.ts +14 -0
  26. package/src/theme/infrastructure/stores/themeStore.ts +13 -11
  27. package/src/timezone/infrastructure/services/BusinessCalendarManager.ts +1 -0
  28. package/src/timezone/infrastructure/services/CalendarManager.ts +2 -2
  29. package/src/timezone/infrastructure/services/DateComparisonUtils.ts +1 -0
  30. package/src/timezone/infrastructure/services/DateFormatter.ts +3 -2
  31. package/src/timezone/infrastructure/services/DateRangeUtils.ts +1 -0
  32. package/src/timezone/infrastructure/utils/TimezoneParsers.ts +27 -0
  33. package/src/utilities/sharing/presentation/hooks/useSharing.ts +44 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "4.23.112",
3
+ "version": "4.23.114",
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
  }
@@ -12,7 +12,7 @@ import type { DirectoryType } from "../../domain/entities/File";
12
12
  export async function createDirectory(uri: string): Promise<boolean> {
13
13
  try {
14
14
  const dir = new Directory(uri);
15
- dir.create({ intermediates: true, idempotent: true });
15
+ await dir.create({ intermediates: true, idempotent: true });
16
16
  return true;
17
17
  } catch {
18
18
  return false;
@@ -25,7 +25,7 @@ export async function createDirectory(uri: string): Promise<boolean> {
25
25
  export async function listDirectory(uri: string): Promise<string[]> {
26
26
  try {
27
27
  const dir = new Directory(uri);
28
- const items = dir.list();
28
+ const items = await dir.list();
29
29
  return items.map((item) => item.uri);
30
30
  } catch {
31
31
  return [];
@@ -38,7 +38,7 @@ export async function downloadFileWithProgress(
38
38
  ): Promise<DownloadWithProgressResult> {
39
39
  try {
40
40
  const dir = new Directory(cacheDir);
41
- if (!dir.exists) dir.create({ intermediates: true, idempotent: true });
41
+ if (!dir.exists) await dir.create({ intermediates: true, idempotent: true });
42
42
 
43
43
  const destUri = getCacheUri(url, cacheDir);
44
44
  if (new File(destUri).exists) return { success: true, uri: destUri, fromCache: true };
@@ -70,7 +70,7 @@ export async function downloadFileWithProgress(
70
70
  const all = new Uint8Array(received);
71
71
  let pos = 0;
72
72
  for (const c of chunks) { all.set(c, pos); pos += c.length; }
73
- new File(destUri).write(all);
73
+ await new File(destUri).write(all);
74
74
 
75
75
  return { success: true, uri: destUri, fromCache: false };
76
76
  } finally {
@@ -81,8 +81,8 @@ export async function downloadFileWithProgress(
81
81
 
82
82
  export const isUrlCached = (url: string, dir: string) => new File(getCacheUri(url, dir)).exists;
83
83
  export const getCachedFileUri = (url: string, dir: string) => isUrlCached(url, dir) ? getCacheUri(url, dir) : null;
84
- export const deleteCachedFile = (url: string, dir: string) => {
84
+ export const deleteCachedFile = async (url: string, dir: string) => {
85
85
  const f = new File(getCacheUri(url, dir));
86
- if (f.exists) f.delete();
86
+ if (f.exists) await f.delete();
87
87
  return true;
88
88
  };
@@ -15,7 +15,7 @@ export async function deleteFile(uri: string): Promise<boolean> {
15
15
  try {
16
16
  const file = new File(uri);
17
17
  if (file.exists) {
18
- file.delete();
18
+ await file.delete();
19
19
  return true;
20
20
  }
21
21
  } catch {
@@ -26,7 +26,7 @@ export async function deleteFile(uri: string): Promise<boolean> {
26
26
  try {
27
27
  const dir = new Directory(uri);
28
28
  if (dir.exists) {
29
- dir.delete();
29
+ await dir.delete();
30
30
  return true;
31
31
  }
32
32
  } catch {
@@ -49,7 +49,7 @@ export async function copyFile(
49
49
  try {
50
50
  const sourceFile = new File(sourceUri);
51
51
  const destination = new File(destinationUri);
52
- sourceFile.copy(destination);
52
+ await sourceFile.copy(destination);
53
53
  return { success: true, uri: destinationUri };
54
54
  } catch (error) {
55
55
  return {
@@ -69,7 +69,7 @@ export async function moveFile(
69
69
  try {
70
70
  const sourceFile = new File(sourceUri);
71
71
  const destination = new File(destinationUri);
72
- sourceFile.move(destination);
72
+ await sourceFile.move(destination);
73
73
  return { success: true, uri: destinationUri };
74
74
  } catch (error) {
75
75
  return {
@@ -18,7 +18,7 @@ export async function writeFile(
18
18
  try {
19
19
  const encodingType = getEncodingType(encoding);
20
20
  const file = new File(uri);
21
- file.write(content, {
21
+ await file.write(content, {
22
22
  encoding: encodingType as ExpoEncodingType,
23
23
  });
24
24
  return { success: true, uri };
@@ -5,7 +5,7 @@
5
5
  * Provides camera, gallery picking functionality.
6
6
  */
7
7
 
8
- import { useState, useCallback } from "react";
8
+ import { useState, useCallback, useEffect, useRef } from "react";
9
9
  import { MediaPickerService } from "../../infrastructure/services/MediaPickerService";
10
10
  import { PermissionManager } from "../../infrastructure/utils/PermissionManager";
11
11
  import type {
@@ -39,24 +39,38 @@ export const useMedia = () => {
39
39
  const [isLoading, setIsLoading] = useState(false);
40
40
  const [error, setError] = useState<string | null>(null);
41
41
 
42
+ // Track mounted state to prevent setState on unmounted component
43
+ const isMountedRef = useRef(true);
44
+ useEffect(() => {
45
+ return () => {
46
+ isMountedRef.current = false;
47
+ };
48
+ }, []);
49
+
42
50
  const pickImage = useCallback(
43
51
  async (options?: MediaPickerOptions): Promise<MediaPickerResult> => {
44
- setIsLoading(true);
45
- setError(null);
52
+ if (isMountedRef.current) {
53
+ setIsLoading(true);
54
+ setError(null);
55
+ }
46
56
  try {
47
57
  const result = await MediaPickerService.pickSingleImage(options);
48
58
  // Set error from validation result if present
49
- if (result.errorMessage) {
59
+ if (result.errorMessage && isMountedRef.current) {
50
60
  setError(result.errorMessage);
51
61
  }
52
62
  return result;
53
63
  } catch (err) {
54
64
  const errorMessage =
55
65
  err instanceof Error ? err.message : "Failed to pick image";
56
- setError(errorMessage);
66
+ if (isMountedRef.current) {
67
+ setError(errorMessage);
68
+ }
57
69
  return { canceled: true };
58
70
  } finally {
59
- setIsLoading(false);
71
+ if (isMountedRef.current) {
72
+ setIsLoading(false);
73
+ }
60
74
  }
61
75
  },
62
76
  []
@@ -64,18 +78,24 @@ export const useMedia = () => {
64
78
 
65
79
  const pickMultipleImages = useCallback(
66
80
  async (options?: MediaPickerOptions): Promise<MediaPickerResult> => {
67
- setIsLoading(true);
68
- setError(null);
81
+ if (isMountedRef.current) {
82
+ setIsLoading(true);
83
+ setError(null);
84
+ }
69
85
  try {
70
86
  const result = await MediaPickerService.pickMultipleImages(options);
71
87
  return result;
72
88
  } catch (err) {
73
89
  const errorMessage =
74
90
  err instanceof Error ? err.message : "Failed to pick images";
75
- setError(errorMessage);
91
+ if (isMountedRef.current) {
92
+ setError(errorMessage);
93
+ }
76
94
  return { canceled: true };
77
95
  } finally {
78
- setIsLoading(false);
96
+ if (isMountedRef.current) {
97
+ setIsLoading(false);
98
+ }
79
99
  }
80
100
  },
81
101
  []
@@ -83,18 +103,24 @@ export const useMedia = () => {
83
103
 
84
104
  const pickVideo = useCallback(
85
105
  async (options?: MediaPickerOptions): Promise<MediaPickerResult> => {
86
- setIsLoading(true);
87
- setError(null);
106
+ if (isMountedRef.current) {
107
+ setIsLoading(true);
108
+ setError(null);
109
+ }
88
110
  try {
89
111
  const result = await MediaPickerService.pickVideo(options);
90
112
  return result;
91
113
  } catch (err) {
92
114
  const errorMessage =
93
115
  err instanceof Error ? err.message : "Failed to pick video";
94
- setError(errorMessage);
116
+ if (isMountedRef.current) {
117
+ setError(errorMessage);
118
+ }
95
119
  return { canceled: true };
96
120
  } finally {
97
- setIsLoading(false);
121
+ if (isMountedRef.current) {
122
+ setIsLoading(false);
123
+ }
98
124
  }
99
125
  },
100
126
  []
@@ -102,18 +128,24 @@ export const useMedia = () => {
102
128
 
103
129
  const launchCamera = useCallback(
104
130
  async (options?: CameraOptions): Promise<MediaPickerResult> => {
105
- setIsLoading(true);
106
- setError(null);
131
+ if (isMountedRef.current) {
132
+ setIsLoading(true);
133
+ setError(null);
134
+ }
107
135
  try {
108
136
  const result = await MediaPickerService.launchCamera(options);
109
137
  return result;
110
138
  } catch (err) {
111
139
  const errorMessage =
112
140
  err instanceof Error ? err.message : "Failed to launch camera";
113
- setError(errorMessage);
141
+ if (isMountedRef.current) {
142
+ setError(errorMessage);
143
+ }
114
144
  return { canceled: true };
115
145
  } finally {
116
- setIsLoading(false);
146
+ if (isMountedRef.current) {
147
+ setIsLoading(false);
148
+ }
117
149
  }
118
150
  },
119
151
  []
@@ -121,18 +153,24 @@ export const useMedia = () => {
121
153
 
122
154
  const launchCameraForVideo = useCallback(
123
155
  async (options?: CameraOptions): Promise<MediaPickerResult> => {
124
- setIsLoading(true);
125
- setError(null);
156
+ if (isMountedRef.current) {
157
+ setIsLoading(true);
158
+ setError(null);
159
+ }
126
160
  try {
127
161
  const result = await MediaPickerService.launchCameraForVideo(options);
128
162
  return result;
129
163
  } catch (err) {
130
164
  const errorMessage =
131
165
  err instanceof Error ? err.message : "Failed to record video";
132
- setError(errorMessage);
166
+ if (isMountedRef.current) {
167
+ setError(errorMessage);
168
+ }
133
169
  return { canceled: true };
134
170
  } finally {
135
- setIsLoading(false);
171
+ if (isMountedRef.current) {
172
+ setIsLoading(false);
173
+ }
136
174
  }
137
175
  },
138
176
  []
@@ -51,6 +51,7 @@ export const BaseModal: React.FC<BaseModalProps> = ({
51
51
  onRequestClose={onClose}
52
52
  statusBarTranslucent
53
53
  testID={testID}
54
+ accessibilityViewIsModal={true}
54
55
  >
55
56
  <View style={styles.overlay}>
56
57
  <TouchableOpacity
@@ -64,6 +64,7 @@ export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
64
64
  onRequestClose={onCancel}
65
65
  statusBarTranslucent
66
66
  testID={testID}
67
+ accessibilityViewIsModal={true}
67
68
  >
68
69
  <View style={getModalOverlayStyle()}>
69
70
  <ConfirmationModalBackdrop
@@ -14,8 +14,22 @@ export const ListItem: React.FC<ListItemProps> = ({
14
14
  const listItemStyles = getListItemStyles(tokens);
15
15
  const Component = onPress ? TouchableOpacity : View;
16
16
 
17
+ const accessibilityProps = onPress
18
+ ? {
19
+ accessibilityRole: 'button' as const,
20
+ accessibilityLabel: title,
21
+ accessibilityState: { disabled },
22
+ }
23
+ : {};
24
+
17
25
  return (
18
- <Component style={[listItemStyles.container, disabled ? listItemStyles.disabled : undefined, style]} onPress={onPress} disabled={disabled} activeOpacity={0.7}>
26
+ <Component
27
+ style={[listItemStyles.container, disabled ? listItemStyles.disabled : undefined, style]}
28
+ onPress={onPress}
29
+ disabled={disabled}
30
+ activeOpacity={0.7}
31
+ {...accessibilityProps}
32
+ >
19
33
  {leftIcon && (
20
34
  <AtomicIcon
21
35
  name={leftIcon}
@@ -5,8 +5,8 @@
5
5
  * Handles loading states, fallbacks, and status indicators.
6
6
  */
7
7
 
8
- import React from 'react';
9
- import { View, Image, StyleSheet, type StyleProp, type ViewStyle, type ImageStyle } from 'react-native';
8
+ import React, { useMemo } from 'react';
9
+ import { View, Image, StyleSheet, TouchableOpacity, type StyleProp, type ViewStyle, type ImageStyle } from 'react-native';
10
10
  import { useAppDesignTokens } from '../../theme';
11
11
  import { AtomicText, AtomicIcon } from '../../atoms';
12
12
  import type { AvatarSize, AvatarShape } from './Avatar.types';
@@ -62,20 +62,29 @@ export const Avatar: React.FC<AvatarProps> = ({
62
62
  onPress,
63
63
  }) => {
64
64
  const tokens = useAppDesignTokens();
65
- const config = SIZE_CONFIGS[size];
65
+ const config = useMemo(() => SIZE_CONFIGS[size], [size]);
66
66
 
67
67
  // Determine avatar type and content
68
68
  const hasImage = !!uri;
69
69
  const hasName = !!name;
70
- const initials = hasName ? AvatarUtils.generateInitials(name) : AVATAR_CONSTANTS.FALLBACK_INITIALS;
71
- const bgColor = backgroundColor || (hasName ? AvatarUtils.getColorForName(name) : tokens.colors.surfaceSecondary);
72
- const borderRadius = AvatarUtils.getBorderRadius(shape, config.size);
70
+ const initials = useMemo(
71
+ () => (hasName ? AvatarUtils.generateInitials(name) : AVATAR_CONSTANTS.FALLBACK_INITIALS),
72
+ [hasName, name]
73
+ );
74
+ const bgColor = useMemo(
75
+ () => backgroundColor || (hasName ? AvatarUtils.getColorForName(name) : tokens.colors.surfaceSecondary),
76
+ [backgroundColor, hasName, name, tokens.colors.surfaceSecondary]
77
+ );
78
+ const borderRadius = useMemo(
79
+ () => AvatarUtils.getBorderRadius(shape, config.size),
80
+ [shape, config.size]
81
+ );
73
82
 
74
83
  // Status indicator position
75
- const statusPosition = {
84
+ const statusPosition = useMemo(() => ({
76
85
  bottom: 0,
77
86
  right: 0,
78
- };
87
+ }), []);
79
88
 
80
89
  const renderContent = () => {
81
90
  if (hasImage) {
@@ -122,8 +131,10 @@ export const Avatar: React.FC<AvatarProps> = ({
122
131
  );
123
132
  };
124
133
 
134
+ const AvatarWrapper = onPress ? TouchableOpacity : View;
135
+
125
136
  return (
126
- <View
137
+ <AvatarWrapper
127
138
  style={[
128
139
  styles.container,
129
140
  {
@@ -134,7 +145,11 @@ export const Avatar: React.FC<AvatarProps> = ({
134
145
  },
135
146
  style,
136
147
  ]}
137
- onTouchEnd={onPress}
148
+ onPress={onPress}
149
+ disabled={!onPress}
150
+ accessibilityRole={onPress ? 'button' : 'image'}
151
+ accessibilityLabel={name || 'User avatar'}
152
+ accessible={true}
138
153
  >
139
154
  {renderContent()}
140
155
 
@@ -153,9 +168,11 @@ export const Avatar: React.FC<AvatarProps> = ({
153
168
  ...statusPosition,
154
169
  },
155
170
  ]}
171
+ accessibilityLabel={`Status: ${status}`}
172
+ accessibilityRole="none"
156
173
  />
157
174
  )}
158
- </View>
175
+ </AvatarWrapper>
159
176
  );
160
177
  };
161
178
 
@@ -99,6 +99,7 @@ export const BottomSheet = forwardRef<BottomSheetRef, BottomSheetProps>((props,
99
99
  animationType="none"
100
100
  onRequestClose={dismiss}
101
101
  statusBarTranslucent
102
+ accessibilityViewIsModal={true}
102
103
  >
103
104
  <Pressable style={styles.overlay} onPress={dismiss}>
104
105
  <View style={styles.container}>
@@ -119,7 +119,7 @@ export const AtomicCalendar: React.FC<AtomicCalendarProps> = ({
119
119
 
120
120
  return (
121
121
  <CalendarDayCell
122
- key={index}
122
+ key={day.date.getTime()}
123
123
  day={day}
124
124
  index={index}
125
125
  isSelected={isSelected}
@@ -124,7 +124,7 @@ export const useResponsive = (): UseResponsiveReturn => {
124
124
  getGridCols,
125
125
  };
126
126
  },
127
- [width, height, getLogoSize, getInputHeight, getIconSize, getMaxWidth, getFontSize, getGridCols],
127
+ [width, height, insets, getLogoSize, getInputHeight, getIconSize, getMaxWidth, getFontSize, getGridCols],
128
128
  );
129
129
 
130
130
  return responsiveValues;
@@ -99,16 +99,24 @@ export function usePersistentCache<T>(
99
99
  // Track if component is mounted to prevent state updates after unmount
100
100
  const isMountedRef = useRef(true);
101
101
 
102
+ // Stabilize actions to prevent circular dependency
103
+ const stableActionsRef = useRef(actions);
104
+ useEffect(() => {
105
+ stableActionsRef.current = actions;
106
+ });
107
+
102
108
  const loadFromStorage = useCallback(async () => {
109
+ const currentActions = stableActionsRef.current;
110
+
103
111
  if (!enabled) {
104
112
  if (isMountedRef.current) {
105
- actions.setLoading(false);
113
+ currentActions.setLoading(false);
106
114
  }
107
115
  return;
108
116
  }
109
117
 
110
118
  if (isMountedRef.current) {
111
- actions.setLoading(true);
119
+ currentActions.setLoading(true);
112
120
  }
113
121
 
114
122
  try {
@@ -117,35 +125,35 @@ export function usePersistentCache<T>(
117
125
  if (isMountedRef.current) {
118
126
  if (cached) {
119
127
  const expired = isCacheExpired(cached, version);
120
- actions.setData(cached.value);
121
- actions.setExpired(expired);
128
+ currentActions.setData(cached.value);
129
+ currentActions.setExpired(expired);
122
130
  } else {
123
- actions.clearData();
131
+ currentActions.clearData();
124
132
  }
125
133
  }
126
134
  } catch {
127
135
  if (isMountedRef.current) {
128
- actions.clearData();
136
+ currentActions.clearData();
129
137
  }
130
138
  } finally {
131
139
  if (isMountedRef.current) {
132
- actions.setLoading(false);
140
+ currentActions.setLoading(false);
133
141
  }
134
142
  }
135
- }, [key, version, enabled, actions, cacheOps]);
143
+ }, [key, version, enabled, cacheOps]); // Removed actions from dependencies
136
144
 
137
145
  const setData = useCallback(
138
146
  async (value: T) => {
139
147
  await cacheOps.saveToStorage(key, value, { ttl, version, enabled });
140
- actions.setData(value);
148
+ stableActionsRef.current.setData(value);
141
149
  },
142
- [key, ttl, version, enabled, actions, cacheOps],
150
+ [key, ttl, version, enabled, cacheOps],
143
151
  );
144
152
 
145
153
  const clearData = useCallback(async () => {
146
154
  await cacheOps.clearFromStorage(key, enabled);
147
- actions.clearData();
148
- }, [key, enabled, actions, cacheOps]);
155
+ stableActionsRef.current.clearData();
156
+ }, [key, enabled, cacheOps]);
149
157
 
150
158
  const refresh = useCallback(async () => {
151
159
  await loadFromStorage();
@@ -16,6 +16,7 @@ export function useStore<T extends object>(config: StoreConfig<T>) {
16
16
  config.version,
17
17
  config.persist,
18
18
  config.storage,
19
+ JSON.stringify(config.initialState), // Track initialState changes
19
20
  ]
20
21
  );
21
22
 
@@ -30,6 +30,13 @@ export function usePrefetchQuery<
30
30
  const queryClient = useQueryClient();
31
31
  const prefetchingRef = useRef(new Set<TVariables>());
32
32
 
33
+ // Cleanup on unmount
34
+ useEffect(() => {
35
+ return () => {
36
+ prefetchingRef.current.clear();
37
+ };
38
+ }, []);
39
+
33
40
  return useCallback(
34
41
  async (variables: TVariables) => {
35
42
  if (prefetchingRef.current.has(variables)) return;
@@ -62,6 +69,13 @@ export function usePrefetchInfiniteQuery<
62
69
  const queryClient = useQueryClient();
63
70
  const hasPrefetchedRef = useRef(false);
64
71
 
72
+ // Cleanup on unmount
73
+ useEffect(() => {
74
+ return () => {
75
+ hasPrefetchedRef.current = false;
76
+ };
77
+ }, []);
78
+
65
79
  return useCallback(async () => {
66
80
  if (hasPrefetchedRef.current) return;
67
81
 
@@ -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
  []