@umituz/react-native-settings 5.4.9 → 5.4.11

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 (31) hide show
  1. package/package.json +1 -1
  2. package/src/core/base/BaseService.ts +141 -0
  3. package/src/core/index.ts +60 -0
  4. package/src/core/patterns/Modal/ModalConfig.ts +282 -0
  5. package/src/core/patterns/Modal/useModalState.ts +128 -0
  6. package/src/core/patterns/Screen/ScreenConfig.ts +375 -0
  7. package/src/core/patterns/Screen/useScreenData.ts +201 -0
  8. package/src/core/utils/logger.ts +138 -0
  9. package/src/core/utils/validators.ts +203 -0
  10. package/src/domains/disclaimer/index.ts +0 -3
  11. package/src/domains/disclaimer/presentation/components/DisclaimerSetting.tsx +18 -43
  12. package/src/domains/disclaimer/presentation/screens/DisclaimerScreen.tsx +42 -92
  13. package/src/domains/feedback/index.ts +2 -1
  14. package/src/domains/feedback/presentation/components/SupportSection.tsx +16 -43
  15. package/src/domains/feedback/presentation/screens/FeatureRequestScreen.tsx +4 -4
  16. package/src/domains/feedback/presentation/screens/FeedbackScreen.tsx +75 -0
  17. package/src/domains/notifications/infrastructure/services/NotificationService.ts +16 -13
  18. package/src/domains/rating/application/services/RatingService.ts +115 -79
  19. package/src/domains/rating/index.ts +3 -3
  20. package/src/domains/rating/presentation/hooks/useAppRating.tsx +42 -65
  21. package/src/domains/rating/presentation/screens/RatingPromptScreen.tsx +162 -0
  22. package/src/index.ts +12 -0
  23. package/src/infrastructure/services/SettingsService.ts +23 -19
  24. package/src/presentation/components/GenericModal.tsx +208 -0
  25. package/src/presentation/components/GenericScreen.tsx +273 -0
  26. package/src/presentation/components/index.ts +27 -0
  27. package/src/presentation/navigation/hooks/useSettingsScreens.ts +26 -1
  28. package/src/presentation/navigation/types.ts +6 -0
  29. package/src/domains/disclaimer/presentation/components/DisclaimerModal.tsx +0 -103
  30. package/src/domains/feedback/presentation/components/FeedbackModal.tsx +0 -99
  31. package/src/domains/rating/presentation/components/RatingPromptModal.tsx +0 -152
@@ -1,37 +1,41 @@
1
1
  /**
2
2
  * Settings Service
3
3
  *
4
- * Orchestrates settings operations using SettingsRepository
4
+ * Orchestrates settings operations using SettingsRepository.
5
+ * Refactored to extend BaseService for consistent error handling.
5
6
  */
6
7
 
7
8
  import { SettingsRepository } from '../repositories/SettingsRepository';
9
+ import { BaseService } from '../../core/base/BaseService';
8
10
  import type { UserSettings, SettingsResult } from '../../application/ports/ISettingsRepository';
9
11
 
