@umituz/react-native-settings 4.23.65 → 4.23.66

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 (25) hide show
  1. package/package.json +1 -1
  2. package/src/domains/about/presentation/hooks/useAboutInfo.ts +16 -5
  3. package/src/domains/appearance/hooks/useAppearanceActions.ts +25 -9
  4. package/src/domains/appearance/presentation/components/AppearancePreview.tsx +3 -3
  5. package/src/domains/appearance/presentation/components/ColorPicker.tsx +10 -4
  6. package/src/domains/appearance/presentation/components/ThemeModeSection.tsx +2 -2
  7. package/src/domains/appearance/presentation/components/ThemeOption.tsx +41 -8
  8. package/src/domains/gamification/components/GamificationScreen/types.ts +1 -0
  9. package/src/domains/gamification/store/gamificationStore.ts +7 -1
  10. package/src/domains/legal/presentation/components/LegalDocumentsList.tsx +6 -6
  11. package/src/domains/localization/presentation/providers/LocalizationProvider.tsx +18 -3
  12. package/src/domains/localization/presentation/screens/LanguageSelectionScreen.types.ts +1 -0
  13. package/src/domains/notifications/infrastructure/services/types.ts +7 -4
  14. package/src/domains/notifications/quietHours/infrastructure/hooks/useQuietHoursActions.ts +12 -1
  15. package/src/domains/notifications/quietHours/presentation/components/QuietHoursCard.tsx +19 -2
  16. package/src/domains/notifications/reminders/presentation/components/ReminderForm.tsx +67 -7
  17. package/src/infrastructure/repositories/SettingsRepository.ts +26 -0
  18. package/src/presentation/components/SettingsErrorBoundary.tsx +25 -13
  19. package/src/presentation/components/SettingsSection.tsx +4 -2
  20. package/src/presentation/hooks/mutations/useSettingsMutations.ts +14 -5
  21. package/src/presentation/hooks/useSettingsScreenConfig.ts +29 -18
  22. package/src/presentation/navigation/SettingsStackNavigator.tsx +65 -41
  23. package/src/presentation/navigation/types.ts +1 -0
  24. package/src/presentation/screens/SettingsScreen.tsx +40 -9
  25. package/src/presentation/screens/components/sections/CustomSettingsList.tsx +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-settings",
3
- "version": "4.23.65",
3
+ "version": "4.23.66",
4
4
  "description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -99,7 +99,12 @@ export const useAboutInfo = (
99
99
  isMountedRef.current = false;
100
100
 
101
101
  if (repository && typeof repository.destroy === 'function') {
102
- repository.destroy();
102
+ try {
103
+ repository.destroy();
104
+ } catch (error) {
105
+ // Log cleanup error but don't throw
106
+ console.warn('Error during repository cleanup:', error);
107
+ }
103
108
  }
104
109
  };
105
110
  }, [repository]);
@@ -112,10 +117,16 @@ export const useAboutInfo = (
112
117
  isMountedRef.current &&
113
118
  !isInitializedRef.current
114
119
  ) {
115
- const { createDefaultAppInfo } = require('../../utils/AppInfoFactory');
116
- const defaultAppInfo = createDefaultAppInfo(initialConfig);
117
- setAppInfo(defaultAppInfo);
118
- isInitializedRef.current = true;
120
+ // Dynamic import to avoid require issues
121
+ import('../../utils/AppInfoFactory').then(({ createDefaultAppInfo }) => {
122
+ if (isMountedRef.current) {
123
+ const defaultAppInfo = createDefaultAppInfo(initialConfig);
124
+ setAppInfo(defaultAppInfo);
125
+ isInitializedRef.current = true;
126
+ }
127
+ }).catch((error) => {
128
+ console.error('Failed to load AppInfoFactory:', error);
129
+ });
119
130
  }
120
131
  }, [initialConfig, autoInit]);
121
132
 
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useEffect } from "react";
1
+ import { useState, useCallback, useEffect, useRef } from "react";
2
2
  import { useAppearance } from "./useAppearance";
3
3
  import type { CustomThemeColors, ThemeMode } from "@umituz/react-native-design-system";
4
4
 
@@ -6,8 +6,19 @@ export const useAppearanceActions = () => {
6
6
  const { themeMode, customColors, setThemeMode, setCustomColors, reset } = useAppearance();
7
7
  const [localCustomColors, setLocalCustomColors] = useState<CustomThemeColors>(customColors || {});
8
8
 
9
+ // Track if component is mounted to prevent state updates after unmount
10
+ const isMountedRef = useRef(true);
11
+
12
+ useEffect(() => {
13
+ isMountedRef.current = true;
14
+ return () => {
15
+ isMountedRef.current = false;
16
+ };
17
+ }, []);
18
+
19
+ // Sync local state with global state, but only if mounted
9
20
  useEffect(() => {
10
- if (customColors) {
21
+ if (isMountedRef.current && customColors) {
11
22
  setLocalCustomColors(customColors);
12
23
  }
13
24
  }, [customColors]);
@@ -17,16 +28,21 @@ export const useAppearanceActions = () => {
17
28
  }, [setThemeMode]);
18
29
 
19
30
  const handleColorChange = useCallback((key: keyof CustomThemeColors, color: string) => {
20
- setLocalCustomColors(prev => {
21
- const newColors = { ...prev, [key]: color };
22
- setCustomColors(newColors);
23
- return newColors;
24
- });
25
- }, [setCustomColors]);
31
+ // Update local state immediately for UI responsiveness
32
+ const newColors = { ...localCustomColors, [key]: color };
33
+
34
+ // Batch state updates to prevent race conditions
35
+ setLocalCustomColors(newColors);
36
+
37
+ // Update global state separately
38
+ setCustomColors(newColors);
39
+ }, [localCustomColors, setCustomColors]);
26
40
 
27
41
  const handleResetColors = useCallback(() => {
28
42
  reset();
29
- setLocalCustomColors({});
43
+ if (isMountedRef.current) {
44
+ setLocalCustomColors({});
45
+ }
30
46
  }, [reset]);
31
47
 
