@umituz/react-native-settings 5.3.81 → 5.4.1

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 (28) hide show
  1. package/package.json +7 -9
  2. package/src/domains/about/presentation/components/AboutSection.tsx +5 -10
  3. package/src/domains/appearance/presentation/components/AppearanceSection.tsx +5 -10
  4. package/src/domains/faqs/presentation/components/FAQCategory.tsx +29 -10
  5. package/src/domains/gamification/components/GamificationScreen/AchievementsList.tsx +4 -1
  6. package/src/domains/gamification/components/GamificationScreen/StatsGrid.tsx +31 -10
  7. package/src/domains/gamification/hooks/useGamification.ts +9 -5
  8. package/src/domains/gamification/store/gamificationStore.ts +64 -11
  9. package/src/domains/gamification/types/index.ts +2 -0
  10. package/src/domains/gamification/utils/calculations.ts +9 -4
  11. package/src/domains/legal/presentation/components/LegalSection.tsx +5 -10
  12. package/src/domains/localization/index.ts +1 -1
  13. package/src/domains/localization/infrastructure/components/useLanguageNavigation.ts +4 -5
  14. package/src/domains/localization/infrastructure/config/NamespaceResolver.ts +1 -1
  15. package/src/domains/localization/infrastructure/config/i18n.ts +1 -1
  16. package/src/domains/localization/infrastructure/hooks/useTranslation.ts +1 -1
  17. package/src/domains/localization/infrastructure/storage/LanguageInitializer.ts +1 -1
  18. package/src/domains/localization/infrastructure/storage/LanguageSwitcher.ts +1 -1
  19. package/src/domains/notifications/infrastructure/services/NotificationScheduler.ts +36 -29
  20. package/src/domains/notifications/presentation/components/NotificationsSection.tsx +6 -9
  21. package/src/domains/notifications/presentation/hooks/useTimePicker.ts +10 -5
  22. package/src/domains/notifications/quietHours/infrastructure/hooks/useQuietHoursActions.ts +8 -8
  23. package/src/presentation/navigation/SettingsStackNavigator.tsx +3 -3
  24. package/src/presentation/navigation/hooks/useSettingsNavigation.ts +21 -17
  25. package/src/presentation/navigation/utils/navigationHelpers.ts +2 -2
  26. package/src/presentation/screens/components/sections/CustomSettingsList.tsx +23 -12
  27. package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +5 -8
  28. package/src/account.ts +0 -45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-settings",
3
- "version": "5.3.81",
3
+ "version": "5.4.1",
4
4
  "description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification - expo-store-review and expo-device now lazy loaded",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -76,11 +76,6 @@
76
76
  "react-native": "./src/domains/video-tutorials/index.ts",
77
77
  "default": "./src/domains/video-tutorials/index.ts"
78
78
  },
79
- "./account": {
80
- "types": "./dist/account.d.ts",
81
- "react-native": "./src/account.ts",
82
- "default": "./src/account.ts"
83
- },
84
79
  "./package.json": "./package.json"
85
80
  },
86
81
  "scripts": {
@@ -118,8 +113,9 @@
118
113
  "url": "https://github.com/umituz/react-native-settings"
119
114
  },
120
115
  "optionalDependencies": {
121
- "expo-store-review": "~6.0.0",
122
- "expo-device": "~8.0.0"
116
+ "expo-constants": "~55.0.0",
117
+ "expo-device": "~8.0.0",
118
+ "expo-store-review": "~6.0.0"
123
119
  },