10
- export class SettingsService {
11
- private repository: SettingsRepository;
12
+ export class SettingsService extends BaseService {
13
+ protected serviceName = 'SettingsService';
14
+ private repository: SettingsRepository;
12
15
 
13
- constructor() {
14
- this.repository = new SettingsRepository();
15
- }
16
+ constructor() {
17
+ super();
18
+ this.repository = new SettingsRepository();
19
+ }
16
20
 
17
- async getSettings(userId: string): Promise<SettingsResult<UserSettings>> {
18
- return this.repository.getSettings(userId);
19
- }
21
+ async getSettings(userId: string): Promise<SettingsResult<UserSettings>> {
22
+ return await this.repository.getSettings(userId);
23
+ }
20
24
 
21
- async saveSettings(settings: UserSettings): Promise<SettingsResult<void>> {
22
- return this.repository.saveSettings(settings);
23
- }
25
+ async saveSettings(settings: UserSettings): Promise<SettingsResult<void>> {
26
+ return await this.repository.saveSettings(settings);
27
+ }
24
28
 
25
- async resetSettings(userId: string): Promise<SettingsResult<void>> {
26
- return this.repository.deleteSettings(userId);
27
- }
29
+ async resetSettings(userId: string): Promise<SettingsResult<void>> {
30
+ return await this.repository.deleteSettings(userId);
31
+ }
28
32
  }
29
33
 
30
34
  let settingsServiceInstance: SettingsService | null = null;
31
35
 
32
36
  export function getSettingsService(): SettingsService {
33
- if (!settingsServiceInstance) {
34
- settingsServiceInstance = new SettingsService();
35
- }
36
- return settingsServiceInstance;
37
+ if (!settingsServiceInstance) {
38
+ settingsServiceInstance = new SettingsService();
39
+ }
40
+ return settingsServiceInstance;
37
41
  }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * GenericModal Component
3
+ *
4
+ * Universal modal component that works with ModalConfig.
5
+ * Replaces all custom modal implementations.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const modal = useModalState();
10
+ *
11
+ * return (
12
+ * <>
13
+ * <Button onPress={() => modal.show(ModalPresets.confirm(...))} />
14
+ * <GenericModal state={modal} />
15
+ * </>
16
+ * );
17
+ * ```
18
+ */
19
+
20
+ import React, { useMemo } from 'react';
21
+ import { View, StyleSheet } from 'react-native';
22
+ import { BaseModal } from '@umituz/react-native-design-system/molecules';
23
+ import { AtomicText, AtomicButton, AtomicIcon } from '@umituz/react-native-design-system/atoms';
24
+ import { useAppDesignTokens } from '@umituz/react-native-design-system/theme';
25
+ import { useResponsive } from '@umituz/react-native-design-system/responsive';
26
+ import type { ModalState } from '../../core/patterns/Modal/ModalConfig';
27
+
28
+ export interface GenericModalProps {
29
+ /**
30
+ * Modal state from useModalState hook
31
+ */
32
+ state: ModalState;
33
+
34
+ /**
35
+ * Custom container style
36
+ */
37
+ containerStyle?: object;
38
+
39
+ /**
40
+ * Test ID for E2E testing
41
+ */
42
+ testID?: string;
43
+ }
44
+
45
+ export const GenericModal: React.FC<GenericModalProps> = ({
46
+ state,
47
+ containerStyle,
48
+ testID = 'generic-modal',
49
+ }) => {
50
+ const tokens = useAppDesignTokens();
51
+ const responsive = useResponsive();
52
+ const { visible, config } = state;
53
+
54
+ const styles = useMemo(() => getStyles(tokens), [tokens]);
55
+
56
+ if (!config) {
57
+ return null;
58
+ }
59
+
60
+ const {
61
+ title,
62
+ subtitle,
63
+ message,
64
+ icon,
65
+ iconColor,
66
+ header,
67
+ body,
68
+ footer,
69
+ actions = [],
70
+ dismissible = true,
71
+ maxWidth,
72
+ } = config;
73
+
74
+ const handleDismiss = () => {
75
+ if (dismissible) {
76
+ state.hide();
77
+ }
78
+ };
79
+
80
+ const modalWidth = maxWidth ?? responsive.maxContentWidth * 0.9;
81
+
82
+ return (
83
+ <BaseModal
84
+ visible={visible}
85
+ onClose={handleDismiss}
86
+ >
87
+ <View
88
+ style={[
89
+ styles.container,
90
+ {
91
+ backgroundColor: tokens.colors.surface,
92
+ borderRadius: tokens.borders.radius.xl,
93
+ padding: tokens.spacing.lg,
94
+ maxWidth: modalWidth,
95
+ width: '90%',
96
+ },
97
+ containerStyle,
98
+ ]}
99
+ testID={testID}
100
+ >
101
+ {/* Custom Header */}
102
+ {header ? (
103
+ header
104
+ ) : (
105
+ <>
106
+ {/* Icon */}
107
+ {icon && (
108
+ <View style={styles.iconContainer}>
109
+ <AtomicIcon
110
+ name={icon}
111
+ size="xl"
112
+ color={iconColor as any || 'primary'}
113
+ />
114
+ </View>
115
+ )}
116
+
117
+ {/* Title and Subtitle */}
118
+ {title && (
119
+ <AtomicText
120
+ type="headlineMedium"
121
+ color="onSurface"
122
+ style={[styles.title, { marginBottom: subtitle ? tokens.spacing.xs : tokens.spacing.md }]}
123
+ >
124
+ {title}
125
+ </AtomicText>
126
+ )}
127
+
128
+ {subtitle && (
129
+ <AtomicText
130
+ type="bodyMedium"
131
+ color="onSurfaceVariant"
132
+ style={[styles.subtitle, { marginBottom: tokens.spacing.md }]}
133
+ >
134
+ {subtitle}
135
+ </AtomicText>
136
+ )}
137
+ </>
138
+ )}
139
+
140
+ {/* Custom Body or Message */}
141
+ {body ? (
142
+ body
143
+ ) : message && (
144
+ <AtomicText
145
+ type="bodyMedium"
146
+ color="onSurfaceVariant"
147
+ style={[styles.message, { marginBottom: actions.length > 0 ? tokens.spacing.lg : 0 }]}
148
+ >
149
+ {message}
150
+ </AtomicText>
151
+ )}
152
+
153
+ {/* Custom Footer or Actions */}
154
+ {footer ? (
155
+ footer
156
+ ) : actions.length > 0 && (
157
+ <View style={[styles.buttonContainer, { gap: tokens.spacing.sm }]}>
158
+ {actions.map((action, index) => (
159
+ <AtomicButton
160
+ key={index}
161
+ variant={action.variant || 'primary'}
162
+ onPress={async () => {
163
+ await action.onPress();
164
+ if (dismissible) {
165
+ state.hide();
166
+ }
167
+ }}
168
+ disabled={action.disabled}
169
+ loading={action.loading}
170
+ style={styles.button}
171
+ testID={action.testID}
172
+ >
173
+ {action.label}
174
+ </AtomicButton>
175
+ ))}
176
+ </View>
177
+ )}
178
+ </View>
179
+ </BaseModal>
180
+ );
181
+ };
182
+
183
+ const getStyles = (_tokens: ReturnType<typeof useAppDesignTokens>) =>
184
+ StyleSheet.create({
185
+ container: {
186
+ alignItems: 'center',
187
+ },
188
+ iconContainer: {
189
+ marginBottom: 16,
190
+ },
191
+ title: {
192
+ textAlign: 'center',
193
+ },
194
+ subtitle: {
195
+ textAlign: 'center',
196
+ },
197
+ message: {
198
+ textAlign: 'center',
199
+ lineHeight: 24,
200
+ },
201
+ buttonContainer: {
202
+ width: '100%',
203
+ marginTop: 16,
204
+ },
205
+ button: {
206
+ width: '100%',
207
+ },
208
+ });
@@ -0,0 +1,273 @@
1
+ /**
2
+ * GenericScreen Component
3
+ *
4
+ * Universal screen component that works with ScreenConfig.
5
+ * Handles loading, error, and empty states automatically.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const screenData = useScreenData({ fetch: async () => await api.getData() });
10
+ *
11
+ * return (
12
+ * <GenericScreen
13
+ * data={screenData}
14
+ * config={ScreenPresets.default}
15
+ * >
16
+ * <MyContent data={screenData.data} />
17
+ * </GenericScreen>
18
+ * );
19
+ * ```
20
+ */
21
+
22
+ import React, { useMemo } from 'react';
23
+ import { View, StyleSheet } from 'react-native';
24
+ import { ScreenLayout } from '@umituz/react-native-design-system/layouts';
25
+ import {
26
+ NavigationHeader,
27
+ useAppNavigation,
28
+ } from '@umituz/react-native-design-system/molecules';
29
+ import {
30
+ AtomicText,
31
+ AtomicButton,
32
+ AtomicSpinner,
33
+ AtomicIcon,
34
+ } from '@umituz/react-native-design-system/atoms';
35
+ import { useAppDesignTokens } from '@umituz/react-native-design-system/theme';
36
+ import type {
37
+ ScreenConfig,
38
+ ScreenData,
39
+ } from '../../core/patterns/Screen/ScreenConfig';
40
+
41
+ export interface GenericScreenProps<T> {
42
+ /**
43
+ * Screen data from useScreenData hook
44
+ */
45
+ data: ScreenData<T>;
46
+
47
+ /**
48
+ * Screen configuration
49
+ */
50
+ config?: ScreenConfig;
51
+
52
+ /**
53
+ * Content component (renders when data is loaded)
54
+ */
55
+ children: React.ReactNode | ((data: T) => React.ReactNode);
56
+
57
+ /**
58
+ * Test ID for E2E testing
59
+ */
60
+ testID?: string;
61
+ }
62
+
63
+ export function GenericScreen<T>({
64
+ data,
65
+ config = {},
66
+ children,
67
+ testID = 'generic-screen',
68
+ }: GenericScreenProps<T>) {
69
+ const tokens = useAppDesignTokens();
70
+ const navigation = useAppNavigation();
71
+ const styles = useMemo(() => getStyles(tokens), [tokens]);
72
+
73
+ const {
74
+ header,
75
+ loading = {},
76
+ error = {},
77
+ empty,
78
+ layout = {},
79
+ } = config;
80
+
81
+ // Header configuration
82
+ const showBackButton = header?.showBackButton !== false;
83
+ const onBackPress = header?.onBackPress || (() => navigation.goBack());
84
+
85
+ const navigationHeader = header?.custom ? (
86
+ header.custom
87
+ ) : header?.title || showBackButton ? (
88
+ <NavigationHeader
89
+ title={header?.title || ''}
90
+ onBackPress={showBackButton ? onBackPress : undefined}
91
+ />
92
+ ) : undefined;
93
+
94
+ // Loading state
95
+ if (data.loading) {
96
+ return (
97
+ <ScreenLayout
98
+ edges={layout.edges || ['top', 'bottom', 'left', 'right']}
99
+ header={navigationHeader}
100
+ scrollable={layout.scrollable}
101
+ keyboardAvoiding={layout.keyboardAvoiding}
102
+ hideScrollIndicator={layout.hideScrollIndicator}
103
+ contentContainerStyle={layout.contentContainerStyle}
104
+ testID={testID}
105
+ >
106
+ {loading.custom ? (
107
+ loading.custom
108
+ ) : (
109
+ <View style={styles.centerContainer}>
110
+ <AtomicSpinner
111
+ size={loading.size || 'lg'}
112
+ fullContainer={loading.fullContainer !== false}
113
+ />
114
+ {loading.message && (
115
+ <AtomicText
116
+ type="bodyMedium"
117
+ color="onSurfaceVariant"
118
+ style={styles.stateMessage}
119
+ >
120
+ {loading.message}
121
+ </AtomicText>
122
+ )}
123
+ </View>
124
+ )}
125
+ </ScreenLayout>
126
+ );
127
+ }
128
+
129
+ // Error state
130
+ if (data.error) {
131
+ return (
132
+ <ScreenLayout
133
+ edges={layout.edges || ['top', 'bottom', 'left', 'right']}
134
+ header={navigationHeader}
135
+ scrollable={layout.scrollable}
136
+ keyboardAvoiding={layout.keyboardAvoiding}
137
+ hideScrollIndicator={layout.hideScrollIndicator}
138
+ contentContainerStyle={layout.contentContainerStyle}
139
+ testID={testID}
140
+ >
141
+ {error.custom ? (
142
+ error.custom
143
+ ) : (
144
+ <View style={styles.centerContainer}>
145
+ <AtomicIcon
146
+ name={error.icon || 'error'}
147
+ size="xl"
148
+ color="error"
149
+ style={styles.stateIcon}
150
+ />
151
+ <AtomicText
152
+ type="headlineMedium"
153
+ color="onSurface"
154
+ style={styles.stateTitle}
155
+ >
156
+ {error.prefix || 'Error'}
157
+ </AtomicText>
158
+ <AtomicText
159
+ type="bodyMedium"
160
+ color="onSurfaceVariant"
161
+ style={styles.stateMessage}
162
+ >
163
+ {error.message || data.error}
164
+ </AtomicText>
165
+ {error.showRetry && (
166
+ <AtomicButton
167
+ variant="outline"
168
+ onPress={error.onRetry || data.refresh}
169
+ style={styles.retryButton}
170
+ >
171
+ {error.retryText || 'Retry'}
172
+ </AtomicButton>
173
+ )}
174
+ </View>
175
+ )}
176
+ </ScreenLayout>
177
+ );
178
+ }
179
+
180
+ // Empty state
181
+ if (!data.data && empty) {
182
+ return (
183
+ <ScreenLayout
184
+ edges={layout.edges || ['top', 'bottom', 'left', 'right']}
185
+ header={navigationHeader}
186
+ scrollable={layout.scrollable}
187
+ keyboardAvoiding={layout.keyboardAvoiding}
188
+ hideScrollIndicator={layout.hideScrollIndicator}
189
+ contentContainerStyle={layout.contentContainerStyle}
190
+ testID={testID}
191
+ >
192
+ {empty.custom ? (
193
+ empty.custom
194
+ ) : (
195
+ <View style={styles.centerContainer}>
196
+ {empty.icon && (
197
+ <AtomicIcon
198
+ name={empty.icon}
199
+ size="xl"
200
+ color="onSurfaceVariant"
201
+ style={styles.stateIcon}
202
+ />
203
+ )}
204
+ <AtomicText
205
+ type="headlineMedium"
206
+ color="onSurface"
207
+ style={styles.stateTitle}
208
+ >
209
+ {empty.message || 'No Data'}
210
+ </AtomicText>
211
+ {empty.description && (
212
+ <AtomicText
213
+ type="bodyMedium"
214
+ color="onSurfaceVariant"
215
+ style={styles.stateMessage}
216
+ >
217
+ {empty.description}
218
+ </AtomicText>
219
+ )}
220
+ {empty.actionLabel && empty.onAction && (
221
+ <AtomicButton
222
+ variant="outline"
223
+ onPress={empty.onAction}
224
+ style={styles.retryButton}
225
+ >
226
+ {empty.actionLabel}
227
+ </AtomicButton>
228
+ )}
229
+ </View>
230
+ )}
231
+ </ScreenLayout>
232
+ );
233
+ }
234
+
235
+ // Content
236
+ return (
237
+ <ScreenLayout
238
+ edges={layout.edges || ['top', 'bottom', 'left', 'right']}
239
+ header={navigationHeader}
240
+ scrollable={layout.scrollable}
241
+ keyboardAvoiding={layout.keyboardAvoiding}
242
+ hideScrollIndicator={layout.hideScrollIndicator}
243
+ contentContainerStyle={layout.contentContainerStyle}
244
+ testID={testID}
245
+ >
246
+ {typeof children === 'function' ? (children as (data: T) => React.ReactNode)(data.data!) : children}
247
+ </ScreenLayout>
248
+ );
249
+ }
250
+
251
+ const getStyles = (_tokens: ReturnType<typeof useAppDesignTokens>) =>
252
+ StyleSheet.create({
253
+ centerContainer: {
254
+ flex: 1,
255
+ justifyContent: 'center',
256
+ alignItems: 'center',
257
+ paddingHorizontal: 32,
258
+ },
259
+ stateIcon: {
260
+ marginBottom: 16,
261
+ },
262
+ stateTitle: {
263
+ marginBottom: 8,
264
+ textAlign: 'center',
265
+ },
266
+ stateMessage: {
267
+ textAlign: 'center',
268
+ marginBottom: 24,
269
+ },
270
+ retryButton: {
271
+ minWidth: 120,
272
+ },
273
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Presentation Components - Public API
3
+ *
4
+ * Shared UI components for the entire application.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { GenericModal, GenericScreen } from '@/presentation/components';
9
+ *
10
+ * const modal = useModalState();
11
+ * return <GenericModal state={modal} />;
12
+ * ```
13
+ */
14
+
15
+ // =============================================================================
16
+ // MODAL COMPONENTS
17
+ // =============================================================================
18
+
19
+ export { GenericModal } from './GenericModal';
20
+ export type { GenericModalProps } from './GenericModal';
21
+
22
+ // =============================================================================
23
+ // SCREEN COMPONENTS
24
+ // =============================================================================
25
+
26
+ export { GenericScreen } from './GenericScreen';
27
+ export type { GenericScreenProps } from './GenericScreen';
@@ -3,6 +3,9 @@ import type { StackScreen } from "@umituz/react-native-design-system/molecules";
3
3
  import { LanguageSelectionScreen } from "../../../domains/localization";
4
4
  import { NotificationSettingsScreen } from "../../../domains/notifications";
5
5
  import { SettingsScreen } from "../../screens/SettingsScreen";
6
+ import { DisclaimerScreen } from "../../../domains/disclaimer/presentation/screens/DisclaimerScreen";
7
+ import { FeedbackScreen } from "../../../domains/feedback/presentation/screens/FeedbackScreen";
8
+ import { RatingPromptScreen } from "../../../domains/rating/presentation/screens/RatingPromptScreen";
6
9
 
7
10
  // AccountScreen is an optional peer — lazy require so the package works without @umituz/react-native-auth
8
11
  // Returns null if @umituz/react-native-auth is not installed
@@ -176,6 +179,25 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
176
179
  }
177
180
  });
178
181
 
182
+ // New screens to replace modals
183
+ const disclaimerScreen = {
184
+ name: "Disclaimer" as const,
185
+ component: DisclaimerScreen,
186
+ options: { headerShown: false },
187
+ };
188
+
189
+ const feedbackScreen = {
190
+ name: "Feedback" as const,
191
+ component: FeedbackScreen,
192
+ options: { headerShown: false },
193
+ };
194
+
195
+ const ratingPromptScreen = {
196
+ name: "RatingPrompt" as const,
197
+ component: RatingPromptScreen,
198
+ options: { headerShown: false },
199
+ };
200
+
179
201
  return combineScreens(
180
202
  baseScreens,
181
203
  faqScreen,
@@ -184,7 +206,10 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
184
206
  languageScreen,
185
207
  accountScreen,
186
208
  videoTutorialScreen,
187
- featureRequestScreen
209
+ featureRequestScreen,
210
+ disclaimerScreen,
211
+ feedbackScreen,
212
+ ratingPromptScreen
188
213
  );
189
214
  }, [
190
215
  translations,
@@ -6,6 +6,9 @@ import type React from 'react';
6
6
  import type { SettingsConfig, CustomSettingsSection } from "../screens/types";
7
7
  import type { DevSettingsProps } from "../../domains/dev";
8
8
  import type { FAQCategory } from "../../domains/faqs";
9
+ import type { DisclaimerScreenParams } from "../../domains/disclaimer/presentation/screens/DisclaimerScreen";
10
+ import type { FeedbackScreenParams } from "../../domains/feedback/presentation/screens/FeedbackScreen";
11
+ import type { RatingPromptScreenParams } from "../../domains/rating/presentation/screens/RatingPromptScreen";
9
12
 
10
13
  /**
11
14
  * App Info passed from main app (APP_INFO constant)
@@ -48,6 +51,9 @@ export type SettingsStackParamList = {
48
51
  Account: undefined;
49
52
  VideoTutorial: undefined;
50
53
  FeatureRequest: undefined;
54
+ Disclaimer: DisclaimerScreenParams;
55
+ Feedback: FeedbackScreenParams;
56
+ RatingPrompt: RatingPromptScreenParams;
51
57
  PasswordPrompt: {
52
58
  onComplete: (password: string | null) => void;
53
59
  title?: string;