@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.
- package/package.json +15 -6
- package/src/domains/about/presentation/components/AboutSection.tsx +14 -71
- package/src/domains/appearance/application/ports/IAppearanceRepository.ts +8 -0
- package/src/domains/appearance/hooks/index.ts +1 -1
- package/src/domains/appearance/hooks/useAppearance.ts +18 -58
- package/src/domains/appearance/hooks/useAppearanceActions.ts +20 -128
- package/src/domains/appearance/infrastructure/repositories/AppearanceRepository.ts +34 -0
- package/src/domains/appearance/infrastructure/services/AppearanceService.ts +51 -0
- package/src/domains/appearance/presentation/components/AppearanceSection.tsx +2 -2
- package/src/domains/appearance/presentation/hooks/mutations/useAppearanceMutations.ts +36 -0
- package/src/domains/appearance/presentation/hooks/queries/useAppearanceQuery.ts +15 -0
- package/src/domains/disclaimer/presentation/components/DisclaimerModal.tsx +37 -40
- package/src/domains/faqs/presentation/components/FAQSection.tsx +1 -1
- package/src/domains/feedback/presentation/components/FeedbackModal.tsx +11 -15
- package/src/domains/feedback/presentation/components/SupportSection.tsx +2 -2
- package/src/domains/legal/presentation/components/LegalItem.tsx +13 -129
- package/src/index.ts +15 -9
- package/src/infrastructure/repositories/SettingsRepository.ts +105 -0
- package/src/infrastructure/services/SettingsService.ts +47 -0
- package/src/presentation/components/SettingItem.tsx +77 -129
- package/src/presentation/components/SettingsFooter.tsx +9 -25
- package/src/presentation/components/SettingsSection.tsx +9 -20
- package/src/presentation/hooks/mutations/useSettingsMutations.ts +58 -0
- package/src/presentation/hooks/queries/useSettingsQuery.ts +27 -0
- package/src/presentation/hooks/useSettings.ts +45 -0
- package/src/presentation/screens/components/SettingsContent.tsx +20 -247
- package/src/presentation/screens/components/sections/CustomSettingsList.tsx +31 -0
- package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +55 -0
- package/src/presentation/screens/components/sections/IdentitySettingsSection.tsx +43 -0
- package/src/presentation/screens/components/sections/ProfileSectionLoader.tsx +47 -0
- package/src/presentation/screens/components/sections/SupportSettingsSection.tsx +84 -0
- package/src/presentation/screens/hooks/useFeatureDetection.ts +1 -16
- package/src/presentation/screens/types/FeatureConfig.ts +18 -0
- package/src/presentation/screens/types/SettingsConfig.ts +7 -0
- package/src/domains/appearance/infrastructure/services/appearanceService.ts +0 -301
- package/src/domains/appearance/infrastructure/storage/appearanceStorage.ts +0 -120
- package/src/domains/appearance/infrastructure/stores/appearanceStore.ts +0 -132
- package/src/infrastructure/storage/SettingsStore.ts +0 -189
- package/src/infrastructure/storage/__tests__/SettingsStore.test.tsx +0 -302
- package/src/presentation/components/CloudSyncSetting.tsx +0 -58
- /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
|
-
<
|
|
37
|
-
style={
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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 {
|
|
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;
|
|
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
|
-
<
|
|
38
|
-
|
|
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"
|
|
45
|
+
<AtomicText type="headlineSmall" color="textPrimary">
|
|
51
46
|
{title}
|
|
52
47
|
</AtomicText>
|
|
53
48
|
{subtitle && (
|
|
54
|
-
<AtomicText type="bodySmall" style={{
|
|
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.
|
|
56
|
+
style={[styles.closeButton, { backgroundColor: tokens.colors.surfaceVariant }]}
|
|
62
57
|
>
|
|
63
|
-
<AtomicIcon name="close"
|
|
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
|
-
</
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
13
|
+
* Icon name from theme library (Ionicons)
|
|
18
14
|
* If not provided, will use emoji icon
|
|
19
15
|
*/
|
|
20
|
-
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
|
-
|
|
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
|
-
<
|
|
138
|
-
{
|
|
139
|
-
|
|
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
|
-
|
|
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 './
|
|
20
|
+
} from './application/ports/ISettingsRepository';
|
|
21
21
|
|
|
22
22
|
// =============================================================================
|
|
23
|
-
// INFRASTRUCTURE LAYER -
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
} from './
|
|
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
|
+
}
|