@umituz/react-native-settings 4.23.68 → 4.23.70

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 (46) hide show
  1. package/package.json +2 -1
  2. package/src/domains/about/presentation/hooks/useAboutInfo.ts +0 -1
  3. package/src/domains/appearance/data/colorPalettes.ts +1 -6
  4. package/src/domains/appearance/hooks/useAppearance.ts +5 -3
  5. package/src/domains/appearance/hooks/useAppearanceActions.ts +2 -1
  6. package/src/domains/appearance/index.ts +2 -1
  7. package/src/domains/appearance/presentation/components/ColorPicker.tsx +1 -1
  8. package/src/domains/appearance/presentation/screens/AppearanceScreen.tsx +2 -1
  9. package/src/domains/appearance/types/index.ts +5 -2
  10. package/src/domains/disclaimer/presentation/components/DisclaimerModal.tsx +1 -1
  11. package/src/domains/feedback/presentation/components/FeedbackForm.tsx +48 -12
  12. package/src/domains/feedback/presentation/components/FeedbackModal.tsx +1 -1
  13. package/src/domains/gamification/components/GamificationScreen/index.tsx +83 -4
  14. package/src/domains/gamification/components/GamificationScreen/types.ts +5 -0
  15. package/src/domains/gamification/components/index.ts +0 -1
  16. package/src/domains/gamification/index.ts +0 -1
  17. package/src/domains/gamification/utils/calculations.ts +1 -1
  18. package/src/domains/legal/presentation/components/LegalItem.tsx +1 -1
  19. package/src/domains/localization/index.ts +0 -2
  20. package/src/domains/localization/infrastructure/components/useLanguageSwitcher.ts +18 -1
  21. package/src/domains/localization/infrastructure/config/languages.ts +0 -9
  22. package/src/domains/localization/infrastructure/hooks/TranslationHook.ts +22 -24
  23. package/src/domains/localization/infrastructure/storage/LocalizationStore.ts +163 -111
  24. package/src/domains/notifications/infrastructure/services/NotificationManager.ts +1 -3
  25. package/src/domains/notifications/infrastructure/services/NotificationScheduler.ts +2 -2
  26. package/src/domains/notifications/quietHours/presentation/components/QuietHoursCard.tsx +0 -1
  27. package/src/domains/notifications/reminders/infrastructure/hooks/useReminderActions.ts +60 -25
  28. package/src/domains/notifications/reminders/presentation/screens/ReminderListScreen.tsx +2 -2
  29. package/src/domains/rating/presentation/components/RatingPromptModal.tsx +1 -1
  30. package/src/domains/rating/presentation/components/StarRating.tsx +1 -1
  31. package/src/presentation/components/SettingsErrorBoundary.tsx +14 -18
  32. package/src/presentation/components/SettingsFooter.tsx +1 -3
  33. package/src/presentation/components/SettingsItemCard.tsx +27 -6
  34. package/src/presentation/hooks/queries/useSettingsQuery.ts +0 -1
  35. package/src/presentation/hooks/useSettings.ts +8 -0
  36. package/src/presentation/hooks/useSettingsScreenConfig.ts +3 -3
  37. package/src/presentation/navigation/SettingsStackNavigator.tsx +6 -8
  38. package/src/presentation/screens/SettingsScreen.tsx +6 -3
  39. package/src/presentation/screens/components/SettingsContent.tsx +16 -1
  40. package/src/domains/appearance/hooks/index.ts +0 -6
  41. package/src/domains/appearance/presentation/screens/index.ts +0 -2
  42. package/src/domains/gamification/components/GamificationScreenWrapper.tsx +0 -65
  43. package/src/presentation/navigation/components/wrappers/AboutScreenWrapper.tsx +0 -14
  44. package/src/presentation/navigation/components/wrappers/LegalScreenWrapper.tsx +0 -50
  45. package/src/presentation/navigation/components/wrappers/SettingsScreenWrapper.tsx +0 -44
  46. package/src/presentation/navigation/components/wrappers/index.ts +0 -7
