@umituz/react-native-settings 4.17.26 → 4.17.30

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 (41) hide show
  1. package/package.json +15 -6
  2. package/src/domains/about/presentation/components/AboutSection.tsx +14 -71
  3. package/src/domains/appearance/application/ports/IAppearanceRepository.ts +8 -0
  4. package/src/domains/appearance/hooks/index.ts +1 -1
  5. package/src/domains/appearance/hooks/useAppearance.ts +18 -58
  6. package/src/domains/appearance/hooks/useAppearanceActions.ts +20 -128
  7. package/src/domains/appearance/infrastructure/repositories/AppearanceRepository.ts +34 -0
  8. package/src/domains/appearance/infrastructure/services/AppearanceService.ts +51 -0
  9. package/src/domains/appearance/presentation/components/AppearanceSection.tsx +2 -2
  10. package/src/domains/appearance/presentation/hooks/mutations/useAppearanceMutations.ts +36 -0
  11. package/src/domains/appearance/presentation/hooks/queries/useAppearanceQuery.ts +15 -0
  12. package/src/domains/disclaimer/presentation/components/DisclaimerModal.tsx +37 -40
  13. package/src/domains/faqs/presentation/components/FAQSection.tsx +1 -1
  14. package/src/domains/feedback/presentation/components/FeedbackModal.tsx +11 -15
  15. package/src/domains/feedback/presentation/components/SupportSection.tsx +2 -2
  16. package/src/domains/legal/presentation/components/LegalItem.tsx +13 -129
  17. package/src/index.ts +15 -9
  18. package/src/infrastructure/repositories/SettingsRepository.ts +105 -0
  19. package/src/infrastructure/services/SettingsService.ts +47 -0
  20. package/src/presentation/components/SettingItem.tsx +77 -129
  21. package/src/presentation/components/SettingsFooter.tsx +9 -25
  22. package/src/presentation/components/SettingsSection.tsx +9 -20
  23. package/src/presentation/hooks/mutations/useSettingsMutations.ts +58 -0
  24. package/src/presentation/hooks/queries/useSettingsQuery.ts +27 -0
  25. package/src/presentation/hooks/useSettings.ts +45 -0
  26. package/src/presentation/screens/components/SettingsContent.tsx +20 -247
  27. package/src/presentation/screens/components/sections/CustomSettingsList.tsx +31 -0
  28. package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +55 -0
  29. package/src/presentation/screens/components/sections/IdentitySettingsSection.tsx +43 -0
  30. package/src/presentation/screens/components/sections/ProfileSectionLoader.tsx +47 -0
  31. package/src/presentation/screens/components/sections/SupportSettingsSection.tsx +84 -0
  32. package/src/presentation/screens/hooks/useFeatureDetection.ts +1 -16
  33. package/src/presentation/screens/types/FeatureConfig.ts +18 -0
  34. package/src/presentation/screens/types/SettingsConfig.ts +7 -0
  35. package/src/domains/appearance/infrastructure/services/appearanceService.ts +0 -301
  36. package/src/domains/appearance/infrastructure/storage/appearanceStorage.ts +0 -120
  37. package/src/domains/appearance/infrastructure/stores/appearanceStore.ts +0 -132
  38. package/src/infrastructure/storage/SettingsStore.ts +0 -189
  39. package/src/infrastructure/storage/__tests__/SettingsStore.test.tsx +0 -302
  40. package/src/presentation/components/CloudSyncSetting.tsx +0 -58
  41. /package/src/{domain/repositories → application/ports}/ISettingsRepository.ts +0 -0
@@ -11,8 +11,7 @@ import {
11
11
  ScrollView,
12
12
  } from 'react-native';
13
13
 
14
- import { useAppDesignTokens } from '@umituz/react-native-design-system';
15
- import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
14
+ import { useAppDesignTokens, AtomicText, AtomicIcon, BaseModal } from '@umituz/react-native-design-system';
16
15
 
