@umituz/react-native-settings 5.2.14 → 5.2.16

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/domains/appearance/presentation/components/ColorPicker.tsx +4 -6
  3. package/src/domains/faqs/presentation/components/FAQItem.tsx +2 -2
  4. package/src/domains/faqs/presentation/hooks/useFAQSearch.ts +17 -5
  5. package/src/domains/faqs/presentation/screens/FAQScreen.tsx +44 -25
  6. package/src/domains/gamification/components/GamificationScreen/AchievementsList.tsx +4 -4
  7. package/src/domains/gamification/components/GamificationScreen/GamificationScreenWithConfig.tsx +11 -10
  8. package/src/domains/gamification/hooks/useGamification.ts +1 -1
  9. package/src/domains/legal/presentation/components/LegalLinks.tsx +4 -4
  10. package/src/domains/localization/infrastructure/config/I18nInitializer.ts +2 -2
  11. package/src/domains/localization/infrastructure/hooks/useLanguageSelection.ts +16 -4
  12. package/src/domains/localization/infrastructure/storage/AsyncStorageWrapper.ts +15 -5
  13. package/src/domains/localization/infrastructure/storage/LanguageSwitcher.ts +21 -17
  14. package/src/domains/localization/infrastructure/storage/LocalizationStore.ts +55 -42
  15. package/src/domains/localization/presentation/screens/LanguageSelectionScreen.tsx +14 -4
  16. package/src/domains/notifications/infrastructure/services/NotificationScheduler.ts +77 -57
  17. package/src/domains/notifications/infrastructure/services/NotificationService.ts +1 -0
  18. package/src/domains/notifications/infrastructure/utils/triggerBuilder.ts +3 -1
  19. package/src/domains/notifications/quietHours/infrastructure/hooks/useQuietHoursActions.ts +19 -10
  20. package/src/domains/notifications/reminders/presentation/screens/ReminderListScreen.tsx +9 -0
  21. package/src/domains/rating/application/services/RatingService.ts +5 -5
  22. package/src/domains/rating/infrastructure/storage/RatingStorage.ts +10 -10
  23. package/src/domains/rating/presentation/components/StarRating.tsx +6 -1
  24. package/src/domains/video-tutorials/presentation/screens/VideoTutorialsScreen.tsx +8 -0
  25. package/src/infrastructure/repositories/SettingsRepository.ts +2 -1
  26. package/src/infrastructure/utils/async/retryAndTimeout.ts +10 -2
  27. package/src/infrastructure/utils/constants/textLimits.ts +23 -0
  28. package/src/infrastructure/utils/sanitizers.ts +4 -2
  29. package/src/infrastructure/utils/styleTokens.ts +13 -0
  30. package/src/infrastructure/utils/validation/formValidators.ts +3 -2
  31. package/src/presentation/components/ErrorBoundary/index.ts +3 -0
  32. package/src/presentation/components/ErrorBoundary/withErrorBoundary.tsx +45 -0
  33. package/src/presentation/components/SettingsItemCard.tsx +2 -1
  34. package/src/presentation/navigation/hooks/useNavigationHandlers.ts +33 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-settings",
3
- "version": "5.2.14",
3
+ "version": "5.2.16",
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",
@@ -12,6 +12,7 @@ import {
12
12
  AtomicText,
13
13
  useAppDesignTokens,
14
14
  } from "@umituz/react-native-design-system";
15
+ import { isDev } from "../../../../utils/devUtils";
15
16
 