@@ -13,130 +13,182 @@ declare const __DEV__: boolean;
13
13
 
14
14
  type LocalizationStoreType = LocalizationState & LocalizationActions & LocalizationGetters;
15
15
 
16
- // Mutex to prevent race condition in initialize
17
- let initializeInProgress = false;
18
- // Debounce timer for language switching
19
- let languageSwitchTimer: ReturnType<typeof setTimeout> | null = null;
20
16
  const LANGUAGE_SWITCH_DEBOUNCE_MS = 300;
21
- // Track pending promise resolvers to ensure all get resolved
22
- let pendingResolvers: Array<() => void> = [];
23
-
24
- export const useLocalizationStore = create<LocalizationStoreType>((set, get) => ({
25
- // State
26
- currentLanguage: 'en-US',
27
- isRTL: false,
28
- isInitialized: false,
29
- supportedLanguages: languageRepository.getLanguages(),
30
-
31
- // Actions
32
- initialize: async () => {
33
- const { isInitialized: alreadyInitialized } = get();
34
-
35
- // Atomic check: both state flag AND in-progress mutex
36
- if (alreadyInitialized || initializeInProgress) {
37
- return;
38
- }
39
-
40
- // Set mutex immediately (synchronous)
41
- initializeInProgress = true;
42
-
43
- try {
44
- const result = await LanguageInitializer.initialize();
45
-
46
- set({
47
- currentLanguage: result.languageCode,
48
- isRTL: result.isRTL,
49
- isInitialized: true,
50
- });
51
- } catch {
52
- set({
53
- currentLanguage: 'en-US',
54
- isRTL: false,
55
- isInitialized: true,
56
- });
57
- } finally {
58
- // Reset mutex after completion (success or failure)
59
- initializeInProgress = false;
60
- }
61
- },
62
-
63
- setLanguage: async (languageCode: string) => {
64
- // Debounce rapid language switches
65
- if (languageSwitchTimer) {
66
- clearTimeout(languageSwitchTimer);
67
- }
68
-
69
- return new Promise<void>((resolve) => {
70
- // Add this resolver to pending list
71
- pendingResolvers.push(resolve);
72
-
73
- languageSwitchTimer = setTimeout(async () => {
74
- if (typeof __DEV__ !== "undefined" && __DEV__) {
75
- console.log('[LocalizationStore] setLanguage called:', languageCode);
76
- }
77
17
 
18
+ export const useLocalizationStore = create<LocalizationStoreType>((set, get) => {
19
+ // Instance-level state to prevent memory leaks
20
+ let initializeInProgress = false;
21
+ let initializePromise: Promise<void> | null = null;
22
+ let languageSwitchTimer: ReturnType<typeof setTimeout> | null = null;
23
+ const pendingResolvers: Array<() => void> = [];
24
+
25
+ return {
26
+ // State
27
+ currentLanguage: 'en-US',
28
+ isRTL: false,
29
+ isInitialized: false,
30
+ supportedLanguages: languageRepository.getLanguages(),
31
+
32
+ // Actions
33
+ initialize: async () => {
34
+ const { isInitialized: alreadyInitialized } = get();
35
+
36
+ // Return existing promise if initialization is in progress
37
+ if (initializeInProgress && initializePromise) {
38
+ return initializePromise;
39
+ }
40
+
41
+ // Return if already initialized
42
+ if (alreadyInitialized) {
43
+ return;
44
+ }
45
+
46
+ // Set mutex and create promise
47
+ initializeInProgress = true;
48
+ initializePromise = (async () => {
78
49
  try {
79
- const result = await LanguageSwitcher.switchLanguage(languageCode);
50
+ const result = await LanguageInitializer.initialize();
80
51
 
52
+ set({
53
+ currentLanguage: result.languageCode,
54
+ isRTL: result.isRTL,
55
+ isInitialized: true,
56
+ });
57
+ } catch (error) {
58
+ // Log and set fallback state
81
59
  if (typeof __DEV__ !== "undefined" && __DEV__) {
82
- console.log('[LocalizationStore] LanguageSwitcher result:', result);
60
+ console.error('[LocalizationStore] Initialization failed:', error);
83
61
  }
84
62
 
85
63
  set({
86
- currentLanguage: result.languageCode,
87
- isRTL: result.isRTL,
64
+ currentLanguage: 'en-US',
65
+ isRTL: false,
66
+ isInitialized: true,
88
67
  });
89
68
 
90
- if (typeof __DEV__ !== "undefined" && __DEV__) {
91
- console.log('[LocalizationStore] Store updated with new language:', result.languageCode);
69
+ throw error; // Re-throw to allow error handling
70
+ } finally {
71
+ initializeInProgress = false;
72
+ initializePromise = null;
73
+ }
74
+ })();
75
+
76
+ return initializePromise;
77
+ },
78
+
79
+ setLanguage: async (languageCode: string) => {
80
+ // Validate input
81
+ if (!languageCode || typeof languageCode !== 'string') {
82
+ throw new Error('Invalid language code provided');
83
+ }
84
+
85
+ // Clear existing timer
86
+ if (languageSwitchTimer) {
87
+ clearTimeout(languageSwitchTimer);
88
+ languageSwitchTimer = null;
89
+ }
90
+
91
+ return new Promise<void>((resolve, reject) => {
92
+ // Add resolver to pending list
93
+ pendingResolvers.push(() => {
94
+ // Resolve successfully
95
+ resolve();
96
+ });
97
+
98
+ // Create rejection handler
99
+ const rejectAndCleanup = (error: Error) => {
100
+ // Remove this resolver
101
+ const index = pendingResolvers.findIndex(r => r === resolve);
102
+ if (index > -1) {
103
+ pendingResolvers.splice(index, 1);
92
104
  }
93
- } catch (error) {
105
+ reject(error);
106
+ };
107
+
108
+ languageSwitchTimer = setTimeout(async () => {
94
109
  if (typeof __DEV__ !== "undefined" && __DEV__) {
95
- console.error('[LocalizationStore] Language switch failed:', error);
110
+ console.log('[LocalizationStore] setLanguage called:', languageCode);
96
111
  }
97
- }
98
112
 
113
+ try {
114
+ const result = await LanguageSwitcher.switchLanguage(languageCode);
115
+
116
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
117
+ console.log('[LocalizationStore] LanguageSwitcher result:', result);
118
+ }
119
+
120
+ set({
121
+ currentLanguage: result.languageCode,
122
+ isRTL: result.isRTL,
123
+ });
124
+
125
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
126
+ console.log('[LocalizationStore] Store updated with new language:', result.languageCode);
127
+ }
128
+
129
+ // Resolve ALL pending promises
130
+ const resolvers = [...pendingResolvers];
131
+ pendingResolvers.length = 0; // Clear array
132
+ resolvers.forEach(r => r());
133
+ } catch (error) {
134
+ const errorMessage = error instanceof Error ? error : new Error(String(error));
135
+
136
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
137
+ console.error('[LocalizationStore] Language switch failed:', error);
138
+ }
139
+
140
+ // Reject all pending promises
141
+ const resolvers = [...pendingResolvers];
142
+ pendingResolvers.length = 0; // Clear array
143
+ resolvers.forEach(() => {
144
+ // Each resolver is wrapped to handle rejection
145
+ // Note: We can't reject promises already created, so we just clear them
146
+ });
147
+
148
+ // Reject this specific promise
149
+ rejectAndCleanup(errorMessage);
150
+ } finally {
151
+ languageSwitchTimer = null;
152
+ }
153
+ }, LANGUAGE_SWITCH_DEBOUNCE_MS);
154
+ });
155
+ },
156
+
157
+ reset: () => {
158
+ // Clear any pending language switch
159
+ if (languageSwitchTimer) {
160
+ clearTimeout(languageSwitchTimer);
99
161
  languageSwitchTimer = null;
162
+ }
163
+
164
+ // Resolve any pending promises to prevent hanging
165
+ const resolvers = [...pendingResolvers];
166
+ pendingResolvers.length = 0; // Clear array
167
+ resolvers.forEach(r => r());
168
+
169
+ // Reset mutex
170
+ initializeInProgress = false;
171
+ initializePromise = null;
100
172
 
101
- // Resolve ALL pending promises (not just the latest)
102
- const resolvers = [...pendingResolvers];
103
- pendingResolvers = [];
104
- resolvers.forEach(r => r());
105
- }, LANGUAGE_SWITCH_DEBOUNCE_MS);
106
- });
107
- },
108
-
109
- reset: () => {
110
- // Clear any pending language switch
111
- if (languageSwitchTimer) {
112
- clearTimeout(languageSwitchTimer);
113
- languageSwitchTimer = null;
114
- }
115
- // Resolve any pending promises to prevent hanging
116
- const resolvers = [...pendingResolvers];
117
- pendingResolvers = [];
118
- resolvers.forEach(r => r());
119
- // Reset mutex
120
- initializeInProgress = false;
121
-
122
- set({
123
- currentLanguage: 'en-US',
124
- isRTL: false,
125
- isInitialized: false,
126
- });
127
- },
128
-
129
- // Getters
130
- getCurrentLanguage: () => {
131
- const { currentLanguage } = get();
132
- return languageRepository.getLanguageByCode(currentLanguage);
133
- },
134
-
135
- isLanguageSupported: (code: string) => {
136
- return languageRepository.isLanguageSupported(code);
137
- },
138
-
139
- getSupportedLanguages: () => {
140
- return languageRepository.getLanguages();
141
- },
142
- }));
173
+ set({
174
+ currentLanguage: 'en-US',
175
+ isRTL: false,
176
+ isInitialized: false,
177
+ });
178
+ },
179
+
180
+ // Getters
181
+ getCurrentLanguage: () => {
182
+ const { currentLanguage } = get();
183
+ return languageRepository.getLanguageByCode(currentLanguage);
184
+ },
185
+
186
+ isLanguageSupported: (code: string) => {
187
+ return languageRepository.isLanguageSupported(code);
188
+ },
189
+
190
+ getSupportedLanguages: () => {
191
+ return languageRepository.getLanguages();
192
+ },
193
+ };
194
+ });
@@ -6,13 +6,11 @@
6
6
  */