32
48
  return {
@@ -59,7 +59,7 @@ export const AppearancePreview: React.FC<AppearancePreviewProps> = ({
59
59
 
60
60
  const colors = previewColors.map((item) => ({
61
61
  label: item.label,
62
- color: localCustomColors[item.key] || item.fallbackColor,
62
+ color: localCustomColors[item.key] ?? item.fallbackColor,
63
63
  }));
64
64
 
65
65
  if (!showPreview) {
@@ -84,8 +84,8 @@ export const AppearancePreview: React.FC<AppearancePreviewProps> = ({
84
84
  </AtomicText>
85
85
  <View style={styles.previewContainer}>
86
86
  <View style={styles.previewColorRow}>
87
- {colors.map((item, index) => (
88
- <View key={index} style={styles.previewColorItem}>
87
+ {colors.map((item) => (
88
+ <View key={`preview-color-${item.label}`} style={styles.previewColorItem}>
89
89
  <View
90
90
  style={[
91
91
  styles.previewColorCircle,
@@ -40,14 +40,16 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
40
40
  // Prevent unnecessary updates if color hasn't changed
41
41
  if (value === color) return;
42
42
  onValueChange(color);
43
- } catch {
44
- // Silent error handling
43
+ } catch (error) {
44
+ // Log error for debugging while preventing UI crashes
45
+ console.error('Failed to change color:', error);
46
+ // Optionally: Show user feedback about the error
45
47
  }
46
48
  }, [value, onValueChange]);
47
49
 
48
50
  // Memoize color options to prevent unnecessary re-renders
49
51
  const colorOptions = useMemo(() => {
50
- return colorsMemo.map((color) => {
52
+ return colorsMemo.map((color, index) => {
51
53
  const isSelected = value === color;
52
54
 
53
55
  return (
@@ -60,6 +62,10 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
60
62
  ]}
61
63
  onPress={() => handleColorChange(color)}
62
64
  activeOpacity={0.8} // Performance optimization
65
+ accessibilityLabel={`Color ${color} ${isSelected ? 'selected' : 'not selected'}`}
66
+ accessibilityRole={isSelected ? 'button' : 'button'}
67
+ accessibilityState={{ selected: isSelected }}
68
+ accessibilityHint={`Select ${color} color`}
63
69
  >
64
70
  {isSelected && (
65
71
  <AtomicIcon name="checkmark" size="sm" customColor={tokens.colors.textInverse} />
@@ -67,7 +73,7 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
67
73
  </TouchableOpacity>
68
74
  );
69
75
  });
70
- }, [colorsMemo, value, handleColorChange, styles]);
76
+ }, [colorsMemo, value, handleColorChange, styles, tokens.colors.textInverse]);
71
77
 
72
78
  return (
73
79
  <View style={styles.container}>
@@ -43,8 +43,8 @@ export const ThemeModeSection: React.FC<ThemeModeSectionProps> = ({
43
43
  const handleThemeSelect = useCallback((mode: ThemeMode) => {
44
44
  try {
45
45
  onThemeSelect(mode);
46
- } catch {
47
- // Silent error handling
46
+ } catch (error) {
47
+ console.error('Failed to select theme:', error);
48
48
  }
49
49
  }, [onThemeSelect]);
50
50
 
@@ -5,7 +5,7 @@
5
5
  * Single Responsibility: Render theme option UI
6
6
  */
7
7
 
8
- import React from "react";
8
+ import React, { useMemo } from "react";
9
9
  import { View, TouchableOpacity, StyleSheet } from "react-native";
10
10
  import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system";
11
11
  import { useAppDesignTokens } from "@umituz/react-native-design-system";
@@ -22,7 +22,24 @@ interface ThemeOptionProps {
22
22
  onSelect: () => void;
23
23
  }
24
24
 
25
- export const ThemeOption: React.FC<ThemeOptionProps> = ({
25
+ // Valid theme modes for validation
26
+ const VALID_THEME_MODES: readonly ThemeMode[] = ["light", "dark", "auto"];
27
+
28
+ // Icon names mapping for type safety
29
+ const THEME_ICONS: Record<ThemeMode, string> = {
30
+ light: "sunny-outline",
31
+ dark: "moon-outline",
32
+ auto: "desktop-outline",
33
+ };
34
+
35
+ // Utility function to add opacity to hex color
36
+ const addOpacityToHex = (hexColor: string, opacity: string): string => {
37
+ // Remove # if present
38
+ const color = hexColor.replace("#", "");
39
+ return `#${color}${opacity}`;
40
+ };
41
+
42
+ export const ThemeOption: React.FC<ThemeOptionProps> = React.memo(({
26
43
  mode,
27
44
  title,
28
45
  subtitle,
@@ -33,13 +50,27 @@ export const ThemeOption: React.FC<ThemeOptionProps> = ({
33
50
  onSelect,
34
51
  }) => {
35
52
  const tokens = useAppDesignTokens();
36
- const styles = getStyles(tokens);
37
- const iconName = mode === "dark" ? "moon-outline" : mode === "light" ? "sunny-outline" : "desktop-outline";
53
+
54
+ // Memoize styles to prevent unnecessary re-creation
55
+ const styles = useMemo(() => getStyles(tokens), [tokens]);
56
+
57
+ // Type-safe icon name selection with validation
58
+ const iconName: string = useMemo(() => {
59
+ if (VALID_THEME_MODES.includes(mode)) {
60
+ return THEME_ICONS[mode];
61
+ }
62
+ // Fallback for invalid mode
63
+ console.warn(`Invalid theme mode: ${mode}. Defaulting to auto.`);
64
+ return THEME_ICONS.auto;
65
+ }, [mode]);
38
66
 
39
67
  return (
40
68
  <TouchableOpacity
41
69
  style={[styles.container, isSelected && styles.selectedContainer]}
42
70
  onPress={onSelect}
71
+ accessibilityLabel={`${title} theme ${isSelected ? 'selected' : 'not selected'}`}
72
+ accessibilityRole="button"
73
+ accessibilityState={{ selected: isSelected }}
43
74
  >
44
75
  <View style={styles.header}>
45
76
  <View style={styles.iconContainer}>
@@ -85,7 +116,7 @@ export const ThemeOption: React.FC<ThemeOptionProps> = ({
85
116
  ) : null}
86
117
  {features.map((feature, index) => (
87
118
  <AtomicText
88
- key={index}
119
+ key={`feature-${index}-${feature.slice(0, 10)}`}
89
120
  type="bodySmall"
90
121
  color="secondary"
91
122
  style={styles.feature}
@@ -97,7 +128,9 @@ export const ThemeOption: React.FC<ThemeOptionProps> = ({
97
128
  ) : null}
98
129
  </TouchableOpacity>
99
130
  );
100
- };
131
+ });
132
+
133
+ ThemeOption.displayName = "ThemeOption";
101
134
 
102
135
  const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
103
136
  StyleSheet.create({
@@ -121,7 +154,7 @@ const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
121
154
  width: 48,
122
155
  height: 48,
123
156
  borderRadius: 24,
124
- backgroundColor: `${tokens.colors.primary}15`,
157
+ backgroundColor: addOpacityToHex(tokens.colors.primary, "15"),
125
158
  justifyContent: "center",
126
159
  alignItems: "center",
127
160
  marginRight: 12,
@@ -134,7 +167,7 @@ const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
134
167
  lineHeight: 20,
135
168
  },
136
169
  featuresContainer: {
137
- backgroundColor: `${tokens.colors.primary}08`,
170
+ backgroundColor: addOpacityToHex(tokens.colors.primary, "08"),
138
171
  borderRadius: 8,
139
172
  padding: 12,
140
173
  },
@@ -3,6 +3,7 @@
3
3
  * Generic types for 100+ apps - NO app-specific code
4
4
  */
5
5
 
6
+ import type React from 'react';
6
7
  import type { LevelProgressProps } from "../LevelProgress";
7
8
  import type { StatsCardProps } from "../StatsCard";
8
9
  import type { AchievementItemProps } from "../AchievementItem";
@@ -116,10 +116,16 @@ export const useGamificationStore = createStore<GamificationState, GamificationA
116
116
  });
117
117
  },
118
118
 
119
- checkAchievements: () => {
119
+ checkAchievements: (): Achievement[] => {
120
120
  if (!currentConfig) return [];
121
121
 
122
122
  const state = get();
123
+
124
+ // Safety check for achievements array
125
+ if (!state.achievements || state.achievements.length === 0) {
126
+ return [];
127
+ }
128
+
123
129
  const newlyUnlocked: Achievement[] = [];
124
130
 
125
131
  const updatedAchievements = state.achievements.map((ach: Achievement) => {
@@ -70,30 +70,30 @@ export const LegalDocumentsList: React.FC<LegalDocumentsListProps> = React.memo(
70
70
  </AtomicText>
71
71
  )}
72
72
 
73
- {showPrivacy && (
73
+ {showPrivacy && privacyTitle && (
74
74
  <LegalItem
75
75
  iconName="shield"
76
- title={privacyTitle!}
76
+ title={privacyTitle}
77
77
  description={privacyDescription}
78
78
  onPress={onPrivacyPress}
79
79
  testID="privacy-policy-item"
80
80
  />
81
81
  )}
82
82
 
83
- {showTerms && (
83
+ {showTerms && termsTitle && (
84
84
  <LegalItem
85
85
  iconName="document-text"
86
- title={termsTitle!}
86
+ title={termsTitle}
87
87
  description={termsDescription}
88
88
  onPress={onTermsPress}
89
89
  testID="terms-of-service-item"
90
90
  />
91
91
  )}
92
92
 
93
- {showEula && (
93
+ {showEula && eulaTitle && (
94
94
  <LegalItem
95
95
  iconName="document"
96
- title={eulaTitle!}
96
+ title={eulaTitle}
97
97
  description={eulaDescription}
98
98
  onPress={handleEulaPress}
99
99
  testID="eula-item"
@@ -22,13 +22,28 @@ export const LocalizationProvider: React.FC<LocalizationProviderProps> = ({
22
22
  const initialize = useLocalizationStore((state) => state.initialize);
23
23
 
24
24
  useEffect(() => {
25
+ let isMounted = true;
26
+
25
27
  const initializeLocalization = async () => {
26
- I18nInitializer.initialize(translations, defaultLanguage);
27
- await initialize();
28
- setIsI18nReady(true);
28
+ try {
29
+ I18nInitializer.initialize(translations, defaultLanguage);
30
+ await initialize();
31
+ if (isMounted) {
32
+ setIsI18nReady(true);
33
+ }
34
+ } catch (error) {
35
+ console.error('Failed to initialize localization:', error);
36
+ if (isMounted) {
37
+ setIsI18nReady(true); // Set ready even on error to prevent indefinite loading
38
+ }
39
+ }
29
40
  };
30
41
 
31
42
  initializeLocalization();
43
+
44
+ return () => {
45
+ isMounted = false;
46
+ };
32
47
  }, [translations, defaultLanguage, initialize]);
33
48
 
34
49
  if (!isI18nReady) {
@@ -2,6 +2,7 @@
2
2
  * Language Selection Screen Types
3
3
  */
4
4
 
5
+ import type React from 'react';
5
6
  import type { Language } from '../../infrastructure/storage/types/Language';
6
7
 
7
8
  export interface LanguageSelectionScreenProps {
@@ -29,10 +29,13 @@ export type NotificationTrigger =
29
29
  | { type: 'weekly'; weekday: number; hour: number; minute: number }
30
30
  | { type: 'monthly'; day: number; hour: number; minute: number };
31
31
 
32
+ // More specific data type for notifications to prevent unsafe data
33
+ export type NotificationData = Record<string, string | number | boolean | null>;
34
+
32
35
  export interface ScheduleNotificationOptions {
33
36
  title: string;
34
37
  body: string;
35
- data?: Record<string, unknown>;
38
+ data?: NotificationData;
36
39
  trigger: NotificationTrigger;
37
40
  sound?: boolean | string;
38
41
  badge?: number;
@@ -44,7 +47,7 @@ export interface ScheduledNotification {
44
47
  content: {
45
48
  title: string;
46
49
  body: string;
47
- data: Record<string, unknown>;
50
+ data: NotificationData;
48
51
  };
49
52
  trigger: Notifications.NotificationTrigger;
50
53
  }
@@ -79,8 +82,8 @@ export interface Reminder {
79
82
  dayOfMonth?: number;
80
83
  enabled: boolean;
81
84
  notificationId?: string;
82
- createdAt: string;
83
- updatedAt: string;
85
+ createdAt: string; // ISO 8601 date string
86
+ updatedAt: string; // ISO 8601 date string
84
87
  }
85
88
 
86
89
  export interface CreateReminderInput {
@@ -35,10 +35,21 @@ export const useQuietHoursActions = () => {
35
35
  const startMinutes = quietHours.startHour * 60 + quietHours.startMinute;
36
36
  const endMinutes = quietHours.endHour * 60 + quietHours.endMinute;
37
37
 
38
+ // Validate time values
39
+ if (startMinutes < 0 || startMinutes >= 1440 || endMinutes < 0 || endMinutes >= 1440) {
40
+ console.error('Invalid quiet hours time values');
41
+ return false;
42
+ }
43
+
44
+ // Handle case where quiet hours span midnight
38
45
  if (startMinutes <= endMinutes) {
46
+ // Same day period (e.g., 22:00 - 06:00 is not this case)
39
47
  return currentMinutes >= startMinutes && currentMinutes < endMinutes;
48
+ } else {
49
+ // Spans midnight (e.g., 22:00 - 06:00)
50
+ // Current time is after start OR before end
51
+ return currentMinutes >= startMinutes || currentMinutes < endMinutes;
40
52
  }
41
- return currentMinutes >= startMinutes || currentMinutes < endMinutes;
42
53
  }, [quietHours]);
43
54
 
44
55
  return {
@@ -48,12 +48,22 @@ export const QuietHoursCard: React.FC<QuietHoursCardProps> = ({
48
48
  onValueChange={onToggle}
49
49
  trackColor={{ false: tokens.colors.surfaceSecondary, true: tokens.colors.primary }}
50
50
  thumbColor={tokens.colors.surface}
51
+ accessibilityLabel={translations.title}
52
+ accessibilityHint={translations.description}
53
+ accessibilityRole="switch"
51
54
  />
52
55
  </View>
53
56
 
54
57
  {config.enabled && (
55
58
  <View style={styles.timeContainer}>
56
- <TouchableOpacity style={styles.timeButton} onPress={onStartTimePress} activeOpacity={0.7}>
59
+ <TouchableOpacity
60
+ style={styles.timeButton}
61
+ onPress={onStartTimePress}
62
+ activeOpacity={0.7}
63
+ accessibilityLabel={`${translations.startTimeLabel}: ${formatTime(config.startHour, config.startMinute)}`}
64
+ accessibilityRole="button"
65
+ accessibilityHint="Press to change start time"
66
+ >
57
67
  <AtomicText type="bodySmall" style={styles.timeLabel}>{translations.startTimeLabel}</AtomicText>
58
68
  <AtomicText type="bodyLarge" style={styles.timeValue}>
59
69
  {formatTime(config.startHour, config.startMinute)}
@@ -64,7 +74,14 @@ export const QuietHoursCard: React.FC<QuietHoursCardProps> = ({
64
74
  <AtomicIcon name="arrow-forward" size="sm" color="secondary" />
65
75
  </View>
66
76
 
67
- <TouchableOpacity style={styles.timeButton} onPress={onEndTimePress} activeOpacity={0.7}>
77
+ <TouchableOpacity
78
+ style={styles.timeButton}
79
+ onPress={onEndTimePress}
80
+ activeOpacity={0.7}
81
+ accessibilityLabel={`${translations.endTimeLabel}: ${formatTime(config.endHour, config.endMinute)}`}
82
+ accessibilityRole="button"
83
+ accessibilityHint="Press to change end time"
84
+ >
68
85
  <AtomicText type="bodySmall" style={styles.timeLabel}>{translations.endTimeLabel}</AtomicText>
69
86
  <AtomicText type="bodyLarge" style={styles.timeValue}>
70
87
  {formatTime(config.endHour, config.endMinute)}
@@ -14,6 +14,18 @@ import { FormButton } from './FormButton';
14
14
  import { DEFAULT_TIME_PRESETS, FREQUENCY_OPTIONS } from '../../infrastructure/config/reminderPresets';
15
15
  import type { Reminder, ReminderFrequency, CreateReminderInput, TimePreset } from '../../../infrastructure/services/types';
16
16
 
17
+ // Constants for magic numbers
18
+ const DEFAULT_HOUR = 9;
19
+ const DEFAULT_MINUTE = 0;
20
+ const DEFAULT_WEEKDAY = 2; // Tuesday
21
+ const MAX_TITLE_LENGTH = 100;
22
+ const MAX_BODY_LENGTH = 500;
23
+
24
+ // Validation constants
25
+ const VALID_HOUR_RANGE = { min: 0, max: 23 } as const;
26
+ const VALID_MINUTE_RANGE = { min: 0, max: 59 } as const;
27
+ const VALID_WEEKDAY_RANGE = { min: 0, max: 6 } as const;
28
+
17
29
  export interface ReminderFormTranslations {
18
30
  titleLabel: string;
19
31
  titlePlaceholder: string;
@@ -52,11 +64,24 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
52
64
  const [body, setBody] = useState(initialData?.body || '');
53
65
  const [frequency, setFrequency] = useState<ReminderFrequency>(initialData?.frequency || 'daily');
54
66
  const [selectedPresetId, setSelectedPresetId] = useState<string | undefined>(initialData?.timePresetId);
55
- const [hour, setHour] = useState(initialData?.hour ?? 9);
56
- const [minute, setMinute] = useState(initialData?.minute ?? 0);
57
- const [weekday, setWeekday] = useState(initialData?.weekday ?? 2);
67
+ const [hour, setHour] = useState(initialData?.hour ?? DEFAULT_HOUR);
68
+ const [minute, setMinute] = useState(initialData?.minute ?? DEFAULT_MINUTE);
69
+ const [weekday, setWeekday] = useState(initialData?.weekday ?? DEFAULT_WEEKDAY);
58
70
  const [isCustomTime, setIsCustomTime] = useState(!initialData?.timePresetId);
59
71
 
72
+ // Validation helper functions
73
+ const isValidHour = useCallback((h: number): boolean => {
74
+ return h >= VALID_HOUR_RANGE.min && h <= VALID_HOUR_RANGE.max;
75
+ }, []);
76
+
77
+ const isValidMinute = useCallback((m: number): boolean => {
78
+ return m >= VALID_MINUTE_RANGE.min && m <= VALID_MINUTE_RANGE.max;
79
+ }, []);
80
+
81
+ const isValidWeekday = useCallback((w: number): boolean => {
82
+ return w >= VALID_WEEKDAY_RANGE.min && w <= VALID_WEEKDAY_RANGE.max;
83
+ }, []);
84
+
60
85
  const handlePresetSelect = useCallback((preset: TimePreset) => {
61
86
  setSelectedPresetId(preset.id);
62
87
  setHour(preset.hour);
@@ -70,10 +95,45 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
70
95
  }, []);
71
96
 
72
97
  const handleSave = useCallback(() => {
73
- if (!title.trim()) return;
98
+ const trimmedTitle = title.trim();
99
+ const trimmedBody = body.trim();
100
+
101
+ // Validate title
102
+ if (!trimmedTitle) {
103
+ console.warn('Reminder title is required');
104
+ return;
105
+ }
106
+
107
+ if (trimmedTitle.length > MAX_TITLE_LENGTH) {
108
+ console.warn(`Reminder title exceeds maximum length of ${MAX_TITLE_LENGTH}`);
109
+ return;
110
+ }
111
+
112
+ // Validate body length
113
+ if (trimmedBody.length > MAX_BODY_LENGTH) {
114
+ console.warn(`Reminder body exceeds maximum length of ${MAX_BODY_LENGTH}`);
115
+ return;
116
+ }
117
+
118
+ // Validate time values
119
+ if (!isValidHour(hour) || !isValidMinute(minute)) {
120
+ console.warn('Invalid time values');
121
+ return;
122
+ }
123
+
124
+ // Validate weekday if frequency is weekly
125
+ if (frequency === 'weekly' && !isValidWeekday(weekday)) {
126
+ console.warn('Invalid weekday value');
127
+ return;
128
+ }
129
+
130
+ // Sanitize input (React Native handles XSS, but we trim extra whitespace)
131
+ const sanitizedTitle = trimmedTitle.replace(/\s+/g, ' ').trim();
132
+ const sanitizedBody = trimmedBody.replace(/\s+/g, ' ').trim();
133
+
74
134
  onSave({
75
- title: title.trim(),
76
- body: body.trim(),
135
+ title: sanitizedTitle,
136
+ body: sanitizedBody,
77
137
  frequency,
78
138
  timePresetId: isCustomTime ? undefined : selectedPresetId,
79
139
  hour,
@@ -81,7 +141,7 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
81
141
  weekday: frequency === 'weekly' ? weekday : undefined,
82
142
  dayOfMonth: frequency === 'monthly' ? 1 : undefined,
83
143
  });
84
- }, [title, body, frequency, selectedPresetId, hour, minute, weekday, isCustomTime, onSave]);
144
+ }, [title, body, frequency, selectedPresetId, hour, minute, weekday, isCustomTime, onSave, isValidHour, isValidMinute, isValidWeekday]);
85
145
 
86
146
  return (
87
147
  <ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
@@ -7,6 +7,23 @@
7
7
  import { storageRepository, StorageKey, createUserKey } from '@umituz/react-native-design-system';
8
8
  import type { ISettingsRepository, UserSettings, SettingsResult } from '../../application/ports/ISettingsRepository';
9
9
 
10
+ /**
11
+ * Validates userId to prevent key injection attacks
12
+ */
13
+ const validateUserId = (userId: string): void => {
14
+ if (!userId || typeof userId !== 'string') {
15
+ throw new Error('Invalid userId: must be a non-empty string');
16
+ }
17
+ // Prevent potential key injection by restricting characters
18
+ if (!/^[a-zA-Z0-9_-]+$/.test(userId)) {
19
+ throw new Error('Invalid userId: contains invalid characters');
20
+ }
21
+ // Limit length to prevent DoS attacks
22
+ if (userId.length > 128) {
23
+ throw new Error('Invalid userId: exceeds maximum length');
24
+ }
25
+ };
26
+
10
27
  export class SettingsRepository implements ISettingsRepository {
11
28
  private readonly defaultSettings: (userId: string) => UserSettings = (userId: string) => ({
12
29
  userId,
@@ -24,6 +41,9 @@ export class SettingsRepository implements ISettingsRepository {
24
41
 
25
42
  async getSettings(userId: string): Promise<SettingsResult<UserSettings>> {
26
43
  try {
44
+ // Validate input
45
+ validateUserId(userId);
46
+
27
47
  const storageKey = createUserKey(StorageKey.USER_PREFERENCES, userId);
28
48
  const defaults = this.defaultSettings(userId);
29
49
  const result = await storageRepository.getItem<UserSettings>(storageKey, defaults);
@@ -51,6 +71,9 @@ export class SettingsRepository implements ISettingsRepository {
51
71
 
52
72
  async saveSettings(settings: UserSettings): Promise<SettingsResult<void>> {
53
73
  try {
74
+ // Validate input
75
+ validateUserId(settings.userId);
76
+
54
77
  const storageKey = createUserKey(StorageKey.USER_PREFERENCES, settings.userId);
55
78
  const result = await storageRepository.setItem(storageKey, settings);
56
79
 
@@ -78,6 +101,9 @@ export class SettingsRepository implements ISettingsRepository {
78
101
 
79
102
  async deleteSettings(userId: string): Promise<SettingsResult<void>> {
80
103
  try {
104
+ // Validate input
105
+ validateUserId(userId);
106
+
81
107
  const storageKey = createUserKey(StorageKey.USER_PREFERENCES, userId);
82
108
  const result = await storageRepository.removeItem(storageKey);
83
109
 
@@ -24,35 +24,47 @@ interface State {
24
24
  export class SettingsErrorBoundary extends Component<Props, State> {
25
25
  constructor(props: Props) {
26
26
  super(props);
27
- this.state = { hasError: false };
27
+ // Initialize state using unknown to avoid type assertion issues
28
+ (this as unknown as { state: State }).state = { hasError: false };
28
29
  }
29
30
 
30
31
  static getDerivedStateFromError(error: Error): State {
31
32
  return { hasError: true, error };
32
33
  }
33
34
 
34
- override componentDidCatch(_error: Error, _errorInfo: React.ErrorInfo) {
35
- // Silent error handling - error already captured in state
36
- void _error;
37
- void _errorInfo;
35
+ componentDidCatch(_error: Error, _errorInfo: React.ErrorInfo): void {
36
+ // Log error to console in development
37
+ if (__DEV__) {
38
+ console.error('Settings Error Boundary caught an error:', _error);
39
+ console.error('Error Info:', _errorInfo);
40
+ }
38
41
  }
39
42
 
40
- override render() {
41
- if (this.state.hasError) {
42
- if (this.props.fallback) {
43
- return this.props.fallback;
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;
52
+
53
+ if (hasError) {
54
+ if (fallback) {
55
+ return fallback;
44
56
  }
45
57
 
46
58
  return (
47
59
  <ErrorBoundaryFallback
48
- error={this.state.error}
49
- fallbackTitle={this.props.fallbackTitle}
50
- fallbackMessage={this.props.fallbackMessage}
60
+ error={error}
61
+ fallbackTitle={fallbackTitle}
62
+ fallbackMessage={fallbackMessage}
51
63
  />
52
64
  );
53
65
  }
54
66
 
55
- return this.props.children;
67
+ return children;
56
68
  }
57
69
  }
58
70
 
@@ -7,7 +7,7 @@ export interface SettingsSectionProps {
7
7
  children: React.ReactNode;
8
8
  }
9
9
 
10
- export const SettingsSection: React.FC<SettingsSectionProps> = ({
10
+ export const SettingsSection: React.FC<SettingsSectionProps> = React.memo(({
11
11
  title,
12
12
  children,
13
13
  }) => {
@@ -23,7 +23,9 @@ export const SettingsSection: React.FC<SettingsSectionProps> = ({
23
23
  {children}
24
24
  </View>
25
25
  );
26
- };
26
+ });
27
+
28
+ SettingsSection.displayName = "SettingsSection";
27
29
 
28
30
  const styles = StyleSheet.create({
29
31
  container: {
@@ -15,13 +15,22 @@ export const useUpdateSettingsMutation = (userId: string) => {
15
15
 
16
16
  return useMutation({
17
17
  mutationFn: async (updates: Partial<UserSettings>) => {
18
- const currentResult = await settingsService.getSettings(userId);
19
- if (!currentResult.success || !currentResult.data) {
20
- throw new Error('Could not find existing settings to update');
21
- }
18
+ // Use optimistic updates to prevent race conditions
19
+ // Get current data from query cache first
20
+ const cacheKey = [...SETTINGS_QUERY_KEY, userId];
21
+ const cachedData = queryClient.getQueryData<UserSettings>(cacheKey);
22
+
23
+ const baseSettings = cachedData
24
+ ? { ...cachedData }
25
+ : await settingsService.getSettings(userId).then(result => {
26
+ if (!result.success || !result.data) {
27
+ throw new Error('Could not find existing settings to update');
28
+ }
29
+ return result.data;
30
+ });
22
31
 
23
32
  const updatedSettings = {
24
- ...currentResult.data,
33
+ ...baseSettings,
25
34
  ...updates,
26
35
  updatedAt: new Date(),
27
36
  };
@@ -129,11 +129,17 @@ export const useSettingsScreenConfig = (
129
129
 
130
130
  const userProfile = useMemo((): UserProfileDisplay => {
131
131
  const isAnonymous = userProfileData?.isAnonymous ?? true;
132
+
133
+ // Ensure t function is available before using it
134
+ const anonymousName = typeof t === 'function'
135
+ ? t("settings.profile.anonymousName")
136
+ : 'Anonymous';
137
+
132
138
  return {
133
- displayName: userProfileData?.displayName || t("settings.profile.anonymousName"),
134
- userId: userProfileData?.userId,
139
+ displayName: userProfileData?.displayName || anonymousName,
140
+ userId: userProfileData?.userId ?? null,
135
141
  isAnonymous,
136
- avatarUrl: userProfileData?.avatarUrl,
142
+ avatarUrl: userProfileData?.avatarUrl ?? null,
137
143
  onPress: isAnonymous ? handleSignIn : undefined,
138
144
  accountSettingsRoute: isAnonymous ? undefined : "Account",
139
145
  };
@@ -141,32 +147,37 @@ export const useSettingsScreenConfig = (
141
147
 
142
148
  const accountConfig = useMemo((): AccountScreenConfig => {
143
149
  const isAnonymous = user?.isAnonymous ?? true;
150
+
151
+ // Ensure t function is available before using it
152
+ const getTranslation = (key: string, fallback: string) =>
153
+ typeof t === 'function' ? t(key) : fallback;
154
+
144
155
  return {
145
156
  profile: {
146
- displayName: userProfileData?.displayName || user?.displayName || t("settings.profile.anonymousName"),
147
- userId: userProfileData?.userId || user?.uid,
157
+ displayName: userProfileData?.displayName || user?.displayName || getTranslation("settings.profile.anonymousName", "Anonymous"),
158
+ userId: userProfileData?.userId ?? user?.uid ?? null,
148
159
  isAnonymous,
149
- avatarUrl: userProfileData?.avatarUrl || user?.photoURL || undefined,
160
+ avatarUrl: userProfileData?.avatarUrl ?? user?.photoURL ?? undefined,
150
161
  benefits: isAnonymous ? [
151
- t("settings.profile.benefits.saveHistory"),
152
- t("settings.profile.benefits.syncDevices"),
153
- t("settings.profile.benefits.cloudSync"),
154
- t("settings.profile.benefits.secureBackup"),
162
+ getTranslation("settings.profile.benefits.saveHistory", "Save history"),
163
+ getTranslation("settings.profile.benefits.syncDevices", "Sync devices"),
164
+ getTranslation("settings.profile.benefits.cloudSync", "Cloud sync"),
165
+ getTranslation("settings.profile.benefits.secureBackup", "Secure backup"),
155
166
  ] : undefined,
156
167
  },
157
168
  isAnonymous,
158
- editProfileText: t("settings.account.editProfile"),
169
+ editProfileText: getTranslation("settings.account.editProfile", "Edit Profile"),
159
170
  onSignIn: handleSignIn,
160
171
  accountActions: {
161
172
  onLogout: handleSignOut,
162
173
  onDeleteAccount: handleDeleteAccount,
163
- logoutText: t("settings.account.logout"),
164
- logoutConfirmTitle: t("settings.account.logoutConfirmTitle"),
165
- logoutConfirmMessage: t("settings.account.logoutConfirmMessage"),
166
- cancelText: t("common.cancel"),
167
- deleteAccountText: t("settings.account.deleteAccount"),
168
- deleteConfirmTitle: t("settings.account.deleteConfirmTitle"),
169
- deleteConfirmMessage: t("settings.account.deleteConfirmMessage"),
174
+ logoutText: getTranslation("settings.account.logout", "Log Out"),
175
+ logoutConfirmTitle: getTranslation("settings.account.logoutConfirmTitle", "Log Out"),
176
+ logoutConfirmMessage: getTranslation("settings.account.logoutConfirmMessage", "Are you sure you want to log out?"),
177
+ cancelText: getTranslation("common.cancel", "Cancel"),
178
+ deleteAccountText: getTranslation("settings.account.deleteAccount", "Delete Account"),
179
+ deleteConfirmTitle: getTranslation("settings.account.deleteConfirmTitle", "Delete Account"),
180
+ deleteConfirmMessage: getTranslation("settings.account.deleteConfirmMessage", "Are you sure you want to delete your account? This action cannot be undone."),
170
181
  },
171
182
  };
172
183
  }, [user, userProfileData, handleSignIn, handleSignOut, handleDeleteAccount, t]);
@@ -23,7 +23,7 @@ import {
23
23
  createQuietHoursTranslations,
24
24
  createLegalScreenProps,
25
25
  } from "./utils";
26
- import type { SettingsStackParamList, SettingsStackNavigatorProps } from "./types";
26
+ import type { SettingsStackParamList, SettingsStackNavigatorProps, AdditionalScreen } from "./types";
27
27
  import { GamificationScreenWrapper } from "../../domains/gamification";
28
28
 
29
29
  export const SettingsStackNavigator: React.FC<SettingsStackNavigatorProps> = ({
@@ -74,7 +74,7 @@ export const SettingsStackNavigator: React.FC<SettingsStackNavigatorProps> = ({
74
74
  );
75
75
 
76
76
  const screens = React.useMemo(() => {
77
- const list: StackScreen[] = [
77
+ const baseScreens: StackScreen[] = [
78
78
  {
79
79
  name: "SettingsMain",
80
80
  options: { headerShown: false },
@@ -119,41 +119,56 @@ export const SettingsStackNavigator: React.FC<SettingsStackNavigatorProps> = ({
119
119
  },
120
120
  ];
121
121
 
122
- if (faqData && faqData.categories.length > 0) {
123
- list.push({
124
- name: "FAQ",
125
- options: { headerTitle: t("settings.faqs.title") },
126
- children: () => (
127
- <FAQScreen
128
- categories={faqData.categories}
129
- searchPlaceholder={t("settings.faqs.searchPlaceholder")}
130
- emptySearchTitle={t("settings.faqs.emptySearchTitle")}
131
- emptySearchMessage={t("settings.faqs.emptySearchMessage")}
132
- headerTitle={t("settings.faqs.headerTitle")}
133
- />
134
- ),
135
- });
136
- }
122
+ // FAQ screen - conditionally add
123
+ const faqScreen: StackScreen | null = (faqData && faqData.categories.length > 0)
124
+ ? {
125
+ name: "FAQ",
126
+ options: { headerTitle: t("settings.faqs.title") },
127
+ children: () => (
128
+ <FAQScreen
129
+ categories={faqData.categories}
130
+ searchPlaceholder={t("settings.faqs.searchPlaceholder")}
131
+ emptySearchTitle={t("settings.faqs.emptySearchTitle")}
132
+ emptySearchMessage={t("settings.faqs.emptySearchMessage")}
133
+ headerTitle={t("settings.faqs.headerTitle")}
134
+ />
135
+ ),
136
+ }
137
+ : null;
137
138
 
138
- // Add additional screens
139
- additionalScreens.forEach((screen) => {
140
- list.push({
139
+ // Additional screens - map to StackScreen format
140
+ const additionalStackScreens: StackScreen[] = (additionalScreens as readonly AdditionalScreen[]).map((screen: AdditionalScreen): StackScreen => {
141
+ // Create base screen object
142
+ const stackScreen: Record<string, unknown> = {
141
143
  name: screen.name,
142
- component: screen.component as StackScreen['component'],
143
- children: screen.children as StackScreen['children'],
144
- options: screen.options as StackScreen['options'],
145
- });
144
+ };
145
+
146
+ // Conditionally add properties
147
+ if (screen.component) {
148
+ stackScreen.component = screen.component;
149
+ }
150
+ if (screen.children) {
151
+ stackScreen.children = screen.children;
152
+ }
153
+ if (screen.options) {
154
+ stackScreen.options = screen.options;
155
+ }
156
+
157
+ // Type assertion to StackScreen
158
+ return stackScreen as unknown as StackScreen;
146
159
  });
147
160
 
148
- if (gamificationConfig?.enabled) {
149
- list.push({
150
- name: "Gamification",
151
- options: { headerTitle: t("settings.gamification.title") },
152
- children: () => <GamificationScreenWrapper config={gamificationConfig} />,
153
- });
154
- }
161
+ // Gamification screen - conditionally add
162
+ const gamificationScreen: StackScreen | null = gamificationConfig?.enabled
163
+ ? {
164
+ name: "Gamification",
165
+ options: { headerTitle: t("settings.gamification.title") },
166
+ children: () => <GamificationScreenWrapper config={gamificationConfig} />,
167
+ }
168
+ : null;
155
169
 
156
- list.push({
170
+ // Language selection screen
171
+ const languageScreen: StackScreen = {
157
172
  name: "LanguageSelection",
158
173
  options: { headerShown: false },
159
174
  children: () => (
@@ -162,17 +177,26 @@ export const SettingsStackNavigator: React.FC<SettingsStackNavigatorProps> = ({
162
177
  searchPlaceholder={t("settings.languageSelection.searchPlaceholder")}
163
178
  />
164
179
  ),
165
- });
180
+ };
166
181
 
167
- if (accountConfig) {
168
- list.push({
169
- name: "Account",
170
- options: { headerTitle: t("settings.account.title") },
171
- children: () => <AccountScreen config={accountConfig} />,
172
- });
173
- }
182
+ // Account screen - conditionally add
183
+ const accountScreen: StackScreen | null = accountConfig
184
+ ? {
185
+ name: "Account",
186
+ options: { headerTitle: t("settings.account.title") },
187
+ children: () => <AccountScreen config={accountConfig} />,
188
+ }
189
+ : null;
174
190
 
175
- return list;
191
+ // Compose final list using spread operator (immutable)
192
+ return [
193
+ ...baseScreens,
194
+ ...(faqScreen ? [faqScreen] : []),
195
+ ...additionalStackScreens,
196
+ ...(gamificationScreen ? [gamificationScreen] : []),
197
+ languageScreen,
198
+ ...(accountScreen ? [accountScreen] : []),
199
+ ];
176
200
  }, [
177
201
  t,
178
202
  showHeader,
@@ -2,6 +2,7 @@
2
2
  * Settings Navigation Types
3
3
  */
4
4
 
5
+ import type React from 'react';
5
6
  import type { SettingsConfig, CustomSettingsSection } from "../screens/types";
6
7
  import type { DevSettingsProps } from "../../domains/dev";
7
8
  import type { FAQCategory } from "../../domains/faqs";
@@ -17,6 +17,7 @@ import type { SettingsConfig, CustomSettingsSection } from "./types";
17
17
  import type { DevSettingsProps } from "../../domains/dev";
18
18
 
19
19
  export interface SettingsScreenProps {
20
+ children?: React.ReactNode;
20
21
  config?: SettingsConfig;
21
22
  /** Show user profile header */
22
23
  showUserProfile?: boolean;
@@ -56,6 +57,7 @@ export interface SettingsScreenProps {
56
57
  }
57
58
 
58
59
  export const SettingsScreen: React.FC<SettingsScreenProps> = ({
60
+ children,
59
61
  config = {},
60
62
  showUserProfile,
61
63
  userProfile,
@@ -72,17 +74,46 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
72
74
  }) => {
73
75
  const navigation = useAppNavigation();
74
76
 
75
- const normalizedConfig = normalizeSettingsConfig(config);
76
- const features = useFeatureDetection(normalizedConfig, navigation, featureOptions);
77
+ // Memoize config normalization to prevent unnecessary recalculations
78
+ const normalizedConfig = React.useMemo(
79
+ () => normalizeSettingsConfig(config),
80
+ [config]
81
+ );
82
+
83
+ // Memoize feature detection to prevent unnecessary recalculations
84
+ const features = React.useMemo(
85
+ () => useFeatureDetection(normalizedConfig, navigation, featureOptions),
86
+ [normalizedConfig, navigation, featureOptions]
87
+ );
77
88
 
78
89
  // Determine if user profile should be shown (explicit prop takes priority, then config)
79
90
  const shouldShowUserProfile = showUserProfile ?? features.userProfile;
80
91
 
81
- return (
82
- <ScreenLayout
83
- header={showHeader ? <SettingsHeader showCloseButton={showCloseButton} onClose={onClose} /> : undefined}
84
- >
92
+ // Workaround: Use conditional rendering with type assertion
93
+ if (showHeader) {
94
+ return <ScreenLayout header={<SettingsHeader showCloseButton={showCloseButton} onClose={onClose} />}>
85
95
  <SettingsErrorBoundary>
96
+ {children ?? (
97
+ <SettingsContent
98
+ normalizedConfig={normalizedConfig}
99
+ features={features}
100
+ showUserProfile={shouldShowUserProfile}
101
+ userProfile={userProfile}
102
+ showFooter={showFooter}
103
+ footerText={footerText}
104
+ appVersion={appVersion}
105
+ customSections={customSections}
106
+ devSettings={devSettings}
107
+ gamificationConfig={gamificationConfig}
108
+ />
109
+ )}
110
+ </SettingsErrorBoundary>
111
+ </ScreenLayout>;
112
+ }
113
+
114
+ return <ScreenLayout>
115
+ <SettingsErrorBoundary>
116
+ {children ?? (
86
117
  <SettingsContent
87
118
  normalizedConfig={normalizedConfig}
88
119
  features={features}
@@ -95,7 +126,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
95
126
  devSettings={devSettings}
96
127
  gamificationConfig={gamificationConfig}
97
128
  />
98
- </SettingsErrorBoundary>
99
- </ScreenLayout>
100
- );
129
+ )}
130
+ </SettingsErrorBoundary>
131
+ </ScreenLayout>;
101
132
  };
@@ -12,7 +12,7 @@ export const CustomSettingsList: React.FC<CustomSettingsListProps> = ({
12
12
  }) => {
13
13
  const sortedSections = useMemo(() => {
14
14
  return Array.from(customSections)
15
- .sort((a, b) => (a.order ?? 999) - (b.order ?? 999));
15
+ .sort((a: CustomSettingsSection, b: CustomSettingsSection) => (a.order ?? 999) - (b.order ?? 999));
16
16
  }, [customSections]);
17
17
 
18
18
  if (!sortedSections.length) return null;
@@ -25,9 +25,9 @@ export const CustomSettingsList: React.FC<CustomSettingsListProps> = ({
25
25
  title={section.title}
26
26
  >
27
27
  {section.content}
28
- {!section.content && section.items?.map((item) => (
28
+ {!section.content && section.items && section.items.length > 0 && section.items.map((item) => (
29
29
  <SettingsItemCard
30
- key={item.id}
30
+ key={item.id || `item-${index}`}
31
31
  title={item.title}
32
32
  description={item.subtitle}
33
33
  icon={item.icon}