17
16
  export interface DisclaimerModalProps {
18
17
  visible: boolean;
@@ -30,56 +29,54 @@ export const DisclaimerModal: React.FC<DisclaimerModalProps> = ({
30
29
  const tokens = useAppDesignTokens();
31
30
  const styles = getStyles(tokens);
32
31
 
33
- if (!visible) return null;
34
-
35
32
  return (
36
- <View
37
- style={[
38
- styles.modalContainer,
39
- { backgroundColor: tokens.colors.backgroundPrimary },
40
- ]}
41
- >
42
- {/* Modal Header */}
43
- <View
44
- style={[
45
- styles.modalHeader,
46
- { borderBottomColor: tokens.colors.borderLight },
47
- ]}
48
- >
49
- <AtomicText type="headlineMedium" color="primary">
50
- {title}
51
- </AtomicText>
52
- <TouchableOpacity
53
- onPress={onClose}
54
- testID="close-disclaimer-modal"
33
+ <BaseModal visible={visible} onClose={onClose}>
34
+ <View style={styles.modalContentWrapper}>
35
+ {/* Modal Header */}
36
+ <View
37
+ style={[
38
+ styles.modalHeader,
39
+ { borderBottomColor: tokens.colors.border },
40
+ ]}
55
41
  >
56
- <AtomicIcon name="x" color="primary" size="md" />
57
- </TouchableOpacity>
58
- </View>
42
+ <AtomicText type="headlineMedium" color="primary">
43
+ {title}
44
+ </AtomicText>
45
+ <TouchableOpacity
46
+ onPress={onClose}
47
+ testID="close-disclaimer-modal"
48
+ >
49
+ <AtomicIcon name="close" color="primary" size="md" />
50
+ </TouchableOpacity>
51
+ </View>
59
52
 
60
- {/* Scrollable Content */}
61
- <ScrollView
62
- style={styles.modalContent}
63
- contentContainerStyle={styles.modalContentContainer}
64
- >
65
- <AtomicText
66
- type="bodyMedium"
67
- color="primary"
68
- style={styles.modalText}
53
+ {/* Scrollable Content */}
54
+ <ScrollView
55
+ style={styles.modalContent}
56
+ contentContainerStyle={styles.modalContentContainer}
69
57
  >
70
- {content}
71
- </AtomicText>
72
- </ScrollView>
73
- </View>
58
+ <AtomicText
59
+ type="bodyMedium"
60
+ color="textPrimary"
61
+ style={styles.modalText}
62
+ >
63
+ {content}
64
+ </AtomicText>
65
+ </ScrollView>
66
+ </View>
67
+ </BaseModal>
74
68
  );
75
69
  };
76
70
 
71
+
77
72
  const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
78
73
  StyleSheet.create({
79
74
  modalContainer: {
80
75
  flex: 1,
81
76
  },
82
-
77
+ modalContentWrapper: {
78
+ flex: 1,
79
+ },
83
80
  modalHeader: {
84
81
  flexDirection: 'row',
85
82
  justifyContent: 'space-between',
@@ -40,7 +40,7 @@ export const FAQSection: React.FC<FAQSectionProps> = ({
40
40
  title: config.title || 'Help & Support',
41
41
  children: renderItem({
42
42
  title: config.description || 'FAQ',
43
- icon: 'help-circle-outline',
43
+ icon: 'help-circle',
44
44
  onPress: config.onPress || (() => console.warn('No FAQ handler')),
45
45
  isLast: true,
46
46
  }),
@@ -4,9 +4,9 @@
4
4
  */
5
5
 
6
6
  import React from "react";
7
- import { Modal, View, StyleSheet, TouchableOpacity, ScrollView, KeyboardAvoidingView, Platform } from "react-native";
7
+ import { View, StyleSheet, TouchableOpacity, ScrollView, KeyboardAvoidingView, Platform } from "react-native";
8
8
  import { SafeAreaView } from "react-native-safe-area-context";
9
- import { useResponsiveDesignTokens, AtomicText, AtomicIcon } from "@umituz/react-native-design-system";
9
+ import { useResponsiveDesignTokens, AtomicText, AtomicIcon, BaseModal } from "@umituz/react-native-design-system";
10
10
  import { FeedbackForm } from "./FeedbackForm";
11
11
  import type { FeedbackType, FeedbackRating } from "../../domain/entities/FeedbackEntity";
12
12
 
@@ -18,7 +18,7 @@ export interface FeedbackModalProps {
18
18
  isSubmitting?: boolean;
19
19
  title?: string;
20
20
  subtitle?: string;
21
- texts: any; // Type should ideally be shared or imported
21
+ texts: any;
22
22
  }
23
23
 
24
24
  export const FeedbackModal: React.FC<FeedbackModalProps> = ({
@@ -34,33 +34,28 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
34
34
  const tokens = useResponsiveDesignTokens();
35
35
 
36
36
  return (
37
- <Modal
38
- visible={visible}
39
- animationType="slide"
40
- presentationStyle="pageSheet"
41
- onRequestClose={onClose}
42
- >
43
- <SafeAreaView style={[styles.safeArea, { backgroundColor: tokens.colors.backgroundPrimary }]}>
37
+ <BaseModal visible={visible} onClose={onClose}>
38
+ <SafeAreaView style={styles.safeArea}>
44
39
  <KeyboardAvoidingView
45
40
  behavior={Platform.OS === "ios" ? "padding" : "height"}
46
41
  style={styles.keyboardView}
47
42
  >
48
43
  <View style={[styles.header, { borderBottomColor: tokens.colors.border }]}>
49
44
  <View style={styles.headerText}>
50
- <AtomicText type="headlineSmall" style={{ color: tokens.colors.textPrimary }}>
45
+ <AtomicText type="headlineSmall" color="textPrimary">
51
46
  {title}
52
47
  </AtomicText>
53
48
  {subtitle && (
54
- <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary, marginTop: 4 }}>
49
+ <AtomicText type="bodySmall" color="textSecondary" style={{ marginTop: 4 }}>
55
50
  {subtitle}
56
51
  </AtomicText>
57
52
  )}
58
53
  </View>
59
54
  <TouchableOpacity
60
55
  onPress={onClose}
61
- style={[styles.closeButton, { backgroundColor: tokens.colors.surface }]}
56
+ style={[styles.closeButton, { backgroundColor: tokens.colors.surfaceVariant }]}
62
57
  >
63
- <AtomicIcon name="close" customSize={20} customColor={tokens.colors.textPrimary} />
58
+ <AtomicIcon name="close" size="sm" color="onSurface" />
64
59
  </TouchableOpacity>
65
60
  </View>
66
61
 
@@ -77,10 +72,11 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
77
72
  </ScrollView>
78
73
  </KeyboardAvoidingView>
79
74
  </SafeAreaView>
80
- </Modal>
75
+ </BaseModal>
81
76
  );
82
77
  };
83
78
 
79
+
84
80
  const styles = StyleSheet.create({
85
81
  safeArea: {
86
82
  flex: 1,
@@ -122,14 +122,14 @@ export const SupportSection: React.FC<SupportSectionProps> = ({
122
122
  <>
123
123
  {showFeedback && feedbackConfig.config?.description && renderItem({
124
124
  title: feedbackConfig.config.description,
125
- icon: "mail-outline",
125
+ icon: "mail",
126
126
  onPress: () => setModalVisible(true),
127
127
  isLast: !showRating
128
128
  })}
129
129
 
130
130
  {showRating && ratingConfig.config?.description && renderItem({
131
131
  title: ratingConfig.config.description,
132
- icon: "star-outline",
132
+ icon: "star",
133
133
  onPress: handleRateApp,
134
134
  isLast: true
135
135
  })}
@@ -6,18 +6,14 @@
6
6
  */
7
7
 
8
8
  import React from "react";
9
- import { View, StyleSheet, TouchableOpacity } from "react-native";
10
- import { useResponsiveDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
11
- import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system";
12
- import type { IconName } from "@umituz/react-native-design-system";
13
- import { StyleCacheService } from "../../domain/services/StyleCacheService";
9
+ import { ListItem } from "@umituz/react-native-design-system";
14
10
 
15
11
  export interface LegalItemProps {
16
12
  /**
17
- * Icon name from Lucide library (e.g., "Shield", "FileText", "ScrollText")
13
+ * Icon name from theme library (Ionicons)
18
14
  * If not provided, will use emoji icon
19
15
  */
20
- iconName?: IconName;
16
+ iconName?: string;
21
17
  /**
22
18
  * Icon emoji or text (fallback if iconName not provided)
23
19
  */
@@ -48,130 +44,18 @@ export const LegalItem: React.FC<LegalItemProps> = React.memo(({
48
44
  onPress,
49
45
  testID,
50
46
  }) => {
51
- const tokens = useResponsiveDesignTokens();
52
-
53
- // Memoize styles to prevent recreation on every render
54
- const styles = React.useMemo(() => {
55
- const cacheKey = StyleCacheService.createTokenCacheKey(tokens);
56
- return StyleCacheService.getCachedStyles(
57
- 'LegalItem',
58
- cacheKey,
59
- () => createLegalItemStyles(tokens)
60
- );
61
- }, [tokens]);
62
-
63
- // Memoize icon rendering to prevent unnecessary re-renders
64
- const renderIcon = React.useCallback(() => {
65
- if (iconName) {
66
- return (
67
- <AtomicIcon
68
- name={iconName}
69
- size="md"
70
- color="info"
71
- />
72
- );
73
- }
74
- if (icon) {
75
- return (
76
- <AtomicText type="bodyLarge" color="info">
77
- {icon}
78
- </AtomicText>
79
- );
80
- }
81
- return null;
82
- }, [iconName, icon]);
83
-
84
- // Memoize icon container style to prevent object creation
85
- const iconContainerStyle = React.useMemo(() => [
86
- styles.iconContainer,
87
- { backgroundColor: tokens.colors.info + "20" }
88
- ], [styles.iconContainer, tokens.colors.info]);
89
-
90
- // Memoize content to prevent unnecessary re-renders
91
- const content = React.useMemo(() => (
92
- <View style={styles.itemContent}>
93
- <View style={styles.itemLeft}>
94
- <View style={iconContainerStyle}>
95
- {renderIcon()}
96
- </View>
97
- <View style={styles.itemText}>
98
- <AtomicText type="bodyLarge" color="textPrimary">
99
- {title}
100
- </AtomicText>
101
- {description && (
102
- <AtomicText
103
- type="bodySmall"
104
- color="textSecondary"
105
- style={styles.itemDescription}
106
- >
107
- {description}
108
- </AtomicText>
109
- )}
110
- </View>
111
- </View>
112
- {onPress && (
113
- <AtomicText type="bodyMedium" color="textSecondary">›</AtomicText>
114
- )}
115
- </View>
116
- ), [styles.itemContent, styles.itemLeft, styles.itemText, styles.itemDescription, iconContainerStyle, renderIcon, title, description, onPress]);
117
-
118
- // Memoize press handler to prevent child re-renders
119
- const handlePress = React.useCallback(() => {
120
- onPress?.();
121
- }, [onPress]);
122
-
123
- if (onPress) {
124
- return (
125
- <TouchableOpacity
126
- style={styles.itemContainer}
127
- onPress={handlePress}
128
- testID={testID}
129
- activeOpacity={0.7}
130
- >
131
- {content}
132
- </TouchableOpacity>
133
- );
134
- }
47
+ // Use iconName if provided, otherwise fallback to default
48
+ const finalIcon = iconName || icon || "shield-checkmark";
135
49
 
136
50
  return (
137
- <View style={styles.itemContainer} testID={testID}>
138
- {content}
139
- </View>
51
+ <ListItem
52
+ title={title}
53
+ subtitle={description}
54
+ leftIcon={finalIcon}
55
+ rightIcon={onPress ? "chevron-forward" : undefined}
56
+ onPress={onPress}
57
+ />
140
58
  );
141
59
  });
142
60
 
143
- const createLegalItemStyles = (tokens: DesignTokens) => {
144
- return StyleSheet.create({
145
- itemContainer: {
146
- marginBottom: tokens.spacing.xs,
147
- },
148
- itemContent: {
149
- flexDirection: "row",
150
- alignItems: "center",
151
- justifyContent: "space-between",
152
- paddingHorizontal: tokens.spacing.md,
153
- paddingVertical: tokens.spacing.md,
154
- minHeight: 64,
155
- },
156
- itemLeft: {
157
- flexDirection: "row",
158
- alignItems: "center",
159
- flex: 1,
160
- },
161
- iconContainer: {
162
- width: 44,
163
- height: 44,
164
- borderRadius: 22,
165
- alignItems: "center",
166
- justifyContent: "center",
167
- marginRight: tokens.spacing.md,
168
- },
169
- itemText: {
170
- flex: 1,
171
- },
172
- itemDescription: {
173
- marginTop: tokens.spacing.xs,
174
- },
175
- });
176
- };
177
-
61
+ LegalItem.displayName = "LegalItem";
package/src/index.ts CHANGED
@@ -17,16 +17,26 @@ export type {
17
17
  UserSettings,
18
18
  SettingsError,
19
19
  SettingsResult,
20
- } from './domain/repositories/ISettingsRepository';
20
+ } from './application/ports/ISettingsRepository';
21
21
 
22
22
  // =============================================================================
23
- // INFRASTRUCTURE LAYER - Storage
23
+ // INFRASTRUCTURE LAYER - Services
24
24
  // =============================================================================
25
25
 
26
+ export { getSettingsService } from './infrastructure/services/SettingsService';
27
+ export { SettingsRepository } from './infrastructure/repositories/SettingsRepository';
28
+
29
+ // =============================================================================
30
+ // PRESENTATION LAYER - Hooks
31
+ // =============================================================================
32
+
33
+ export { useSettings } from './presentation/hooks/useSettings';
34
+ export { useSettingsQuery } from './presentation/hooks/queries/useSettingsQuery';
26
35
  export {
27
- useSettingsStore,
28
- useSettings,
29
- } from './infrastructure/storage/SettingsStore';
36
+ useUpdateSettingsMutation,
37
+ useResetSettingsMutation
38
+ } from './presentation/hooks/mutations/useSettingsMutations';
39
+
30
40
 
31
41
  // =============================================================================
32
42
  // PRESENTATION LAYER - Screens
@@ -71,9 +81,6 @@ export type { SettingsFooterProps } from './presentation/components/SettingsFoot
71
81
 
72
82
  export { SettingsErrorBoundary } from './presentation/components/SettingsErrorBoundary';
73
83
 
74
- export { CloudSyncSetting } from './presentation/components/CloudSyncSetting';
75
- export type { CloudSyncSettingProps } from './presentation/components/CloudSyncSetting';
76
-
77
84
  export { StorageClearSetting } from './presentation/components/StorageClearSetting';
78
85
  export type { StorageClearSettingProps } from './presentation/components/StorageClearSetting';
79
86
 
@@ -118,4 +125,3 @@ export type { OnboardingResetSettingProps } from '@umituz/react-native-onboardin
118
125
  export { createSentryTestSetting } from '@umituz/react-native-sentry';
119
126
  // @ts-ignore - Re-exporting from peer dependency
120
127
  export type { SentryTestSettingProps } from '@umituz/react-native-sentry';
121
-
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Settings Repository Implementation
3
+ *
4
+ * Handles data access for user settings using @umituz/react-native-storage
5
+ */
6
+
7
+ import { storageRepository, StorageKey, createUserKey } from '@umituz/react-native-storage';
8
+ import type { ISettingsRepository, UserSettings, SettingsResult } from '../../application/ports/ISettingsRepository';
9
+
10
+ export class SettingsRepository implements ISettingsRepository {
11
+ private readonly defaultSettings: (userId: string) => UserSettings = (userId: string) => ({
12
+ userId,
13
+ theme: 'auto',
14
+ language: 'en-US',
15
+ notificationsEnabled: true,
16
+ emailNotifications: true,
17
+ pushNotifications: true,
18
+ soundEnabled: true,
19
+ vibrationEnabled: true,
20
+ privacyMode: false,
21
+ disclaimerAccepted: false,
22
+ updatedAt: new Date(),
23
+ });
24
+
25
+ async getSettings(userId: string): Promise<SettingsResult<UserSettings>> {
26
+ try {
27
+ const storageKey = createUserKey(StorageKey.USER_PREFERENCES, userId);
28
+ const defaults = this.defaultSettings(userId);
29
+ const result = await storageRepository.getItem<UserSettings>(storageKey, defaults);
30
+
31
+ if (!result.success) {
32
+ // If not found, save defaults and return them
33
+ await this.saveSettings(defaults);
34
+ return { success: true, data: defaults };
35
+ }
36
+
37
+ return {
38
+ success: true,
39
+ data: result.data || defaults,
40
+ };
41
+ } catch (error) {
42
+ return {
43
+ success: false,
44
+ error: {
45
+ code: 'GET_SETTINGS_FAILED',
46
+ message: error instanceof Error ? error.message : 'Unknown error',
47
+ },
48
+ };
49
+ }
50
+ }
51
+
52
+ async saveSettings(settings: UserSettings): Promise<SettingsResult<void>> {
53
+ try {
54
+ const storageKey = createUserKey(StorageKey.USER_PREFERENCES, settings.userId);
55
+ const result = await storageRepository.setItem(storageKey, settings);
56
+
57
+ if (!result.success) {
58
+ return {
59
+ success: false,
60
+ error: {
61
+ code: 'SAVE_SETTINGS_FAILED',
62
+ message: 'Failed to save settings to storage',
63
+ },
64
+ };
65
+ }
66
+
67
+ return { success: true };
68
+ } catch (error) {
69
+ return {
70
+ success: false,
71
+ error: {
72
+ code: 'SAVE_SETTINGS_FAILED',
73
+ message: error instanceof Error ? error.message : 'Unknown error',
74
+ },
75
+ };
76
+ }
77
+ }
78
+
79
+ async deleteSettings(userId: string): Promise<SettingsResult<void>> {
80
+ try {
81
+ const storageKey = createUserKey(StorageKey.USER_PREFERENCES, userId);
82
+ const result = await storageRepository.removeItem(storageKey);
83
+
84
+ if (!result.success) {
85
+ return {
86
+ success: false,
87
+ error: {
88
+ code: 'DELETE_SETTINGS_FAILED',
89
+ message: 'Failed to delete settings from storage',
90
+ },
91
+ };
92
+ }
93
+
94
+ return { success: true };
95
+ } catch (error) {
96
+ return {
97
+ success: false,
98
+ error: {
99
+ code: 'DELETE_SETTINGS_FAILED',
100
+ message: error instanceof Error ? error.message : 'Unknown error',
101
+ },
102
+ };
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Settings Service
3
+ *
4
+ * Orchestrates settings operations using SettingsRepository
5
+ */
6
+
7
+ import { SettingsRepository } from '../repositories/SettingsRepository';
8
+ import type { UserSettings, SettingsResult } from '../../application/ports/ISettingsRepository';
9
+
10
+ export class SettingsService {
11
+ private repository: SettingsRepository;
12
+ private initialized: boolean = false;
13
+
14
+ constructor() {
15
+ this.repository = new SettingsRepository();
16
+ }
17
+
18
+ async initialize(): Promise<void> {
19
+ if (this.initialized) return;
20
+ this.initialized = true;
21
+ }
22
+
23
+ async getSettings(userId: string): Promise<SettingsResult<UserSettings>> {
24
+ return this.repository.getSettings(userId);
25
+ }
26
+
27
+ async saveSettings(settings: UserSettings): Promise<SettingsResult<void>> {
28
+ return this.repository.saveSettings(settings);
29
+ }
30
+
31
+ async resetSettings(userId: string): Promise<SettingsResult<void>> {
32
+ // Repository getSettings handles returning defaults if not found,
33
+ // but here we want to explicitly reset.
34
+ // We can just delete and get again.
35
+ await this.repository.deleteSettings(userId);
36
+ return this.repository.getSettings(userId) as unknown as SettingsResult<void>;
37
+ }
38
+ }
39
+
40
+ let settingsServiceInstance: SettingsService | null = null;
41
+
42
+ export function getSettingsService(): SettingsService {
43
+ if (!settingsServiceInstance) {
44
+ settingsServiceInstance = new SettingsService();
45
+ }
46
+ return settingsServiceInstance;
47
+ }