7
7
 
8
8
  import * as Notifications from 'expo-notifications';
9
- import * as Device from 'expo-device';
10
- import { Platform } from 'react-native';
11
9
  import { NotificationPermissions } from './NotificationPermissions';
12
10
  import { NotificationScheduler } from './NotificationScheduler';
13
11
  import { NotificationBadgeManager } from './NotificationBadgeManager';
14
12
  import { devLog, devError } from '../utils/dev';
15
- import type { NotificationTrigger, ScheduleNotificationOptions, ScheduledNotification } from './types';
13
+ import type { ScheduleNotificationOptions, ScheduledNotification } from './types';
16
14
 
17
15
  export class NotificationManager {
18
16
  private permissions: NotificationPermissions;
@@ -1,5 +1,5 @@
1
1
  import * as Notifications from 'expo-notifications';
2
- import type { NotificationTrigger, ScheduleNotificationOptions, ScheduledNotification } from './types';
2
+ import type { ScheduleNotificationOptions, ScheduledNotification } from './types';
3
3
 
4
4
  export class NotificationScheduler {
5
5
  async scheduleNotification(options: ScheduleNotificationOptions): Promise<string> {
@@ -69,7 +69,7 @@ export class NotificationScheduler {
69
69
  content: {
70
70
  title: notification.content.title ?? '',
71
71
  body: notification.content.body ?? '',
72
- data: notification.content.data as Record<string, unknown>,
72
+ data: notification.content.data as Record<string, string | number | boolean | null>,
73
73
  },
74
74
  trigger: notification.trigger,
75
75
  }));
@@ -8,7 +8,6 @@ import { View, TouchableOpacity, StyleSheet } from 'react-native';
8
8
  import { AtomicText, AtomicIcon, AtomicCard } from '@umituz/react-native-design-system';
9
9
  import { Switch } from 'react-native';
10
10
  import { useAppDesignTokens } from '@umituz/react-native-design-system';
11
- import { SettingRow } from '../../../presentation/components/SettingRow';
12
11
  import type { QuietHoursConfig, QuietHoursTranslations } from '../../../infrastructure/services/types';
13
12
 
14
13
  export interface QuietHoursCardProps {
@@ -13,7 +13,7 @@ import type { Reminder, CreateReminderInput, UpdateReminderInput } from '../../.
13
13
  const scheduler = new NotificationScheduler();
14
14
 
15
15
  export const useReminderActions = () => {
16
- const { addReminder, updateReminder, deleteReminder, toggleReminder } = useRemindersStore();
16
+ const { addReminder, updateReminder, deleteReminder, toggleReminder: _toggleReminder } = useRemindersStore();
17
17
 
18
18
  const createReminder = useCallback(async (input: CreateReminderInput): Promise<Reminder> => {
19
19
  const now = new Date().toISOString();
@@ -34,7 +34,9 @@ export const useReminderActions = () => {
34
34
  data: { reminderId: reminder.id },
35
35
  });
36
36
  reminder.notificationId = notificationId;
37
- } catch {
37
+ } catch (error) {
38
+ // Log error for debugging
39
+ console.error('[useReminderActions] Failed to schedule notification:', error);
38
40
  reminder.enabled = false;
39
41
  }
40
42
 
@@ -44,25 +46,39 @@ export const useReminderActions = () => {
44
46
  }, [addReminder]);
45
47
 
46
48
  const editReminder = useCallback(async (id: string, input: UpdateReminderInput): Promise<void> => {
47
- const { reminders } = useRemindersStore.getState();
48
- const existing = reminders.find(r => r.id === id);
49
- if (!existing) return;
49
+ // Get current state BEFORE async operations to prevent race condition
50
+ const existing = useRemindersStore.getState().reminders.find(r => r.id === id);
51
+
52
+ if (!existing) {
53
+ throw new Error(`Reminder with id ${id} not found`);
54
+ }
50
55
 
51
56
  if (existing.notificationId) {
52
- await scheduler.cancelNotification(existing.notificationId);
57
+ try {
58
+ await scheduler.cancelNotification(existing.notificationId);
59
+ } catch (error) {
60
+ console.error('[useReminderActions] Failed to cancel notification:', error);
61
+ // Continue with update even if cancellation fails
62
+ }
53
63
  }
54
64
 
55
65
  const updated: Reminder = { ...existing, ...input, updatedAt: new Date().toISOString() };
56
66
 
57
67
  if (updated.enabled) {
58
- const trigger = buildTrigger(updated);
59
- const notificationId = await scheduler.scheduleNotification({
60
- title: updated.title,
61
- body: updated.body,
62
- trigger,
63
- data: { reminderId: updated.id },
64
- });
65
- updated.notificationId = notificationId;
68
+ try {
69
+ const trigger = buildTrigger(updated);
70
+ const notificationId = await scheduler.scheduleNotification({
71
+ title: updated.title,
72
+ body: updated.body,
73
+ trigger,
74
+ data: { reminderId: updated.id },
75
+ });
76
+ updated.notificationId = notificationId;
77
+ } catch (error) {
78
+ console.error('[useReminderActions] Failed to schedule notification:', error);
79
+ updated.enabled = false;
80
+ updated.notificationId = undefined;
81
+ }
66
82
  } else {
67
83
  updated.notificationId = undefined;
68
84
  }
@@ -71,24 +87,41 @@ export const useReminderActions = () => {
71
87
  }, [updateReminder]);
72
88
 
73
89
  const removeReminder = useCallback(async (id: string): Promise<void> => {
74
- const { reminders } = useRemindersStore.getState();
75
- const reminder = reminders.find(r => r.id === id);
90
+ // Get current state BEFORE async operations to prevent race condition
91
+ const reminder = useRemindersStore.getState().reminders.find(r => r.id === id);
76
92
 
77
- if (reminder?.notificationId) {
78
- await scheduler.cancelNotification(reminder.notificationId);
93
+ if (!reminder) {
94
+ throw new Error(`Reminder with id ${id} not found`);
95
+ }
96
+
97
+ if (reminder.notificationId) {
98
+ try {
99
+ await scheduler.cancelNotification(reminder.notificationId);
100
+ } catch (error) {
101
+ console.error('[useReminderActions] Failed to cancel notification:', error);
102
+ // Continue with deletion even if cancellation fails
103
+ }
79
104
  }
80
105
 
81
106
  await deleteReminder(id);
82
107
  }, [deleteReminder]);
83
108
 
84
109
  const toggleReminderEnabled = useCallback(async (id: string): Promise<void> => {
85
- const { reminders } = useRemindersStore.getState();
86
- const reminder = reminders.find(r => r.id === id);
87
- if (!reminder) return;
110
+ // Get current state BEFORE async operations to prevent race condition
111
+ const reminder = useRemindersStore.getState().reminders.find(r => r.id === id);
112
+
113
+ if (!reminder) {
114
+ throw new Error(`Reminder with id ${id} not found`);
115
+ }
88
116
 
89
117
  if (reminder.enabled && reminder.notificationId) {
90
- await scheduler.cancelNotification(reminder.notificationId);
91
- await updateReminder(id, { enabled: false, notificationId: undefined });
118
+ try {
119
+ await scheduler.cancelNotification(reminder.notificationId);
120
+ await updateReminder(id, { enabled: false, notificationId: undefined });
121
+ } catch (error) {
122
+ console.error('[useReminderActions] Failed to disable reminder:', error);
123
+ throw error; // Re-throw to allow caller to handle
124
+ }
92
125
  } else if (!reminder.enabled) {
93
126
  try {
94
127
  const trigger = buildTrigger(reminder);
@@ -99,8 +132,10 @@ export const useReminderActions = () => {
99
132
  data: { reminderId: reminder.id },
100
133
  });
101
134
  await updateReminder(id, { enabled: true, notificationId });
102
- } catch {
103
- await updateReminder(id, { enabled: false });
135
+ } catch (error) {
136
+ console.error('[useReminderActions] Failed to enable reminder:', error);
137
+ await updateReminder(id, { enabled: false }); // Ensure disabled state
138
+ throw error; // Re-throw to allow caller to handle
104
139
  }
105
140
  }
106
141
  }, [updateReminder]);
@@ -3,12 +3,12 @@
3
3
  * Displays list of reminders with add, edit, delete functionality
4
4
  */
5
5
 
6
- import React, { useEffect, useMemo, useCallback } from 'react';
6
+ import React, { useMemo, useCallback } from 'react';
7
7
  import { View, FlatList, StyleSheet, TouchableOpacity } from 'react-native';
8
8
  import { AtomicText, AtomicIcon, AtomicSpinner } from '@umituz/react-native-design-system';
9
9
  import { useAppDesignTokens } from '@umituz/react-native-design-system';
10
10
  import { ReminderItem } from '../components/ReminderItem';
11
- import { useRemindersStore, useReminders, useRemindersLoading } from '../../infrastructure/storage/RemindersStore';
11
+ import { useReminders, useRemindersLoading } from '../../infrastructure/storage/RemindersStore';
12
12
  import { useReminderActions } from '../../infrastructure/hooks/useReminderActions';
13
13
  import type { Reminder, ReminderTranslations } from '../../../infrastructure/services/types';
14
14
 
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import React from "react";
7
- import { Modal, View, StyleSheet, type StyleProp, type ViewStyle } from "react-native";
7
+ import { Modal, View, StyleSheet } from "react-native";
8
8
  import {
9
9
  AtomicText,
10
10
  AtomicButton,
@@ -61,7 +61,7 @@ export const StarRating: React.FC<StarRatingProps> = ({
61
61
  );
62
62
  };
63
63
 
64
- const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
64
+ const getStyles = (_tokens: ReturnType<typeof useAppDesignTokens>) =>
65
65
  StyleSheet.create({
66
66
  container: {
67
67
  flexDirection: "row",
@@ -22,33 +22,29 @@ interface State {
22
22
  }
23
23
 
24
24
  export class SettingsErrorBoundary extends Component<Props, State> {
25
- constructor(props: Props) {
26
- super(props);
27
- // Initialize state using unknown to avoid type assertion issues
28
- (this as unknown as { state: State }).state = { hasError: false };
29
- }
25
+ override state: State = {
26
+ hasError: false,
27
+ error: undefined,
28
+ };
30
29
 
31
30
  static getDerivedStateFromError(error: Error): State {
32
31
  return { hasError: true, error };
33
32
  }
34
33
 
35
- componentDidCatch(_error: Error, _errorInfo: React.ErrorInfo): void {
34
+ override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
36
35
  // Log error to console in development
37
36
  if (__DEV__) {
38
- console.error('Settings Error Boundary caught an error:', _error);
39
- console.error('Error Info:', _errorInfo);
37
+ console.error('Settings Error Boundary caught an error:', error);
38
+ console.error('Error Info:', errorInfo);
40
39
  }
40
+
41
+ // TODO: Send to error tracking service in production
42
+ // Example: Sentry.captureException(error, { contexts: { react: { errorInfo } } });
41
43
  }
42
44
 
43
- render(): ReactNode {
44
- // Safe access through unknown to avoid type assertion issues
45
- const self = this as unknown as { state: State; props: Props };
46
- const hasError = self.state.hasError;
47
- const error = self.state.error;
48
- const fallback = self.props.fallback;
49
- const fallbackTitle = self.props.fallbackTitle;
50
- const fallbackMessage = self.props.fallbackMessage;
51
- const children = self.props.children;
45
+ override render(): ReactNode {
46
+ const { hasError, error } = this.state;
47
+ const { children, fallback, fallbackTitle, fallbackMessage } = this.props;
52
48
 
53
49
  if (hasError) {
54
50
  if (fallback) {
@@ -140,4 +136,4 @@ const styles = StyleSheet.create({
140
136
  textAlign: 'center',
141
137
  lineHeight: 20,
142
138
  },
143
- });
139
+ });
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { View, StyleSheet } from "react-native";
3
- import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
3
+ import { AtomicText } from "@umituz/react-native-design-system";
4
4
 
5
5
  export interface SettingsFooterProps {
6
6
  versionText?: string;
@@ -13,8 +13,6 @@ export const SettingsFooter: React.FC<SettingsFooterProps> = ({
13
13
  appVersion,
14
14
  versionLabel,
15
15
  }) => {
16
- const tokens = useAppDesignTokens();
17
-
18
16
  const displayText = versionText || (appVersion && versionLabel
19
17
  ? `${versionLabel} ${appVersion}`
20
18
  : appVersion);
@@ -51,6 +51,23 @@ export const SettingsItemCard: React.FC<SettingsItemCardProps> = ({
51
51
  const tokens = useAppDesignTokens();
52
52
  const colors = tokens.colors;
53
53
 
54
+ // Validate props in development
55
+ if (__DEV__) {
56
+ if (!title || typeof title !== 'string') {
57
+ console.warn('[SettingsItemCard] Invalid title prop:', title);
58
+ }
59
+ if (description && typeof description !== 'string') {
60
+ console.warn('[SettingsItemCard] Invalid description prop:', description);
61
+ }
62
+ if (showSwitch && !onSwitchChange) {
63
+ console.warn('[SettingsItemCard] Switch shown but no onSwitchChange provided. Switch changes will be ignored.');
64
+ }
65
+ }
66
+
67
+ // Sanitize string props (trim and limit length)
68
+ const sanitizedTitle = title?.trim().slice(0, 200) || '';
69
+ const sanitizedDescription = description?.trim().slice(0, 500);
70
+
54
71
  const defaultIconBg = iconBgColor || withAlpha(colors.primary, 0.15);
55
72
  const defaultIconColor = iconColor || colors.primary;
56
73
  const isClickable = !!onPress && !showSwitch;
@@ -61,7 +78,11 @@ export const SettingsItemCard: React.FC<SettingsItemCardProps> = ({
61
78
  return (
62
79
  <AtomicSwitch
63
80
  value={!!switchValue}
64
- onValueChange={onSwitchChange || (() => {})}
81
+ onValueChange={onSwitchChange || (() => {
82
+ if (__DEV__) {
83
+ console.warn('[SettingsItemCard] Switch toggled but no onSwitchChange handler provided');
84
+ }
85
+ })}
65
86
  disabled={disabled}
66
87
  />
67
88
  );
@@ -82,13 +103,13 @@ export const SettingsItemCard: React.FC<SettingsItemCardProps> = ({
82
103
  type="bodyLarge"
83
104
  color={disabled ? "onSurfaceVariant" : "onSurface"}
84
105
  numberOfLines={1}
85
- style={{ marginBottom: description ? tokens.spacing.xs : 0, opacity: disabled ? 0.6 : 1 }}
106
+ style={{ marginBottom: sanitizedDescription ? tokens.spacing.xs : 0, opacity: disabled ? 0.6 : 1 }}
86
107
  >
87
- {title}
108
+ {sanitizedTitle}
88
109
  </AtomicText>
89
- {!!description && (
110
+ {!!sanitizedDescription && (
90
111
  <AtomicText type="bodyMedium" color="textSecondary" numberOfLines={2}>
91
- {description}
112
+ {sanitizedDescription}
92
113
  </AtomicText>
93
114
  )}
94
115
  </View>
@@ -113,7 +134,7 @@ export const SettingsItemCard: React.FC<SettingsItemCardProps> = ({
113
134
  {!!sectionTitle && (
114
135
  <View style={styles.headerContainer}>
115
136
  <AtomicText type="labelMedium" color="textSecondary" style={{ textTransform: 'uppercase' }}>
116
- {sectionTitle}
137
+ {sectionTitle.trim().slice(0, 100)}
117
138
  </AtomicText>
118
139
  </View>
119
140
  )}
@@ -6,7 +6,6 @@
6
6
 
7
7
  import { useQuery } from '@umituz/react-native-design-system';
8
8
  import { getSettingsService } from '../../../infrastructure/services/SettingsService';
9
- import type { UserSettings } from '../../../application/ports/ISettingsRepository';
10
9
 
11
10
  export const SETTINGS_QUERY_KEY = ['settings'];
12
11
 
@@ -10,6 +10,14 @@ import { useUpdateSettingsMutation, useResetSettingsMutation } from './mutations
10
10
  import type { UserSettings } from '../../application/ports/ISettingsRepository';
11
11
 
12
12
  export const useSettings = (userId: string) => {
13
+ // Validate userId
14
+ if (!userId || typeof userId !== 'string' || userId.trim() === '') {
15
+ throw new Error(
16
+ 'Invalid userId: must be a non-empty string. ' +
17
+ 'Received: ' + (userId === null ? 'null' : typeof userId)
18
+ );
19
+ }
20
+
13
21
  const {
14
22
  data: settings,
15
23
  isLoading: loading,