124
120
  "peerDependencies": {
125
121
  "@expo/vector-icons": ">=14.0.0",
@@ -128,6 +124,7 @@
128
124
  "@tanstack/react-query": ">=5.0.0",
129
125
  "@umituz/react-native-design-system": "*",
130
126
  "expo": ">=54.0.0",
127
+ "expo-constants": ">=55.0.0",
131
128
  "expo-device": ">=6.0.0",
132
129
  "expo-haptics": ">=15.0.0",
133
130
  "expo-localization": ">=16.0.0",
@@ -165,7 +162,7 @@
165
162
  "@typescript-eslint/eslint-plugin": "^7.18.0",
166
163
  "@typescript-eslint/parser": "^7.18.0",
167
164
  "@umituz/react-native-auth": "^4.3.39",
168
- "@umituz/react-native-design-system": "latest",
165
+ "@umituz/react-native-design-system": "^4.27.0",
169
166
  "@umituz/react-native-firebase": "^2.4.55",
170
167
  "@umituz/react-native-sentry": "latest",
171
168
  "eslint": "^8.57.0",
@@ -174,6 +171,7 @@
174
171
  "eslint-plugin-react-native": "^5.0.0",
175
172
  "expo-apple-authentication": "^8.0.8",
176
173
  "expo-clipboard": "^8.0.8",
174
+ "expo-constants": "~55.0.0",
177
175
  "expo-crypto": "^15.0.8",
178
176
  "expo-device": "~7.0.0",
179
177
  "expo-file-system": "^19.0.21",
@@ -3,6 +3,7 @@ import { ViewStyle } from 'react-native';
3
3
  import { AboutConfig } from '../../domain/entities/AppInfo';
4
4
  import { SettingsItemCard } from '../../../../presentation/components/SettingsItemCard';
5
5
  import { useSettingsNavigation } from '../../../../presentation/navigation/hooks/useSettingsNavigation';
6
+ import { createRouteOrPressHandler } from '../../../../presentation/navigation/utils/navigationHelpers';
6
7
 
7
8
  export interface AboutSectionProps {
8
9
  config?: AboutConfig;
@@ -29,20 +30,14 @@ export const AboutSection: React.FC<AboutSectionProps> = ({
29
30
 
30
31
  const navigation = useSettingsNavigation();
31
32
 
32
- const route = config?.route || config?.defaultRoute || 'About';
33
33
  const title = propsTitle || config?.title;
34
34
  const description = propsDescription || config?.description;
35
35
  const sectionTitle = propsSectionTitle;
36
36
 
37
- const handlePress = () => {
38
- if (onPress) {
39
- onPress();
40
- } else if (config?.onPress) {
41
- config.onPress();
42
- } else {
43
- navigation.navigate(route as 'About');
44
- }
45
- };
37
+ const handlePress = createRouteOrPressHandler(navigation.navigate, {
38
+ route: config?.route || config?.defaultRoute || 'About',
39
+ onPress: onPress || config?.onPress,
40
+ });
46
41
 
47
42
  if (!title) return null;
48
43
 
@@ -3,6 +3,7 @@ import { ViewStyle } from 'react-native';
3
3
  import { AppearanceSectionConfig } from '../../types';
4
4
  import { SettingsItemCard } from '../../../../presentation/components/SettingsItemCard';
5
5
  import { useSettingsNavigation } from '../../../../presentation/navigation/hooks/useSettingsNavigation';
6
+ import { createRouteOrPressHandler } from '../../../../presentation/navigation/utils/navigationHelpers';
6
7
 
7
8
  export interface AppearanceSectionProps {
8
9
  config?: AppearanceSectionConfig;
@@ -31,19 +32,13 @@ export const AppearanceSection: React.FC<AppearanceSectionProps> = ({
31
32
 
32
33
  const navigation = useSettingsNavigation();
33
34
 
34
- const route = config?.route || config?.defaultRoute || 'Appearance';
35
35
  const title = titleProp || config?.title;
36
36
  const description = descriptionProp || config?.description;
37
37
 
38
- const handlePress = () => {
39
- if (onPress) {
40
- onPress();
41
- } else if (config?.onPress) {
42
- config.onPress();
43
- } else {
44
- navigation.navigate(route as 'Appearance');
45
- }
46
- };
38
+ const handlePress = createRouteOrPressHandler(navigation.navigate, {
39
+ route: config?.route || config?.defaultRoute || 'Appearance',
40
+ onPress: onPress || config?.onPress,
41
+ });
47
42
 
48
43
  if (!title) return null;
49
44
 
@@ -24,6 +24,29 @@ export interface FAQCategoryProps {
24
24
  styles?: FAQCategoryStyles;
25
25
  }
26
26
 
27
+ // Memoized FAQ items list to prevent unnecessary re-renders when other categories update
28
+ const FAQItemsList: React.FC<{
29
+ items: FAQCategoryType['items'];
30
+ isExpanded: (itemId: string) => boolean;
31
+ onToggleItem: (itemId: string) => void;
32
+ customStyles?: FAQCategoryStyles;
33
+ }> = React.memo(({ items, isExpanded, onToggleItem, customStyles }) => (
34
+ <>
35
+ {items.map((item, index) => (
36
+ <FAQItemComponent
37
+ key={item.id}
38
+ item={item}
39
+ isExpanded={isExpanded(item.id)}
40
+ onToggle={() => onToggleItem(item.id)}
41
+ isLast={index === items.length - 1}
42
+ styles={customStyles?.itemStyles}
43
+ />
44
+ ))}
45
+ </>
46
+ ));
47
+
48
+ FAQItemsList.displayName = "FAQItemsList";
49
+
27
50
  export const FAQCategoryComponent: React.FC<FAQCategoryProps> = ({
28
51
  category,
29
52
  isExpanded,
@@ -66,16 +89,12 @@ export const FAQCategoryComponent: React.FC<FAQCategoryProps> = ({
66
89
  </AtomicText>
67
90
  <View style={styles.titleLine} />
68
91
  </View>
69
- {category.items.map((item, index) => (
70
- <FAQItemComponent
71
- key={item.id}
72
- item={item}
73
- isExpanded={isExpanded(item.id)}
74
- onToggle={() => onToggleItem(item.id)}
75
- isLast={index === category.items.length - 1}
76
- styles={customStyles?.itemStyles}
77
- />
78
- ))}
92
+ <FAQItemsList
93
+ items={category.items}
94
+ isExpanded={isExpanded}
95
+ onToggleItem={onToggleItem}
96
+ customStyles={customStyles}
97
+ />
79
98
  </View>
80
99
  );
81
100
  };
@@ -54,13 +54,16 @@ export const AchievementsList: React.FC<AchievementsListProps> = ({
54
54
  );
55
55
  }, [achievements]);
56
56
 
57
+ // Memoize empty state check to avoid recalculation
58
+ const showEmptyState = achievements.length === 0 && emptyAchievementsText;
59
+
57
60
  return (
58
61
  <View style={styles.section}>
59
62
  <AtomicText style={[styles.sectionTitle, { color: textColor }, sectionTitleStyle]}>
60
63
  {achievementsTitle}
61
64
  </AtomicText>
62
65
 
63
- {achievements.length === 0 && emptyAchievementsText ? (
66
+ {showEmptyState ? (
64
67
  <AtomicText style={[styles.emptyText, { color: subtextColor }]}>
65
68
  {emptyAchievementsText}
66
69
  </AtomicText>
@@ -9,6 +9,30 @@ import { StatsCard } from "../StatsCard";
9
9
  import { styles } from "./styles";
10
10
  import type { StatsCardProps } from "../StatsCard";
11
11
 
12
+ // Memoized stats list to prevent unnecessary re-renders when parent updates
13
+ const StatsList: React.FC<{
14
+ stats: StatsGridProps['stats'];
15
+ accentColor: string;
16
+ cardBackgroundColor: string;
17
+ textColor: string;
18
+ subtextColor: string;
19
+ }> = React.memo(({ stats, accentColor, cardBackgroundColor, textColor, subtextColor }) => (
20
+ <>
21
+ {stats.map((stat) => (
22
+ <StatsCard
23
+ key={`${stat.label}-${stat.value}`}
24
+ {...stat}
25
+ accentColor={accentColor}
26
+ backgroundColor={cardBackgroundColor}
27
+ textColor={textColor}
28
+ subtextColor={subtextColor}
29
+ />
30
+ ))}
31
+ </>
32
+ ));
33
+
34
+ StatsList.displayName = "StatsList";
35
+
12
36
  export interface StatsGridProps {
13
37
  statsTitle: string;
14
38
  stats: Array<Omit<StatsCardProps, "accentColor" | "backgroundColor" | "textColor" | "subtextColor">>;
@@ -36,16 +60,13 @@ export const StatsGrid: React.FC<StatsGridProps> = ({
36
60
  {statsTitle}
37
61
  </AtomicText>
38
62
  <View style={styles.statsGrid}>
39
- {stats.map((stat) => (
40
- <StatsCard
41
- key={`${stat.label}-${stat.value}`}
42
- {...stat}
43
- accentColor={accentColor}
44
- backgroundColor={cardBackgroundColor}
45
- textColor={textColor}
46
- subtextColor={subtextColor}
47
- />
48
- ))}
63
+ <StatsList
64
+ stats={stats}
65
+ accentColor={accentColor}
66
+ cardBackgroundColor={cardBackgroundColor}
67
+ textColor={textColor}
68
+ subtextColor={subtextColor}
69
+ />
49
70
  </View>
50
71
  </View>
51
72
  );
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { useCallback, useEffect, useMemo } from "react";
7
7
  import { useGamificationStore } from "../store/gamificationStore";
8
- import { calculateLevel } from "../utils/calculations";
8
+ import { calculateLevel, DEFAULT_POINTS_PER_LEVEL, MAX_PROGRESS } from "../utils/calculations";
9
9
  import type { GamificationConfig, LevelState, Achievement } from "../types";
10
10
 
11
11
  export interface UseGamificationReturn {
@@ -33,9 +33,13 @@ export const useGamification = (
33
33
  // Auto-initialize if config provided
34
34
  useEffect(() => {
35
35
  if (config && !store.isInitialized) {
36
- store.initialize(config);
36
+ store.initialize(config).catch((error) => {
37
+ // Log error but don't crash - initialization is not critical
38
+ console.error('[useGamification] Failed to initialize:', error);
39
+ });
37
40
  }
38
- }, [config, store.isInitialized, store.initialize]);
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ }, [config, store.isInitialized]);
39
43
 
40
44
  // Calculate level from config
41
45
  const level = useMemo((): LevelState => {
@@ -43,8 +47,8 @@ export const useGamification = (
43
47
  return {
44
48
  currentLevel: 1,
45
49
  currentPoints: store.points,
46
- pointsToNext: 50,
47
- progress: Math.min(100, (store.points / 50) * 100),
50
+ pointsToNext: DEFAULT_POINTS_PER_LEVEL,
51
+ progress: Math.min(MAX_PROGRESS, (store.points / DEFAULT_POINTS_PER_LEVEL) * MAX_PROGRESS),
48
52
  };
49
53
  }
50
54
  return calculateLevel(store.points, config.levels);
@@ -30,8 +30,6 @@ const DEFAULT_STATE: GamificationState = {
30
30
  isInitialized: false,
31
31
  };
32
32
 
33
- let currentConfig: GamificationConfig | null = null;
34
-
35
33
  export const useGamificationStore = createStore<GamificationState, GamificationActions>({
36
34
  name: "gamification-storage",
37
35
  initialState: DEFAULT_STATE,
@@ -45,12 +43,15 @@ export const useGamificationStore = createStore<GamificationState, GamificationA
45
43
  streak: state.streak,
46
44
  isLoading: false,
47
45
  isInitialized: false,
46
+ // Don't persist _config - it's runtime only
48
47
  }),
49
48
  actions: (set, get) => ({
50
49
  initialize: async (config: GamificationConfig) => {
51
- currentConfig = config;
52
50
  const state = get();
53
51
 
52
+ // Store config in state (not module-level)
53
+ set({ _config: config, isInitialized: true });
54
+
54
55
  // Initialize achievements from config
55
56
  const achievements: Achievement[] = config.achievements.map((def) => ({
56
57
  ...def,
@@ -67,7 +68,7 @@ export const useGamificationStore = createStore<GamificationState, GamificationA
67
68
  return ach;
68
69
  });
69
70
 
70
- set({ achievements: mergedAchievements, isInitialized: true });
71
+ set({ achievements: mergedAchievements });
71
72
  },
72
73
 
73
74
  addPoints: (amount: number) => {
@@ -77,7 +78,7 @@ export const useGamificationStore = createStore<GamificationState, GamificationA
77
78
 
78
79
  completeTask: () => {
79
80
  const state = get();
80
- const pointsToAdd = currentConfig?.pointsPerAction ?? 15;
81
+ const pointsToAdd = state._config?.pointsPerAction ?? 15;
81
82
 
82
83
  const newTotalTasks = state.totalTasksCompleted + 1;
83
84
 
@@ -86,9 +87,63 @@ export const useGamificationStore = createStore<GamificationState, GamificationA
86
87
  points: state.points + pointsToAdd,
87
88
  });
88
89
 
89
- const actions = get() as GamificationActions;
90
- actions.updateStreak();
91
- actions.checkAchievements();
90
+ // Update streak
91
+ const currentStreak = state.streak.lastActivityDate
92
+ ? new Date(state.streak.lastActivityDate)
93
+ : null;
94
+ const now = new Date();
95
+
96
+ let newStreak = state.streak.current;
97
+
98
+ if (!currentStreak || !isSameDay(currentStreak, now)) {
99
+ if (isStreakActive(state.streak.lastActivityDate)) {
100
+ newStreak = state.streak.current + 1;
101
+ } else {
102
+ newStreak = 1;
103
+ }
104
+ }
105
+
106
+ set({
107
+ streak: {
108
+ current: newStreak,
109
+ longest: Math.max(state.streak.longest, newStreak),
110
+ lastActivityDate: now.toISOString(),
111
+ },
112
+ });
113
+
114
+ // Check achievements
115
+ if (!state.achievements || state.achievements.length === 0) {
116
+ return;
117
+ }
118
+
119
+ const updatedAchievements = state.achievements.map((ach: Achievement) => {
120
+ if (ach.isUnlocked) return ach;
121
+
122
+ const progress = updateAchievementProgress(
123
+ ach,
124
+ newTotalTasks,
125
+ newStreak
126
+ );
127
+
128
+ const shouldUnlock = checkAchievementUnlock(
129
+ ach,
130
+ newTotalTasks,
131
+ newStreak
132
+ );
133
+
134
+ if (shouldUnlock) {
135
+ return {
136
+ ...ach,
137
+ isUnlocked: true,
138
+ unlockedAt: new Date().toISOString(),
139
+ progress: 100,
140
+ };
141
+ }
142
+
143
+ return { ...ach, progress };
144
+ });
145
+
146
+ set({ achievements: updatedAchievements });
92
147
  },
93
148
 
94
149
  updateStreak: () => {
@@ -118,8 +173,6 @@ export const useGamificationStore = createStore<GamificationState, GamificationA
118
173
  },
119
174
 
120
175
  checkAchievements: (): Achievement[] => {
121
- if (!currentConfig) return [];
122
-
123
176
  const state = get();
124
177
 
125
178
  if (!state.achievements || state.achievements.length === 0) {
@@ -143,7 +196,7 @@ export const useGamificationStore = createStore<GamificationState, GamificationA
143
196
  state.streak.current
144
197
  );
145
198
 
146
- if (shouldUnlock && !ach.isUnlocked) {
199
+ if (shouldUnlock) {
147
200
  const unlocked = {
148
201
  ...ach,
149
202
  isUnlocked: true,
@@ -78,6 +78,8 @@ export interface GamificationState {
78
78
  streak: StreakState;
79
79
  isLoading: boolean;
80
80
  isInitialized: boolean;
81
+ // Internal config storage (not persisted, kept in memory)
82
+ _config?: GamificationConfig;
81
83
  }
82
84
 
83
85
  // Store Actions
@@ -5,6 +5,11 @@
5
5
 
6
6
  import type { LevelDefinition, LevelState, AchievementDefinition } from "../types";
7
7
 
8
+ // Constants for gamification calculations
9
+ export const DEFAULT_LEVEL_RANGE = 100;
10
+ export const DEFAULT_POINTS_PER_LEVEL = 50;
11
+ export const MAX_PROGRESS = 100;
12
+
8
13
  export const calculateLevel = (
9
14
  points: number,
10
15
  levels: LevelDefinition[]
@@ -16,7 +21,7 @@ export const calculateLevel = (
16
21
  return {
17
22
  currentLevel: 1,
18
23
  currentPoints: points,
19
- pointsToNext: 0,
24
+ pointsToNext: DEFAULT_POINTS_PER_LEVEL,
20
25
  progress: 0,
21
26
  };
22
27
  }
@@ -34,8 +39,8 @@ export const calculateLevel = (
34
39
  const pointsInLevel = points - currentLevelDef.minPoints;
35
40
  const levelRange = nextLevelDef
36
41
  ? nextLevelDef.minPoints - currentLevelDef.minPoints
37
- : 100;
38
- const progress = Math.min(100, (pointsInLevel / levelRange) * 100);
42
+ : DEFAULT_LEVEL_RANGE;
43
+ const progress = Math.min(MAX_PROGRESS, (pointsInLevel / levelRange) * MAX_PROGRESS);
39
44
  const pointsToNext = nextLevelDef
40
45
  ? nextLevelDef.minPoints - points
41
46
  : 0;
@@ -70,7 +75,7 @@ export const updateAchievementProgress = (
70
75
  currentStreak: number
71
76
  ): number => {
72
77
  const value = definition.type === "streak" ? currentStreak : tasksCompleted;
73
- return Math.min(100, (value / definition.threshold) * 100);
78
+ return Math.min(MAX_PROGRESS, (value / definition.threshold) * MAX_PROGRESS);
74
79
  };
75
80
 
76
81
  /**
@@ -3,6 +3,7 @@ import { ViewStyle } from 'react-native';
3
3
  import { LegalConfig } from '../../domain/entities/LegalConfig';
4
4
  import { SettingsItemCard } from '../../../../presentation/components/SettingsItemCard';
5
5
  import { useSettingsNavigation } from '../../../../presentation/navigation/hooks/useSettingsNavigation';
6
+ import { createRouteOrPressHandler } from '../../../../presentation/navigation/utils/navigationHelpers';
6
7
 
7
8
  export interface LegalSectionProps {
8
9
  config?: LegalConfig;
@@ -29,20 +30,14 @@ export const LegalSection: React.FC<LegalSectionProps> = ({
29
30
 
30
31
  const navigation = useSettingsNavigation();
31
32
 
32
- const route = config?.route || config?.defaultRoute || 'Legal';
33
33
  const title = propsTitle || config?.title;
34
34
  const description = propsDescription || config?.description;
35
35
  const sectionTitle = propsSectionTitle;
36
36
 
37
- const handlePress = () => {
38
- if (onPress) {
39
- onPress();
40
- } else if (config?.onPress) {
41
- config.onPress();
42
- } else {
43
- navigation.navigate(route as 'Legal');
44
- }
45
- };
37
+ const handlePress = createRouteOrPressHandler(navigation.navigate, {
38
+ route: config?.route || config?.defaultRoute || 'Legal',
39
+ onPress: onPress || config?.onPress,
40
+ });
46
41
 
47
42
  if (!title) return null;
48
43
 
@@ -19,7 +19,7 @@ export { LanguageSection } from './presentation/components/LanguageSection';
19
19
  export type { LanguageSectionProps, LanguageSectionConfig } from './presentation/components/LanguageSection';
20
20
 
21
21
  // Configuration
22
- export { default as i18n } from './infrastructure/config/i18n';
22
+ export { i18n } from './infrastructure/config/i18n';
23
23
  export { I18nInitializer } from './infrastructure/config/I18nInitializer';
24
24
  export {
25
25
  DEFAULT_LANGUAGE,
@@ -1,6 +1,7 @@
1
1
  import { useLocalization } from '../hooks/useLocalization';
2
2
  import { languageRepository } from '../repository/LanguageRepository';
3
3
  import { useSettingsNavigation } from '../../../../presentation/navigation/hooks/useSettingsNavigation';
4
+ import { createRouteOrPressHandler } from '../../../../presentation/navigation/utils/navigationHelpers';
4
5
 
5
6
 
6
7
  export const useLanguageNavigation = (navigationScreen: string) => {
@@ -8,11 +9,9 @@ export const useLanguageNavigation = (navigationScreen: string) => {
8
9
  const { currentLanguage } = useLocalization();
9
10
  const currentLang = languageRepository.getLanguageByCode(currentLanguage) || languageRepository.getDefaultLanguage();
10
11
 
11
- const navigateToLanguageSelection = () => {
12
- if (navigation && navigationScreen) {
13
- navigation.navigate(navigationScreen as 'LanguageSelection');
14
- }
15
- };
12
+ const navigateToLanguageSelection = createRouteOrPressHandler(navigation.navigate, {
13
+ route: navigationScreen,
14
+ });
16
15
 
17
16
  return { currentLang, navigateToLanguageSelection };
18
17
  };
@@ -32,7 +32,7 @@ export class NamespaceResolver {
32
32
  appNamespaces = Object.keys(langTranslations);
33
33
  }
34
34
  } else {
35
- // If structured by namespace (legacy/simple), keys are namespaces
35
+ // If structured by namespace, keys are namespaces
36
36
  appNamespaces = Object.keys(appTranslations);
37
37
  }
38
38
 
@@ -6,4 +6,4 @@
6
6
 
7
7
  import i18n from 'i18next';
8
8
 
9
- export default i18n;
9
+ export { i18n };
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { useCallback } from 'react';
10
10
  import { useTranslation } from 'react-i18next';
11
- import i18n from '../config/i18n';
11
+ import { i18n } from '../config/i18n';
12
12
  import { devWarn } from '../../../../utils/devUtils';
13
13
 
14
14
  export interface TranslationOptions {
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { storageRepository } from '@umituz/react-native-design-system/storage';
11
- import i18n from '../config/i18n';
11
+ import { i18n } from '../config/i18n';
12
12
  import { languageRepository } from '../repository/LanguageRepository';
13
13
  import { getDeviceLocale } from '../config/languages';
14
14
 
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { storageRepository } from '@umituz/react-native-design-system/storage';
10
- import i18n from '../config/i18n';
10
+ import { i18n } from '../config/i18n';
11
11
  import { languageRepository } from '../repository/LanguageRepository';
12
12
  import { isDev } from '../../../../utils/devUtils';
13
13
 
@@ -7,36 +7,43 @@ export class NotificationScheduler {
7
7
  try {
8
8
  const { title, body, data = {}, trigger, sound = true, badge, categoryIdentifier } = options;
9
9
 
10
- let notificationTrigger: Notifications.NotificationTriggerInput = null;
10
+ let notificationTrigger: Notifications.NotificationTriggerInput;
11
11
 
12
- if (trigger.type === 'date') {
13
- notificationTrigger = {
14
- date: trigger.date,
15
- channelId: categoryIdentifier || 'default',
16
- };
17
- } else if (trigger.type === 'daily') {
18
- notificationTrigger = {
19
- hour: trigger.hour,
20
- minute: trigger.minute,
21
- repeats: true,
22
- channelId: categoryIdentifier || 'reminders',
23
- };
24
- } else if (trigger.type === 'weekly') {
25
- notificationTrigger = {
26
- weekday: trigger.weekday,
27
- hour: trigger.hour,
28
- minute: trigger.minute,
29
- repeats: true,
30
- channelId: categoryIdentifier || 'reminders',
31
- };
32
- } else if (trigger.type === 'monthly') {
33
- notificationTrigger = {
34
- day: trigger.day,
35
- hour: trigger.hour,
36
- minute: trigger.minute,
37
- repeats: true,
38
- channelId: categoryIdentifier || 'reminders',
39
- };
12
+ switch (trigger.type) {
13
+ case 'date':
14
+ notificationTrigger = {
15
+ date: trigger.date,
16
+ channelId: categoryIdentifier || 'default',
17
+ };
18
+ break;
19
+ case 'daily':
20
+ notificationTrigger = {
21
+ hour: trigger.hour,
22
+ minute: trigger.minute,
23
+ repeats: true,
24
+ channelId: categoryIdentifier || 'reminders',
25
+ };
26
+ break;
27
+ case 'weekly':
28
+ notificationTrigger = {
29
+ weekday: trigger.weekday,
30
+ hour: trigger.hour,
31
+ minute: trigger.minute,
32
+ repeats: true,
33
+ channelId: categoryIdentifier || 'reminders',
34
+ };
35
+ break;
36
+ case 'monthly':
37
+ notificationTrigger = {
38
+ day: trigger.day,
39
+ hour: trigger.hour,
40
+ minute: trigger.minute,
41
+ repeats: true,
42
+ channelId: categoryIdentifier || 'reminders',
43
+ };
44
+ break;
45
+ default:
46
+ throw new Error(`Unsupported trigger type: ${(trigger as { type: string }).type}`);
40
47
  }
41
48
 
42
49
  const notificationId = await Notifications.scheduleNotificationAsync({
@@ -1,7 +1,8 @@
1
- import React, { useCallback } from 'react';
1
+ import React from 'react';
2
2
  import type { StyleProp, ViewStyle } from 'react-native';
3
3
  import { SettingsItemCard } from '../../../../presentation/components/SettingsItemCard';
4
4
  import { useSettingsNavigation } from '../../../../presentation/navigation/hooks/useSettingsNavigation';
5
+ import { createRouteOrPressHandler } from '../../../../presentation/navigation/utils/navigationHelpers';
5
6
 
6
7
  export interface NotificationsSectionConfig {
7
8
  route?: string;
@@ -26,14 +27,10 @@ export const NotificationsSection: React.FC<NotificationsSectionProps> = ({
26
27
  }) => {
27
28
  const navigation = useSettingsNavigation();
28
29
 
29
- const handlePress = useCallback(() => {
30
- if (config?.onPress) {
31
- config.onPress();
32
- } else {
33
- const route = config?.route || 'Notifications';
34
- navigation.navigate(route as 'Notifications');
35
- }
36
- }, [config?.route, config?.onPress, navigation]);
30
+ const handlePress = createRouteOrPressHandler(navigation.navigate, {
31
+ route: config?.route || ('Notifications' as const),
32
+ onPress: config?.onPress,
33
+ });
37
34
 
38
35
  const title = config?.title || "";
39
36
  const description = config?.description || "";
@@ -62,13 +62,18 @@ export const useTimePicker = ({
62
62
  }, [onStartTimeChange, onEndTimeChange]);
63
63
 
64
64
  const getPickerDate = useCallback((): Date => {
65
- const date = new Date();
66
65
  if (pickerMode === 'start') {
67
- date.setHours(quietHours.startHour, quietHours.startMinute);
68
- } else if (pickerMode === 'end') {
69
- date.setHours(quietHours.endHour, quietHours.endMinute);
66
+ // Don't mutate - create new Date with hours/minutes
67
+ return new Date(
68
+ new Date().setHours(quietHours.startHour, quietHours.startMinute, 0, 0)
69
+ );
70
70
  }
71
- return date;
71
+ if (pickerMode === 'end') {
72
+ return new Date(
73
+ new Date().setHours(quietHours.endHour, quietHours.endMinute, 0, 0)
74
+ );
75
+ }
76
+ return new Date();
72
77
  }, [pickerMode, quietHours]);
73
78
 
74
79
  return {
@@ -11,26 +11,26 @@ export const useQuietHoursActions = () => {
11
11
  const quietHours = useQuietHours();
12
12
  const { updateQuietHours } = useNotificationStore();
13
13
 
14
- const setQuietHoursEnabled = useCallback(async (enabled: boolean): Promise<void> => {
14
+ const setQuietHoursEnabled = useCallback((enabled: boolean): void => {
15
15
  // Use getState() to avoid stale closure and race conditions
16
16
  const currentQuietHours = useNotificationStore.getState().preferences.quietHours;
17
- await updateQuietHours({ ...currentQuietHours, enabled });
17
+ updateQuietHours({ ...currentQuietHours, enabled });
18
18
  }, [updateQuietHours]);
19
19
 
20
- const setStartTime = useCallback(async (hour: number, minute: number): Promise<void> => {
20
+ const setStartTime = useCallback((hour: number, minute: number): void => {
21
21
  // Use getState() to avoid stale closure and race conditions
22
22
  const currentQuietHours = useNotificationStore.getState().preferences.quietHours;
23
- await updateQuietHours({ ...currentQuietHours, startHour: hour, startMinute: minute });
23
+ updateQuietHours({ ...currentQuietHours, startHour: hour, startMinute: minute });
24
24
  }, [updateQuietHours]);
25
25
 
26
- const setEndTime = useCallback(async (hour: number, minute: number): Promise<void> => {
26
+ const setEndTime = useCallback((hour: number, minute: number): void => {
27
27
  // Use getState() to avoid stale closure and race conditions
28
28
  const currentQuietHours = useNotificationStore.getState().preferences.quietHours;
29
- await updateQuietHours({ ...currentQuietHours, endHour: hour, endMinute: minute });
29
+ updateQuietHours({ ...currentQuietHours, endHour: hour, endMinute: minute });
30
30
  }, [updateQuietHours]);
31
31
 
32
- const setQuietHours = useCallback(async (config: QuietHoursConfig): Promise<void> => {
33
- await updateQuietHours(config);
32
+ const setQuietHours = useCallback((config: QuietHoursConfig): void => {
33
+ updateQuietHours(config);
34
34
  }, [updateQuietHours]);
35
35
 
36
36
  const isInQuietHours = useCallback((): boolean => {
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import React from "react";
9
- import { useNavigation } from "@react-navigation/native";
9
+ import { useAppNavigation } from "@umituz/react-native-design-system/molecules";
10
10
  import { StackNavigator, type StackNavigatorConfig } from "@umituz/react-native-design-system/molecules";
11
11
  import { useNavigationHandlers, useSettingsScreens } from "./hooks";
12
12
  import {
@@ -43,7 +43,7 @@ export const SettingsStackNavigator: React.FC<SettingsStackNavigatorProps> = (pr
43
43
  );
44
44
 
45
45
  // Get navigation for custom sections
46
- const navigation = useNavigation();
46
+ const navigation = useAppNavigation();
47
47
 
48
48
  const screens = useSettingsScreens({
49
49
  ...props,
@@ -51,7 +51,7 @@ export const SettingsStackNavigator: React.FC<SettingsStackNavigatorProps> = (pr
51
51
  legalProps: legalScreenProps,
52
52
  notificationTranslations,
53
53
  quietHoursTranslations,
54
- navigation,
54
+ navigation: navigation as unknown as Record<string, unknown>,
55
55
  });
56
56
 
57
57
  const navigatorConfig: StackNavigatorConfig<SettingsStackParamList> = {
@@ -1,28 +1,32 @@
1
1
  /**
2
- * Typed Settings Navigation Hook
2
+ * Settings Navigation Hook
3
3
  *
4
- * Provides type-safe navigation for Settings stack screens.
5
- * Replaces unsafe `as never` casts throughout the codebase.
4
+ * Provides standardized navigation for Settings stack screens.
5
+ * Uses useAppNavigation from design system for consistency.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { useSettingsNavigation } from '@umituz/react-native-settings/presentation/navigation';
10
+ *
11
+ * function LanguageSelectionScreen() {
12
+ * const navigation = useSettingsNavigation();
13
+ * navigation.navigate('Appearance');
14
+ * navigation.goBack();
15
+ * }
16
+ * ```
6
17
  */
7
18
 
8
- import { useNavigation } from '@react-navigation/native';
9
- import type { StackNavigationProp } from '@react-navigation/stack';
10
- import type { SettingsStackParamList } from '../types';
19
+ import { useAppNavigation } from '@umituz/react-native-design-system/molecules';
11
20
 
12
21
  /**
13
- * Type for Settings navigation prop
22
+ * Navigation result type inferred from useAppNavigation
14
23
  */
15
- export type SettingsNavigationProp = StackNavigationProp<SettingsStackParamList>;
24
+ type SettingsNavigation = ReturnType<typeof useAppNavigation>;
16
25
 
17
26
  /**
18
- * Hook to get typed navigation for Settings screens
19
- *
20
- * @example
21
- * ```typescript
22
- * const navigation = useSettingsNavigation();
23
- * navigation.navigate('LanguageSelection'); // Fully typed!
24
- * ```
27
+ * Hook to get navigation for Settings screens
28
+ * Delegates to useAppNavigation for consistent API
25
29
  */
26
- export const useSettingsNavigation = () => {
27
- return useNavigation<SettingsNavigationProp>();
30
+ export const useSettingsNavigation = (): SettingsNavigation => {
31
+ return useAppNavigation();
28
32
  };
@@ -11,7 +11,7 @@ type NavigateFunction = <RouteName extends keyof SettingsStackParamList>(
11
11
  ) => void;
12
12
 
13
13
  interface RouteOrPressConfig<T extends keyof SettingsStackParamList = keyof SettingsStackParamList> {
14
- route?: T;
14
+ route?: T | string;
15
15
  onPress?: () => void;
16
16
  fallback?: T;
17
17
  }
@@ -26,7 +26,7 @@ export const createRouteOrPressHandler = <T extends keyof SettingsStackParamList
26
26
  } else {
27
27
  const targetRoute = config.route || config.fallback;
28
28
  if (targetRoute) {
29
- navigate(targetRoute);
29
+ navigate(targetRoute as T);
30
30
  }
31
31
  }
32
32
  };
@@ -10,6 +10,26 @@ interface CustomSettingsListProps {
10
10
 
11
11
  const EMPTY_SECTIONS: CustomSettingsSection[] = [];
12
12
 
13
+ // Extract to separate memoized component to prevent unnecessary re-renders when other sections change
14
+ const SettingsItemsList: React.FC<{ items: CustomSettingsSection['items'] }> = React.memo(({ items }) => (
15
+ <>
16
+ {items?.map((item, itemIndex) => (
17
+ <SettingsItemCard
18
+ key={item.id || `item-${itemIndex}`}
19
+ title={item.title}
20
+ description={item.subtitle}
21
+ icon={item.icon}
22
+ onPress={item.onPress}
23
+ rightIcon={item.rightIcon}
24
+ iconBgColor={item.iconBgColor}
25
+ iconColor={item.iconColor}
26
+ />
27
+ ))}
28
+ </>
29
+ ));
30
+
31
+ SettingsItemsList.displayName = "SettingsItemsList";
32
+
13
33
  export const CustomSettingsList: React.FC<CustomSettingsListProps> = ({ customSections = EMPTY_SECTIONS }) => {
14
34
  const sortedSections = useMemo(() => {
15
35
  return Array.from(customSections)
@@ -26,18 +46,9 @@ export const CustomSettingsList: React.FC<CustomSettingsListProps> = ({ customSe
26
46
  title={section.title}
27
47
  >
28
48
  {section.content}
29
- {!section.content && section.items && section.items.length > 0 && section.items.map((item, itemIndex) => (
30
- <SettingsItemCard
31
- key={item.id || `item-${itemIndex}`}
32
- title={item.title}
33
- description={item.subtitle}
34
- icon={item.icon}
35
- onPress={item.onPress}
36
- rightIcon={item.rightIcon}
37
- iconBgColor={item.iconBgColor}
38
- iconColor={item.iconColor}
39
- />
40
- ))}
49
+ {!section.content && section.items && section.items.length > 0 && (
50
+ <SettingsItemsList items={section.items} />
51
+ )}
41
52
  </SettingsSection>
42
53
  ))}
43
54
  </>
@@ -6,6 +6,7 @@ import { SettingsItemCard } from "../../../components/SettingsItemCard";
6
6
  import type { NormalizedConfig } from "../../utils/normalizeConfig";
7
7
  import { SettingsSection } from "../../../components/SettingsSection";
8
8
  import { useSettingsNavigation } from "../../../navigation/hooks/useSettingsNavigation";
9
+ import { createRouteOrPressHandler } from "../../../navigation/utils/navigationHelpers";
9
10
 
10
11
  interface FeatureSettingsSectionProps {
11
12
  normalizedConfig: NormalizedConfig;
@@ -25,14 +26,10 @@ export const FeatureSettingsSection: React.FC<FeatureSettingsSectionProps> = ({
25
26
  const translations = normalizedConfig.translations;
26
27
  const navigation = useSettingsNavigation();
27
28
 
28
- const handleLanguagePress = React.useCallback(() => {
29
- if (normalizedConfig.language.config?.onPress) {
30
- normalizedConfig.language.config.onPress();
31
- } else {
32
- const route = normalizedConfig.language.config?.route || "LanguageSelection";
33
- navigation.navigate(route as 'LanguageSelection');
34
- }
35
- }, [navigation, normalizedConfig.language.config]);
29
+ const handleLanguagePress = createRouteOrPressHandler(navigation.navigate, {
30
+ route: normalizedConfig.language.config?.route || "LanguageSelection",
31
+ onPress: normalizedConfig.language.config?.onPress,
32
+ });
36
33
 
37
34
  const langCode = currentLanguage || "en-US";
38
35
  const currentLanguageData = React.useMemo(() => getLanguageByCode(langCode), [langCode]);
package/src/account.ts DELETED
@@ -1,45 +0,0 @@
1
- /**
2
- * @umituz/react-native-settings/account
3
- *
4
- * NOTE: Auth has been removed from this application.
5
- * This file provides empty exports for compatibility.
6
- *
7
- * Apps that use @umituz/react-native-auth should import directly from that package:
8
- * import { AccountScreen, ProfileSection } from '@umituz/react-native-auth';
9
- */
10
-
11
- // Empty exports
12
- export const AccountScreen: React.ComponentType<Record<string, never>> | null = null;
13
- export const ProfileSection: React.ComponentType<Record<string, never>> | null = null;
14
-
15
- // Stub hooks that return default values
16
- export const useAuth = () => ({
17
- user: null,
18
- loading: false,
19
- isAuthReady: true,
20
- isAnonymous: true,
21
- });
22
-
23
- export const useUserProfile = () => null;
24
-
25
- export const useAuthHandlers = () => ({
26
- handleRatePress: async () => {},
27
- handleSignOut: async () => {},
28
- handleDeleteAccount: async () => {},
29
- handleSignIn: async () => {},
30
- });
31
-
32
- // Empty types
33
- export type AccountScreenConfig = Record<string, never>;
34
-
35
- // Base hook (no auth version)
36
- export { useSettingsScreenConfig } from './presentation/hooks/useSettingsScreenConfig';
37
-
38
- // Re-export types
39
- export type {
40
- UseSettingsScreenConfigParams,
41
- SettingsScreenConfigResult,
42
- SettingsFeatures,
43
- } from './presentation/hooks/useSettingsScreenConfig';
44
-
45
- export type { AccountConfig } from './presentation/navigation/types';