16
17
  interface ColorPickerProps {
17
18
  label: string;
@@ -31,9 +32,6 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
31
32
  // Memoize styles to prevent unnecessary re-creation
32
33
  const styles = useMemo(() => getStyles(tokens), [tokens]);
33
34
 
34
- // Memoize colors array to prevent unnecessary re-renders
35
- const colorsMemo = useMemo(() => colors, [colors]);
36
-
37
35
  // Stable callback for color change to prevent infinite re-renders
38
36
  const handleColorChange = useCallback((color: string) => {
39
37
  try {
@@ -41,7 +39,7 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
41
39
  if (value === color) return;
42
40
  onValueChange(color);
43
41
  } catch (error) {
44
- if (__DEV__) {
42
+ if (isDev()) {
45
43
  console.error('[ColorPicker] Failed to change color:', error);
46
44
  }
47
45
  // Optionally: Show user feedback about the error
@@ -50,7 +48,7 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
50
48
 
51
49
  // Memoize color options to prevent unnecessary re-renders
52
50
  const colorOptions = useMemo(() => {
53
- return colorsMemo.map((color) => {
51
+ return colors.map((color) => {
54
52
  const isSelected = value === color;
55
53
 
56
54
  return (
@@ -74,7 +72,7 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
74
72
  </TouchableOpacity>
75
73
  );
76
74
  });
77
- }, [colorsMemo, value, handleColorChange, styles, tokens.colors.textInverse]);
75
+ }, [colors, value, handleColorChange, styles, tokens.colors.textInverse]);
78
76
 
79
77
  return (
80
78
  <View style={styles.container}>
@@ -26,7 +26,7 @@ export interface FAQItemProps {
26
26
  styles?: FAQItemStyles;
27
27
  }
28
28
 
29
- export const FAQItemComponent: React.FC<FAQItemProps> = ({
29
+ export const FAQItemComponent: React.FC<FAQItemProps> = React.memo(({
30
30
  item,
31
31
  isExpanded,
32
32
  onToggle,
@@ -115,4 +115,4 @@ export const FAQItemComponent: React.FC<FAQItemProps> = ({
115
115
  )}
116
116
  </View>
117
117
  );
118
- };
118
+ });
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * FAQ Search Hook
3
- * Handles FAQ search state and filtering
3
+ * Handles FAQ search state and filtering with debounce
4
4
  */
5
5
 
6
- import { useState, useMemo } from 'react';
6
+ import { useState, useMemo, useEffect } from 'react';
7
7
  import { FAQCategory } from '../../domain/entities/FAQEntity';
8
8
  import { FAQSearchService } from '../../domain/services/FAQSearchService';
9
9
 
10
+ const SEARCH_DEBOUNCE_MS = 300;
11
+
10
12
  export interface UseFAQSearchResult {
11
13
  searchQuery: string;
12
14
  setSearchQuery: (query: string) => void;
@@ -16,13 +18,23 @@ export interface UseFAQSearchResult {
16
18
 
17
19
  export function useFAQSearch(categories: FAQCategory[]): UseFAQSearchResult {
18
20
  const [searchQuery, setSearchQuery] = useState('');
21
+ const [debouncedQuery, setDebouncedQuery] = useState('');
22
+
23
+ // Debounce search query
24
+ useEffect(() => {
25
+ const timer = setTimeout(() => {
26
+ setDebouncedQuery(searchQuery);
27
+ }, SEARCH_DEBOUNCE_MS);
28
+
29
+ return () => clearTimeout(timer);
30
+ }, [searchQuery]);
19
31
 
20
32
  const filteredCategories = useMemo(
21
- () => FAQSearchService.searchCategories(searchQuery, categories),
22
- [searchQuery, categories]
33
+ () => FAQSearchService.searchCategories(debouncedQuery, categories),
34
+ [debouncedQuery, categories]
23
35
  );
24
36
 
25
- const hasResults = filteredCategories.length > 0 || !searchQuery.trim();
37
+ const hasResults = filteredCategories.length > 0 || !debouncedQuery.trim();
26
38
 
27
39
  return {
28
40
  searchQuery,
@@ -4,8 +4,8 @@
4
4
  * Uses design system tokens for theming
5
5
  */
6
6
 
7
- import React, { useMemo } from 'react';
8
- import { View, ScrollView, StyleSheet, ViewStyle, TextStyle, useWindowDimensions } from 'react-native';
7
+ import React, { useMemo, useCallback } from 'react';
8
+ import { View, FlatList, StyleSheet, ViewStyle, TextStyle, useWindowDimensions } from 'react-native';
9
9
  import { useAppDesignTokens, ScreenLayout, getContentMaxWidth, NavigationHeader, useAppNavigation } from '@umituz/react-native-design-system';
10
10
  import { FAQCategory } from '../../domain/entities/FAQEntity';
11
11
  import { useFAQSearch } from '../hooks/useFAQSearch';
@@ -83,7 +83,29 @@ export const FAQScreen: React.FC<FAQScreenProps> = ({
83
83
  />
84
84
  );
85
85
 
86
- const renderContent = () => {
86
+ const renderCategory = useCallback(({ item }: { item: FAQCategory }) => (
87
+ <FAQCategoryComponent
88
+ category={item}
89
+ isExpanded={isExpanded}
90
+ onToggleItem={toggleExpansion}
91
+ styles={customStyles?.category}
92
+ />
93
+ ), [isExpanded, toggleExpansion, customStyles?.category]);
94
+
95
+ const keyExtractor = useCallback((item: FAQCategory) => item.id, []);
96
+
97
+ const renderListHeader = useCallback(() => (
98
+ <View style={[styles.header, customStyles?.header]}>
99
+ <FAQSearchBar
100
+ value={searchQuery}
101
+ onChangeText={setSearchQuery}
102
+ placeholder={searchPlaceholder}
103
+ styles={customStyles?.searchBar}
104
+ />
105
+ </View>
106
+ ), [searchQuery, setSearchQuery, searchPlaceholder, customStyles?.searchBar, customStyles?.header, styles.header]);
107
+
108
+ const renderListEmpty = useCallback(() => {
87
109
  if (searchQuery && !hasResults) {
88
110
  return (
89
111
  <FAQEmptyState
@@ -93,34 +115,31 @@ export const FAQScreen: React.FC<FAQScreenProps> = ({
93
115
  />
94
116
  );
95
117
  }
118
+ return null;
119
+ }, [searchQuery, hasResults, emptySearchTitle, emptySearchMessage, customStyles?.emptyState]);
96
120
 
121
+ const renderListFooter = useCallback(() => (
122
+ <View style={{ height: tokens.spacing.xl * 2 }} />
123
+ ), [tokens.spacing.xl]);
124
+
125
+ const renderContent = () => {
97
126
  return (
98
127
  <View style={{ flex: 1 }}>
99
- <View style={[styles.header, customStyles?.header]}>
100
- <FAQSearchBar
101
- value={searchQuery}
102
- onChangeText={setSearchQuery}
103
- placeholder={searchPlaceholder}
104
- styles={customStyles?.searchBar}
105
- />
106
- </View>
107
-
108
- <ScrollView
128
+ <FlatList
129
+ data={filteredCategories}
130
+ renderItem={renderCategory}
131
+ keyExtractor={keyExtractor}
132
+ ListHeaderComponent={renderListHeader}
133
+ ListEmptyComponent={renderListEmpty}
134
+ ListFooterComponent={renderListFooter}
109
135
  style={[styles.content, customStyles?.content]}
110
136
  contentContainerStyle={{ paddingVertical: tokens.spacing.md }}
111
137
  showsVerticalScrollIndicator={false}
112
- >
113
- {filteredCategories.map((category) => (
114
- <FAQCategoryComponent
115
- key={category.id}
116
- category={category}
117
- isExpanded={isExpanded}
118
- onToggleItem={toggleExpansion}
119
- styles={customStyles?.category}
120
- />
121
- ))}
122
- <View style={{ height: tokens.spacing.xl * 2 }} />
123
- </ScrollView>
138
+ initialNumToRender={5}
139
+ maxToRenderPerBatch={5}
140
+ windowSize={10}
141
+ removeClippedSubviews={true}
142
+ />
124
143
  </View>
125
144
  );
126
145
  };
@@ -67,9 +67,9 @@ export const AchievementsList: React.FC<AchievementsListProps> = ({
67
67
  ) : (
68
68
  <>
69
69
  {/* Unlocked achievements first */}
70
- {unlocked.map((achievement, index) => (
70
+ {unlocked.map((achievement) => (
71
71
  <AchievementItem
72
- key={`unlocked-${achievement.title}-${index}`}
72
+ key={`achievement-unlocked-${achievement.id || achievement.title}`}
73
73
  {...achievement}
74
74
  accentColor={accentColor}
75
75
  backgroundColor={cardBackgroundColor}
@@ -79,9 +79,9 @@ export const AchievementsList: React.FC<AchievementsListProps> = ({
79
79
  ))}
80
80
 
81
81
  {/* Locked achievements */}
82
- {locked.map((achievement, index) => (
82
+ {locked.map((achievement) => (
83
83
  <AchievementItem
84
- key={`locked-${achievement.title}-${index}`}
84
+ key={`achievement-locked-${achievement.id || achievement.title}`}
85
85
  {...achievement}
86
86
  accentColor={accentColor}
87
87
  backgroundColor={cardBackgroundColor}
@@ -38,39 +38,40 @@ export const GamificationScreenWithConfig: React.FC<GamificationConfigProps> = (
38
38
  type: a.type
39
39
  }));
40
40
 
41
+ const translations = config.translations || {};
41
42
  const screenProps: GamificationScreenProps = {
42
- title: config.translations.title,
43
- statsTitle: config.translations.statsTitle,
44
- achievementsTitle: config.translations.achievementsTitle,
45
- streakTitle: config.translations.streakTitle,
43
+ title: translations.title || 'Gamification',
44
+ statsTitle: translations.statsTitle || 'Stats',
45
+ achievementsTitle: translations.achievementsTitle || 'Achievements',
46
+ streakTitle: translations.streakTitle || 'Streak',
46
47
  levelProps: {
47
48
  level: level.currentLevel,
48
49
  points: level.currentPoints,
49
50
  pointsToNext: level.pointsToNext,
50
51
  progress: level.progress,
51
- levelTitle: config.translations.levelTitle,
52
+ levelTitle: translations.levelTitle || 'Level',
52
53
  showPoints: true,
53
54
  },
54
55
  streakProps: {
55
56
  current: streak.current,
56
57
  longest: streak.longest,
57
- bestLabel: config.translations.bestStreak,
58
- daysLabel: config.translations.days,
58
+ bestLabel: translations.bestStreak || 'Best',
59
+ daysLabel: translations.days || 'days',
59
60
  },
60
61
  stats: [
61
62
  {
62
- label: config.translations.pointsLabel,
63
+ label: translations.pointsLabel || 'Points',
63
64
  value: points,
64
65
  icon: "⭐",
65
66
  },
66
67
  {
67
- label: config.translations.totalCompletedLabel,
68
+ label: translations.totalCompletedLabel || 'Completed',
68
69
  value: totalTasksCompleted,
69
70
  icon: "✅",
70
71
  },
71
72
  ],
72
73
  achievements: achievementItems,
73
- emptyAchievementsText: config.translations.emptyAchievements,
74
+ emptyAchievementsText: translations.emptyAchievements || 'No achievements yet',
74
75
  };
75
76
 
76
77
  return <GamificationScreenInner {...screenProps} />;
@@ -35,7 +35,7 @@ export const useGamification = (
35
35
  if (config && !store.isInitialized) {
36
36
  store.initialize(config);
37
37
  }
38
- }, [config, store.isInitialized]);
38
+ }, [config, store.isInitialized, store.initialize]);
39
39
 
40
40
  // Calculate level from config
41
41
  const level = useMemo((): LevelState => {
@@ -75,10 +75,10 @@ export const LegalLinks: React.FC<LegalLinksProps> = React.memo(
75
75
  }
76
76
  }, [onTermsPress, termsOfServiceUrl]);
77
77
 
78
- // Memoize conditional rendering to prevent unnecessary re-renders
79
- const showPrivacy = React.useMemo(() => !!(onPrivacyPress || privacyPolicyUrl), [onPrivacyPress, privacyPolicyUrl]);
80
- const showTerms = React.useMemo(() => !!(onTermsPress || termsOfServiceUrl), [onTermsPress, termsOfServiceUrl]);
81
- const showSeparator = React.useMemo(() => showPrivacy && showTerms, [showPrivacy, showTerms]);
78
+ // Direct boolean calculations - no need for useMemo overhead
79
+ const showPrivacy = !!(onPrivacyPress || privacyPolicyUrl);
80
+ const showTerms = !!(onTermsPress || termsOfServiceUrl);
81
+ const showSeparator = showPrivacy && showTerms;
82
82
 
83
83
  return (
84
84
  <View style={[styles.container, style]}>
@@ -51,8 +51,8 @@ export class I18nInitializer {
51
51
  returnEmptyString: false,
52
52
  returnNull: false,
53
53
  });
54
- } catch {
55
- // Silent error handling
54
+ } catch (error) {
55
+ console.error('[I18nInitializer] Failed to initialize i18n:', languageCode, error);
56
56
  }
57
57
  }
58
58
 
@@ -1,22 +1,34 @@
1
1
  /**
2
2
  * Language Selection Hook
3
- * Manages language selection state and filtering
3
+ * Manages language selection state and filtering with debounce
4
4
  */
5
5
 
6
- import { useState, useMemo } from 'react';
6
+ import { useState, useMemo, useEffect } from 'react';
7
7
  import { useLocalization } from './useLocalization';
8
8
  import { searchLanguages } from '../config/LanguageQuery';
9
9
  import { devError } from '../../../../utils/devUtils';
10
10
 
11
+ const SEARCH_DEBOUNCE_MS = 300;
12
+
11
13
  export const useLanguageSelection = () => {
12
14
  const { currentLanguage, setLanguage } = useLocalization();
13
15
  const [searchQuery, setSearchQuery] = useState('');
16
+ const [debouncedQuery, setDebouncedQuery] = useState('');
14
17
  const [selectedCode, setSelectedCode] = useState(currentLanguage);
15
18
 
16
- const filteredLanguages = useMemo(() => {
17
- return searchLanguages(searchQuery);
19
+ // Debounce search query
20
+ useEffect(() => {
21
+ const timer = setTimeout(() => {
22
+ setDebouncedQuery(searchQuery);
23
+ }, SEARCH_DEBOUNCE_MS);
24
+
25
+ return () => clearTimeout(timer);
18
26
  }, [searchQuery]);
19
27
 
28
+ const filteredLanguages = useMemo(() => {
29
+ return searchLanguages(debouncedQuery);
30
+ }, [debouncedQuery]);
31
+
20
32
  const handleLanguageSelect = async (code: string, onComplete?: () => void) => {
21
33
  try {
22
34
  setSelectedCode(code);
@@ -11,14 +11,24 @@ export const STORAGE_KEYS = {
11
11
 
12
12
  export const StorageWrapper = {
13
13
  async getString(key: string, defaultValue: string): Promise<string> {
14
- const result = await storageRepository.getString(key, defaultValue);
15
- if (result.success && result.data !== null) {
16
- return result.data;
14
+ try {
15
+ const result = await storageRepository.getString(key, defaultValue);
16
+ if (result.success && result.data !== null) {
17
+ return result.data;
18
+ }
19
+ return defaultValue;
20
+ } catch (error) {
21
+ console.error('[StorageWrapper] Failed to get string:', key, error);
22
+ return defaultValue;
17
23
  }
18
- return defaultValue;
19
24
  },
20
25
 
21
26
  async setString(key: string, value: string): Promise<void> {
22
- await storageRepository.setString(key, value);
27
+ try {
28
+ await storageRepository.setString(key, value);
29
+ } catch (error) {
30
+ console.error('[StorageWrapper] Failed to set string:', key, error);
31
+ throw error;
32
+ }
23
33
  },
24
34
  };
@@ -6,11 +6,10 @@
6
6
  * - Persistence
7
7
  */
8
8
 
9
- declare const __DEV__: boolean;
10
-
11
9
  import { storageRepository } from '@umituz/react-native-design-system';
12
10
  import i18n from '../config/i18n';
13
11
  import { languageRepository } from '../repository/LanguageRepository';
12
+ import { isDev } from '../../../../utils/devUtils';
14
13
 
15
14
  const LANGUAGE_STORAGE_KEY = '@localization:language';
16
15
 
@@ -22,27 +21,32 @@ export class LanguageSwitcher {
22
21
  languageCode: string;
23
22
  isRTL: boolean;
24
23
  }> {
25
- if (typeof __DEV__ !== "undefined" && __DEV__) {
26
- }
24
+ try {
25
+ if (isDev()) {
26
+ }
27
27
 
28
- const language = languageRepository.getLanguageByCode(languageCode);
28
+ const language = languageRepository.getLanguageByCode(languageCode);
29
29
 
30
- if (typeof __DEV__ !== "undefined" && __DEV__) {
31
- }
30
+ if (isDev()) {
31
+ }
32
32
 
33
- await i18n.changeLanguage(languageCode);
33
+ await i18n.changeLanguage(languageCode);
34
34
 
35
- if (typeof __DEV__ !== "undefined" && __DEV__) {
36
- }
35
+ if (isDev()) {
36
+ }
37
37
 
38
- await storageRepository.setString(LANGUAGE_STORAGE_KEY, languageCode);
38
+ await storageRepository.setString(LANGUAGE_STORAGE_KEY, languageCode);
39
39
 
40
- if (typeof __DEV__ !== "undefined" && __DEV__) {
41
- }
40
+ if (isDev()) {
41
+ }
42
42
 
43
- return {
44
- languageCode,
45
- isRTL: language?.isRTL ?? false,
46
- };
43
+ return {
44
+ languageCode,
45
+ isRTL: language?.isRTL ?? false,
46
+ };
47
+ } catch (error) {
48
+ console.error('[LanguageSwitcher] Failed to switch language:', languageCode, error);
49
+ throw error;
50
+ }
47
51
  }
48
52
  }
@@ -1,58 +1,71 @@
1
1
  /**
2
- * Localization Store Factory
3
- * Creates and manages localization state with proper separation of concerns
2
+ * Localization Store
3
+ * Manages localization state using createStore pattern for consistency
4
4
  */
5
5
 
6
- import { create } from "zustand";
6
+ import { createStore, storageService } from "@umituz/react-native-design-system";
7
7
  import type { LocalizationState, LocalizationActions, LocalizationGetters } from "./types/LocalizationState";
8
8
  import { languageRepository } from "../repository/LanguageRepository";
9
9
  import { InitializationManager, LanguageSwitchManager, localizationGetters } from "./localizationStoreUtils";
10
10
 
11
- type LocalizationStoreType = LocalizationState & LocalizationActions & LocalizationGetters;
12
-
13
11
  // Instance-level managers
14
12
  const initManager = new InitializationManager();
15
13
  const switchManager = new LanguageSwitchManager();
16
14
 
17
- export const useLocalizationStore = create<LocalizationStoreType>((set, get) => ({
18
- // State
15
+ const DEFAULT_STATE: LocalizationState = {
19
16
  currentLanguage: "en-US",
20
17
  isRTL: false,
21
18
  isInitialized: false,
22
19
  supportedLanguages: languageRepository.getLanguages(),
20
+ };
21
+
22
+ type LocalizationStoreActions = LocalizationActions & LocalizationGetters;
23
+
24
+ export const useLocalizationStore = createStore<LocalizationState, LocalizationStoreActions>({
25
+ name: "localization-storage",
26
+ initialState: DEFAULT_STATE,
27
+ persist: true,
28
+ storage: storageService,
29
+ version: 1,
30
+ partialize: (state) => ({
31
+ currentLanguage: state.currentLanguage,
32
+ isRTL: state.isRTL,
33
+ isInitialized: false, // Don't persist initialization state
34
+ supportedLanguages: state.supportedLanguages,
35
+ }),
36
+ actions: (set, get) => ({
37
+ initialize: async () => {
38
+ const { isInitialized } = get();
39
+ await initManager.initialize(isInitialized, set);
40
+ },
41
+
42
+ setLanguage: async (languageCode: string) => {
43
+ await switchManager.setLanguage(languageCode, set);
44
+ },
45
+
46
+ reset: () => {
47
+ initManager.reset();
48
+ switchManager.reset();
49
+
50
+ set({
51
+ currentLanguage: "en-US",
52
+ isRTL: false,
53
+ isInitialized: false,
54
+ });
55
+ },
56
+
57
+ // Getters
58
+ getCurrentLanguage: () => {
59
+ const { currentLanguage } = get();
60
+ return localizationGetters.getCurrentLanguage(currentLanguage);
61
+ },
62
+
63
+ isLanguageSupported: (code: string) => {
64
+ return localizationGetters.isLanguageSupported(code);
65
+ },
23
66
 
24
- // Actions
25
- initialize: async () => {
26
- const { isInitialized } = get();
27
- await initManager.initialize(isInitialized, set);
28
- },
29
-
30
- setLanguage: async (languageCode: string) => {
31
- await switchManager.setLanguage(languageCode, set);
32
- },
33
-
34
- reset: () => {
35
- initManager.reset();
36
- switchManager.reset();
37
-
38
- set({
39
- currentLanguage: "en-US",
40
- isRTL: false,
41
- isInitialized: false,
42
- });
43
- },
44
-
45
- // Getters
46
- getCurrentLanguage: () => {
47
- const { currentLanguage } = get();
48
- return localizationGetters.getCurrentLanguage(currentLanguage);
49
- },
50
-
51
- isLanguageSupported: (code: string) => {
52
- return localizationGetters.isLanguageSupported(code);
53
- },
54
-
55
- getSupportedLanguages: () => {
56
- return localizationGetters.getSupportedLanguages();
57
- },
58
- }));
67
+ getSupportedLanguages: () => {
68
+ return localizationGetters.getSupportedLanguages();
69
+ },
70
+ }),
71
+ });
@@ -12,6 +12,7 @@ import {
12
12
  NavigationHeader,
13
13
  useAppNavigation,
14
14
  } from '@umituz/react-native-design-system';
15
+ import { isDev } from '../../../../utils/devUtils';
15
16
  import { useLanguageSelection } from '../../infrastructure/hooks/useLanguageSelection';
16
17
  import { LanguageItem } from '../components/LanguageItem';
17
18
  import type { Language } from '../../infrastructure/storage/types/Language';
@@ -38,14 +39,14 @@ export const LanguageSelectionScreen: React.FC<LanguageSelectionScreenProps> = (
38
39
  } = useLanguageSelection();
39
40
 
40
41
  const onSelect = async (code: string) => {
41
- if (__DEV__) {
42
+ if (isDev()) {
42
43
  }
43
44
  await handleLanguageSelect(code, () => {
44
- if (__DEV__) {
45
+ if (isDev()) {
45
46
  }
46
47
  navigation.goBack();
47
48
  });
48
- if (__DEV__) {
49
+ if (isDev()) {
49
50
  }
50
51
  };
51
52
 
@@ -113,12 +114,21 @@ export const LanguageSelectionScreen: React.FC<LanguageSelectionScreenProps> = (
113
114
  renderItem={renderItem}
114
115
  keyExtractor={item => item.code}
115
116
  contentContainerStyle={[
116
- styles.listContent,
117
+ styles.listContent,
117
118
  { paddingBottom: tokens.spacing.xl },
118
119
  customStyles?.listContent
119
120
  ]}
120
121
  showsVerticalScrollIndicator={false}
121
122
  keyboardShouldPersistTaps="handled"
123
+ initialNumToRender={15}
124
+ maxToRenderPerBatch={10}
125
+ windowSize={10}
126
+ removeClippedSubviews={true}
127
+ getItemLayout={(data, index) => ({
128
+ length: 64,
129
+ offset: 64 * index,
130
+ index,
131
+ })}
122
132
  />
123
133
  </ScreenLayout>
124
134
  );
@@ -3,75 +3,95 @@ import type { ScheduleNotificationOptions, ScheduledNotification } from './types
3
3
 
4
4
  export class NotificationScheduler {
5
5
  async scheduleNotification(options: ScheduleNotificationOptions): Promise<string> {
6
- const { title, body, data = {}, trigger, sound = true, badge, categoryIdentifier } = options;
6
+ try {
7
+ const { title, body, data = {}, trigger, sound = true, badge, categoryIdentifier } = options;
7
8
 
8
- let notificationTrigger: Notifications.NotificationTriggerInput = null;
9
+ let notificationTrigger: Notifications.NotificationTriggerInput = null;
9
10
 
10
- if (trigger.type === 'date') {
11
- notificationTrigger = {
12
- date: trigger.date,
13
- channelId: categoryIdentifier || 'default',
14
- };
15
- } else if (trigger.type === 'daily') {
16
- notificationTrigger = {
17
- hour: trigger.hour,
18
- minute: trigger.minute,
19
- repeats: true,
20
- channelId: categoryIdentifier || 'reminders',
21
- };
22
- } else if (trigger.type === 'weekly') {
23
- notificationTrigger = {
24
- weekday: trigger.weekday,
25
- hour: trigger.hour,
26
- minute: trigger.minute,
27
- repeats: true,
28
- channelId: categoryIdentifier || 'reminders',
29
- };
30
- } else if (trigger.type === 'monthly') {
31
- notificationTrigger = {
32
- day: trigger.day,
33
- hour: trigger.hour,
34
- minute: trigger.minute,
35
- repeats: true,
36
- channelId: categoryIdentifier || 'reminders',
37
- };
38
- }
11
+ if (trigger.type === 'date') {
12
+ notificationTrigger = {
13
+ date: trigger.date,
14
+ channelId: categoryIdentifier || 'default',
15
+ };
16
+ } else if (trigger.type === 'daily') {
17
+ notificationTrigger = {
18
+ hour: trigger.hour,
19
+ minute: trigger.minute,
20
+ repeats: true,
21
+ channelId: categoryIdentifier || 'reminders',
22
+ };
23
+ } else if (trigger.type === 'weekly') {
24
+ notificationTrigger = {
25
+ weekday: trigger.weekday,
26
+ hour: trigger.hour,
27
+ minute: trigger.minute,
28
+ repeats: true,
29
+ channelId: categoryIdentifier || 'reminders',
30
+ };
31
+ } else if (trigger.type === 'monthly') {
32
+ notificationTrigger = {
33
+ day: trigger.day,
34
+ hour: trigger.hour,
35
+ minute: trigger.minute,
36
+ repeats: true,
37
+ channelId: categoryIdentifier || 'reminders',
38
+ };
39
+ }
39
40
 
40
- const notificationId = await Notifications.scheduleNotificationAsync({
41
- content: {
42
- title,
43
- body,
44
- data,
45
- sound: sound === true ? 'default' : sound || undefined,
46
- badge,
47
- categoryIdentifier,
48
- priority: Notifications.AndroidNotificationPriority.HIGH,
49
- vibrate: [0, 250, 250, 250],
50
- },
51
- trigger: notificationTrigger,
52
- });
41
+ const notificationId = await Notifications.scheduleNotificationAsync({
42
+ content: {
43
+ title,
44
+ body,
45
+ data,
46
+ sound: sound === true ? 'default' : sound || undefined,
47
+ badge,
48
+ categoryIdentifier,
49
+ priority: Notifications.AndroidNotificationPriority.HIGH,
50
+ vibrate: [0, 250, 250, 250],
51
+ },
52
+ trigger: notificationTrigger,
53
+ });
53
54
 
54
- return notificationId;
55
+ return notificationId;
56
+ } catch (error) {
57
+ console.error('Failed to schedule notification:', error);
58
+ throw new Error(`Notification scheduling failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
59
+ }
55
60
  }
56
61
 
57
62
  async cancelNotification(notificationId: string): Promise<void> {
58
- await Notifications.cancelScheduledNotificationAsync(notificationId);
63
+ try {
64
+ await Notifications.cancelScheduledNotificationAsync(notificationId);
65
+ } catch (error) {
66
+ console.error('Failed to cancel notification:', notificationId, error);
67
+ // Don't throw - canceling a non-existent notification is not critical
68
+ }
59
69
  }
60
70
 
61
71
  async cancelAllNotifications(): Promise<void> {
62
- await Notifications.cancelAllScheduledNotificationsAsync();
72
+ try {
73
+ await Notifications.cancelAllScheduledNotificationsAsync();
74
+ } catch (error) {
75
+ console.error('Failed to cancel all notifications:', error);
76
+ throw new Error(`Failed to cancel notifications: ${error instanceof Error ? error.message : 'Unknown error'}`);
77
+ }
63
78
  }
64
79
 
65
80
  async getScheduledNotifications(): Promise<ScheduledNotification[]> {
66
- const notifications = await Notifications.getAllScheduledNotificationsAsync();
67
- return notifications.map(notification => ({
68
- identifier: notification.identifier,
69
- content: {
70
- title: notification.content.title ?? '',
71
- body: notification.content.body ?? '',
72
- data: notification.content.data as Record<string, string | number | boolean | null>,
73
- },
74
- trigger: notification.trigger,
75
- }));
81
+ try {
82
+ const notifications = await Notifications.getAllScheduledNotificationsAsync();
83
+ return notifications.map(notification => ({
84
+ identifier: notification.identifier,
85
+ content: {
86
+ title: notification.content.title ?? '',
87
+ body: notification.content.body ?? '',
88
+ data: notification.content.data as Record<string, string | number | boolean | null>,
89
+ },
90
+ trigger: notification.trigger,
91
+ }));
92
+ } catch (error) {
93
+ console.error('Failed to get scheduled notifications:', error);
94
+ return []; // Return empty array as fallback
95
+ }
76
96
  }
77
97
  }
@@ -31,6 +31,7 @@ export class NotificationService {
31
31
  NotificationManager.configure();
32
32
  this.isConfigured = true;
33
33
  } catch (error) {
34
+ console.error('[NotificationService] Failed to configure NotificationManager:', error);
34
35
  }
35
36
  }
36
37
  }
@@ -10,10 +10,12 @@ export const buildTrigger = (reminder: Reminder): NotificationTrigger => {
10
10
 
11
11
  switch (frequency) {
12
12
  case 'once': {
13
+ const now = new Date();
13
14
  const date = new Date();
14
15
  date.setHours(hour, minute, 0, 0);
15
16
 
16
- if (date <= new Date()) {
17
+ // If scheduled time has passed today, schedule for tomorrow
18
+ if (date <= now) {
17
19
  date.setDate(date.getDate() + 1);
18
20
  }
19
21
 
@@ -12,28 +12,37 @@ export const useQuietHoursActions = () => {
12
12
  const { updateQuietHours } = usePreferencesStore();
13
13
 
14
14
  const setQuietHoursEnabled = useCallback(async (enabled: boolean): Promise<void> => {
15
- await updateQuietHours({ ...quietHours, enabled });
16
- }, [quietHours, updateQuietHours]);
15
+ // Use getState() to avoid stale closure and race conditions
16
+ const currentQuietHours = usePreferencesStore.getState().quietHours;
17
+ await updateQuietHours({ ...currentQuietHours, enabled });
18
+ }, [updateQuietHours]);
17
19
 
18
20
  const setStartTime = useCallback(async (hour: number, minute: number): Promise<void> => {
19
- await updateQuietHours({ ...quietHours, startHour: hour, startMinute: minute });
20
- }, [quietHours, updateQuietHours]);
21
+ // Use getState() to avoid stale closure and race conditions
22
+ const currentQuietHours = usePreferencesStore.getState().quietHours;
23
+ await updateQuietHours({ ...currentQuietHours, startHour: hour, startMinute: minute });
24
+ }, [updateQuietHours]);
21
25
 
22
26
  const setEndTime = useCallback(async (hour: number, minute: number): Promise<void> => {
23
- await updateQuietHours({ ...quietHours, endHour: hour, endMinute: minute });
24
- }, [quietHours, updateQuietHours]);
27
+ // Use getState() to avoid stale closure and race conditions
28
+ const currentQuietHours = usePreferencesStore.getState().quietHours;
29
+ await updateQuietHours({ ...currentQuietHours, endHour: hour, endMinute: minute });
30
+ }, [updateQuietHours]);
25
31
 
26
32
  const setQuietHours = useCallback(async (config: QuietHoursConfig): Promise<void> => {
27
33
  await updateQuietHours(config);
28
34
  }, [updateQuietHours]);
29
35
 
30
36
  const isInQuietHours = useCallback((): boolean => {
31
- if (!quietHours.enabled) return false;
37
+ // Use getState() to get current quietHours for consistency
38
+ const currentQuietHours = usePreferencesStore.getState().quietHours;
39
+
40
+ if (!currentQuietHours.enabled) return false;
32
41
 
33
42
  const now = new Date();
34
43
  const currentMinutes = now.getHours() * 60 + now.getMinutes();
35
- const startMinutes = quietHours.startHour * 60 + quietHours.startMinute;
36
- const endMinutes = quietHours.endHour * 60 + quietHours.endMinute;
44
+ const startMinutes = currentQuietHours.startHour * 60 + currentQuietHours.startMinute;
45
+ const endMinutes = currentQuietHours.endHour * 60 + currentQuietHours.endMinute;
37
46
 
38
47
  // Validate time values
39
48
  if (startMinutes < 0 || startMinutes >= 1440 || endMinutes < 0 || endMinutes >= 1440) {
@@ -49,7 +58,7 @@ export const useQuietHoursActions = () => {
49
58
  // Current time is after start OR before end
50
59
  return currentMinutes >= startMinutes || currentMinutes < endMinutes;
51
60
  }
52
- }, [quietHours]);
61
+ }, []);
53
62
 
54
63
  return {
55
64
  quietHours,
@@ -112,6 +112,15 @@ export const ReminderListScreen: React.FC<ReminderListScreenProps> = ({
112
112
  ListEmptyComponent={renderEmpty}
113
113
  contentContainerStyle={styles.listContent}
114
114
  showsVerticalScrollIndicator={false}
115
+ initialNumToRender={10}
116
+ maxToRenderPerBatch={5}
117
+ windowSize={10}
118
+ removeClippedSubviews={true}
119
+ getItemLayout={(data, index) => ({
120
+ length: 88,
121
+ offset: 88 * index,
122
+ index,
123
+ })}
115
124
  />
116
125
 
117
126
  {canAddMore && (
@@ -40,7 +40,7 @@ export async function trackEvent(eventType: string): Promise<void> {
40
40
  try {
41
41
  await incrementEventCount(eventType);
42
42
  } catch (error) {
43
- // Silent error handling
43
+ console.error('[RatingService] Failed to track event:', eventType, error);
44
44
  }
45
45
  }
46
46
 
@@ -90,7 +90,7 @@ export async function markPromptShown(eventType: string): Promise<void> {
90
90
  try {
91
91
  await setLastPromptDate(eventType, toISOString());
92
92
  } catch (error) {
93
- // Silent error handling
93
+ console.error('[RatingService] Failed to mark prompt shown:', eventType, error);
94
94
  }
95
95
  }
96
96
 
@@ -101,7 +101,7 @@ export async function markRated(): Promise<void> {
101
101
  try {
102
102
  await setHasRated(true);
103
103
  } catch (error) {
104
- // Silent error handling
104
+ console.error('[RatingService] Failed to mark as rated:', error);
105
105
  }
106
106
  }
107
107
 
@@ -112,7 +112,7 @@ export async function markDismissed(): Promise<void> {
112
112
  try {
113
113
  await setDismissed(true);
114
114
  } catch (error) {
115
- // Silent error handling
115
+ console.error('[RatingService] Failed to mark as dismissed:', error);
116
116
  }
117
117
  }
118
118
 
@@ -130,6 +130,6 @@ export async function reset(eventType?: string): Promise<void> {
130
130
  try {
131
131
  await resetStorage(eventType);
132
132
  } catch (error) {
133
- // Silent error handling
133
+ console.error('[RatingService] Failed to reset:', eventType, error);
134
134
  }
135
135
  }
@@ -35,8 +35,8 @@ export async function getEventCount(eventType: string): Promise<number> {
35
35
  export async function setEventCount(eventType: string, count: number): Promise<void> {
36
36
  try {
37
37
  await storageRepository.setString(KEYS.eventCount(eventType), count.toString());
38
- } catch {
39
- // Silent error handling
38
+ } catch (error) {
39
+ console.error('[RatingStorage] Failed to set event count:', eventType, count, error);
40
40
  }
41
41
  }
42
42
 
@@ -67,8 +67,8 @@ export async function getLastPromptDate(eventType: string): Promise<string | nul
67
67
  export async function setLastPromptDate(eventType: string, date: string): Promise<void> {
68
68
  try {
69
69
  await storageRepository.setString(KEYS.lastPrompt(eventType), date);
70
- } catch {
71
- // Silent error handling
70
+ } catch (error) {
71
+ console.error('[RatingStorage] Failed to set last prompt date:', eventType, date, error);
72
72
  }
73
73
  }
74
74
 
@@ -90,8 +90,8 @@ export async function getHasRated(): Promise<boolean> {
90
90
  export async function setHasRated(value: boolean): Promise<void> {
91
91
  try {
92
92
  await storageRepository.setString(KEYS.hasRated, value.toString());
93
- } catch {
94
- // Silent error handling
93
+ } catch (error) {
94
+ console.error('[RatingStorage] Failed to set has rated:', value, error);
95
95
  }
96
96
  }
97
97
 
@@ -113,8 +113,8 @@ export async function getDismissed(): Promise<boolean> {
113
113
  export async function setDismissed(value: boolean): Promise<void> {
114
114
  try {
115
115
  await storageRepository.setString(KEYS.dismissed, value.toString());
116
- } catch {
117
- // Silent error handling
116
+ } catch (error) {
117
+ console.error('[RatingStorage] Failed to set dismissed:', value, error);
118
118
  }
119
119
  }
120
120
 
@@ -154,7 +154,7 @@ export async function reset(eventType?: string): Promise<void> {
154
154
  ratingKeys.map((key) => storageRepository.removeItem(key))
155
155
  );
156
156
  }
157
- } catch {
158
- // Silent error handling
157
+ } catch (error) {
158
+ console.error('[RatingStorage] Failed to reset rating data:', eventType, error);
159
159
  }
160
160
  }
@@ -1,4 +1,4 @@
1
- import React, { useState } from "react";
1
+ import React, { useState, useEffect } from "react";
2
2
  import { View, StyleSheet, TouchableOpacity, ViewStyle } from "react-native";
3
3
  import { useAppDesignTokens, AtomicIcon } from "@umituz/react-native-design-system";
4
4
 
@@ -27,6 +27,11 @@ export const StarRating: React.FC<StarRatingProps> = ({
27
27
  const styles = getStyles(tokens);
28
28
  const [internalRating, setInternalRating] = useState(rating);
29
29
 
30
+ // Sync internal state with rating prop changes
31
+ useEffect(() => {
32
+ setInternalRating(rating);
33
+ }, [rating]);
34
+
30
35
  const filledColor = activeColor || tokens.colors.warning;
31
36
  const emptyColor = inactiveColor || tokens.colors.borderLight;
32
37
 
@@ -110,6 +110,10 @@ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> = React.m
110
110
  horizontal
111
111
  showsHorizontalScrollIndicator={false}
112
112
  contentContainerStyle={styles.horizontalList}
113
+ initialNumToRender={3}
114
+ maxToRenderPerBatch={2}
115
+ windowSize={5}
116
+ removeClippedSubviews={true}
113
117
  />
114
118
  </View>
115
119
  )}
@@ -128,6 +132,10 @@ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> = React.m
128
132
  showsVerticalScrollIndicator={false}
129
133
  contentContainerStyle={styles.verticalList}
130
134
  ListHeaderComponent={ListHeader}
135
+ initialNumToRender={8}
136
+ maxToRenderPerBatch={5}
137
+ windowSize={10}
138
+ removeClippedSubviews={true}
131
139
  />
132
140
  </ScreenLayout>
133
141
  );
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { storageRepository, StorageKey, createUserKey } from '@umituz/react-native-design-system';
8
8
  import type { ISettingsRepository, UserSettings, SettingsResult } from '../../application/ports/ISettingsRepository';
9
+ import { TEXT_LENGTH_LIMITS } from '../utils/constants/textLimits';
9
10
 
10
11
  /**
11
12
  * Validates userId to prevent key injection attacks
@@ -19,7 +20,7 @@ const validateUserId = (userId: string): void => {
19
20
  throw new Error('Invalid userId: contains invalid characters');
20
21
  }
21
22
  // Limit length to prevent DoS attacks
22
- if (userId.length > 128) {
23
+ if (userId.length > TEXT_LENGTH_LIMITS.USER_ID) {
23
24
  throw new Error('Invalid userId: exceeds maximum length');
24
25
  }
25
26
  };
@@ -49,9 +49,17 @@ export const withTimeout = async <T>(
49
49
  timeoutMs: number,
50
50
  timeoutMessage: string = "Operation timed out"
51
51
  ): Promise<T> => {
52
+ let timeoutId: NodeJS.Timeout | undefined;
53
+
52
54
  const timeoutPromise = new Promise<never>((_, reject) => {
53
- setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
55
+ timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
54
56
  });
55
57
 
56
- return Promise.race([operation, timeoutPromise]);
58
+ try {
59
+ return await Promise.race([operation, timeoutPromise]);
60
+ } finally {
61
+ if (timeoutId) {
62
+ clearTimeout(timeoutId);
63
+ }
64
+ }
57
65
  };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Text Length Limits
3
+ * Standardized text length constraints used across the application
4
+ */
5
+
6
+ export const TEXT_LENGTH_LIMITS = {
7
+ /** Short titles and labels (100 characters) */
8
+ TITLE_SHORT: 100,
9
+
10
+ /** Standard titles (200 characters) */
11
+ TITLE: 200,
12
+
13
+ /** Descriptions and short text (500 characters) */
14
+ DESCRIPTION: 500,
15
+
16
+ /** Long text and body content (1000 characters) */
17
+ BODY: 1000,
18
+
19
+ /** User ID maximum length (128 characters) */
20
+ USER_ID: 128,
21
+ } as const;
22
+
23
+ export type TextLengthLimit = typeof TEXT_LENGTH_LIMITS[keyof typeof TEXT_LENGTH_LIMITS];
@@ -4,6 +4,8 @@
4
4
  * Provides sanitization functions for user input and props.
5
5
  */
6
6
 
7
+ import { TEXT_LENGTH_LIMITS } from './constants/textLimits';
8
+
7
9
  /**
8
10
  * Sanitize string by trimming and limiting length
9
11
  */
@@ -16,14 +18,14 @@ export function sanitizeString(str: string | undefined, maxLength: number): stri
16
18
  * Sanitize title prop
17
19
  */
18
20
  export function sanitizeTitle(title: string): string {
19
- return sanitizeString(title, 200) || "";
21
+ return sanitizeString(title, TEXT_LENGTH_LIMITS.TITLE) || "";
20
22
  }
21
23
 
22
24
  /**
23
25
  * Sanitize description prop
24
26
  */
25
27
  export function sanitizeDescription(description: string | undefined): string | undefined {
26
- return sanitizeString(description || "", 500);
28
+ return sanitizeString(description || "", TEXT_LENGTH_LIMITS.DESCRIPTION);
27
29
  }
28
30
 
29
31
  /**
@@ -55,6 +55,19 @@ export const OPACITY = {
55
55
  icon: 0.6,
56
56
  } as const;
57
57
 
58
+ /**
59
+ * Alpha/Transparency values for withAlpha() usage
60
+ * Used with design system's withAlpha(color, alpha) function
61
+ */
62
+ export const ALPHA_VALUES = {
63
+ subtle: 0.1, // Very subtle tint
64
+ light: 0.15, // Light background tint
65
+ medium: 0.2, // Medium background
66
+ strong: 0.3, // Strong background
67
+ visible: 0.5, // Clearly visible
68
+ prominent: 0.8, // Almost opaque
69
+ } as const;
70
+
58
71
  /**
59
72
  * Icon size tokens
60
73
  */
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { ValidationResult } from "./core";
7
7
  import { validateRating } from "./numericValidators";
8
+ import { TEXT_LENGTH_LIMITS } from "../constants/textLimits";
8
9
 
9
10
  /**
10
11
  * Feedback form validation
@@ -30,8 +31,8 @@ export const validateFeedbackForm = (data: {
30
31
  return { isValid: false, error: "Description must be at least 10 characters" };
31
32
  }
32
33
 
33
- if (data.description.length > 1000) {
34
- return { isValid: false, error: "Description must be less than 1000 characters" };
34
+ if (data.description.length > TEXT_LENGTH_LIMITS.BODY) {
35
+ return { isValid: false, error: `Description must be less than ${TEXT_LENGTH_LIMITS.BODY} characters` };
35
36
  }
36
37
 
37
38
  return { isValid: true };
@@ -7,3 +7,6 @@ export type {
7
7
  SettingsErrorBoundaryProps,
8
8
  SettingsErrorBoundaryState,
9
9
  } from './SettingsErrorBoundary';
10
+
11
+ export { withErrorBoundary } from './withErrorBoundary';
12
+ export type { WithErrorBoundaryOptions } from './withErrorBoundary';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * withErrorBoundary HOC
3
+ * Wraps screens with SettingsErrorBoundary for error handling
4
+ */
5
+
6
+ import React from 'react';
7
+ import { SettingsErrorBoundary } from './SettingsErrorBoundary';
8
+
9
+ export interface WithErrorBoundaryOptions {
10
+ domainName: string;
11
+ fallback?: React.ReactNode;
12
+ onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
13
+ }
14
+
15
+ /**
16
+ * Higher-Order Component to wrap screens with ErrorBoundary
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * export const AppearanceScreen = withErrorBoundary(
21
+ * AppearanceScreenComponent,
22
+ * { domainName: 'Appearance' }
23
+ * );
24
+ * ```
25
+ */
26
+ export function withErrorBoundary<P extends object>(
27
+ Component: React.ComponentType<P>,
28
+ options: WithErrorBoundaryOptions
29
+ ): React.FC<P> {
30
+ const WrappedComponent: React.FC<P> = (props) => {
31
+ return (
32
+ <SettingsErrorBoundary
33
+ domainName={options.domainName}
34
+ fallback={options.fallback}
35
+ onError={options.onError}
36
+ >
37
+ <Component {...props} />
38
+ </SettingsErrorBoundary>
39
+ );
40
+ };
41
+
42
+ WrappedComponent.displayName = `withErrorBoundary(${Component.displayName || Component.name || 'Component'})`;
43
+
44
+ return WrappedComponent;
45
+ }
@@ -7,6 +7,7 @@ import {
7
7
  } from "@umituz/react-native-design-system";
8
8
  import { validateTitle, validateDescription, validateSwitchProps } from "../../infrastructure/utils/validators";
9
9
  import { sanitizeTitle, sanitizeDescription } from "../../infrastructure/utils/sanitizers";
10
+ import { TEXT_LENGTH_LIMITS } from "../../infrastructure/utils/constants/textLimits";
10
11
  import { SettingsItemCardRightElement } from "./settings/SettingsItemCardRightElement";
11
12
  import { SettingsItemCardContent } from "./settings/SettingsItemCardContent";
12
13
  import { SettingsItemCardSection } from "./settings/SettingsItemCardSection";
@@ -54,7 +55,7 @@ export const SettingsItemCard: React.FC<SettingsItemCardProps> = ({
54
55
  // Sanitize props (before hooks)
55
56
  const sanitizedTitle = sanitizeTitle(title);
56
57
  const sanitizedDescription = sanitizeDescription(description);
57
- const sanitizedSectionTitle = sectionTitle?.trim().slice(0, 100);
58
+ const sanitizedSectionTitle = sectionTitle?.trim().slice(0, TEXT_LENGTH_LIMITS.TITLE_SHORT);
58
59
 
59
60
  const defaultIconBg = iconBgColor || withAlpha(colors.primary, 0.15);
60
61
  const defaultIconColor = iconColor || colors.primary;
@@ -20,17 +20,44 @@ export const useNavigationHandlers = (
20
20
  legalUrls: LegalUrls,
21
21
  aboutTranslations?: AboutConfig["texts"]
22
22
  ): NavigationHandlersResult => {
23
- const handlePrivacyPress = useCallback(() => {
24
- Linking.openURL(legalUrls.privacy);
23
+ const handlePrivacyPress = useCallback(async () => {
24
+ try {
25
+ const canOpen = await Linking.canOpenURL(legalUrls.privacy);
26
+ if (canOpen) {
27
+ await Linking.openURL(legalUrls.privacy);
28
+ } else {
29
+ console.warn('Cannot open privacy policy URL:', legalUrls.privacy);
30
+ }
31
+ } catch (error) {
32
+ console.error('Failed to open privacy policy:', error);
33
+ }
25
34
  }, [legalUrls.privacy]);
26
35
 
27
- const handleTermsPress = useCallback(() => {
28
- Linking.openURL(legalUrls.terms);
36
+ const handleTermsPress = useCallback(async () => {
37
+ try {
38
+ const canOpen = await Linking.canOpenURL(legalUrls.terms);
39
+ if (canOpen) {
40
+ await Linking.openURL(legalUrls.terms);
41
+ } else {
42
+ console.warn('Cannot open terms of service URL:', legalUrls.terms);
43
+ }
44
+ } catch (error) {
45
+ console.error('Failed to open terms of service:', error);
46
+ }
29
47
  }, [legalUrls.terms]);
30
48
 
31
- const handleEulaPress = useCallback(() => {
49
+ const handleEulaPress = useCallback(async () => {
32
50
  if (legalUrls.eula) {
33
- Linking.openURL(legalUrls.eula);
51
+ try {
52
+ const canOpen = await Linking.canOpenURL(legalUrls.eula);
53
+ if (canOpen) {
54
+ await Linking.openURL(legalUrls.eula);
55
+ } else {
56
+ console.warn('Cannot open EULA URL:', legalUrls.eula);
57
+ }
58
+ } catch (error) {
59
+ console.error('Failed to open EULA:', error);
60
+ }
34
61
  }
35
62
  }, [legalUrls.eula]);
36
63