@umituz/react-native-settings 5.4.9 → 5.4.10

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.
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * Rating Service
3
- * Core business logic for app rating system
3
+ *
4
+ * Core business logic for app rating system.
5
+ * Refactored to extend BaseService for consistent error handling.
6
+ *
7
+ * @module RatingService
4
8
  */
5
9
 
6
10
  import type { RatingConfig, RatingState } from "../../domain/entities/RatingConfig";
7
- import { isDev } from "../../../../utils/devUtils";
11
+ import { BaseService } from "../../../../core/base/BaseService";
8
12
  import {
9
13
  getEventCount,
10
14
  incrementEventCount,
@@ -35,112 +39,113 @@ function toISOString(date: Date = new Date()): string {
35
39
  }
36
40
 
37
41
  /**
38
- * Track an event occurrence
42
+ * Rating Service Class
39
43
  */
40
- export async function trackEvent(eventType: string): Promise<void> {
41
- try {
42
- await incrementEventCount(eventType);
43
- } catch (error) {
44
- if (isDev()) {
45
- console.error('[RatingService] Failed to track event:', eventType, error);
46
- }
44
+ export class RatingService extends BaseService {
45
+ protected serviceName = "RatingService";
46
+
47
+ /**
48
+ * Track an event occurrence
49
+ */
50
+ async trackEvent(eventType: string): Promise<void> {
51
+ await this.execute("trackEvent", async () => {
52
+ await incrementEventCount(eventType);
53
+ });
47
54
  }
48
- }
49
55
 
50
- /**
51
- * Check if prompt should be shown based on criteria
52
- */
53
- export async function shouldShowPrompt(config: RatingConfig): Promise<boolean> {
54
- try {
55
- const hasRated = await getHasRated();
56
- if (hasRated) {
57
- return false;
58
- }
56
+ /**
57
+ * Check if prompt should be shown based on criteria
58
+ */
59
+ async shouldShowPrompt(config: RatingConfig): Promise<boolean> {
60
+ const result = await this.execute("shouldShowPrompt", async () => {
61
+ const hasRated = await getHasRated();
62
+ if (hasRated) return false;
59
63
 
60
- const dismissed = await getDismissed();
61
- if (dismissed) {
62
- return false;
63
- }
64
+ const dismissed = await getDismissed();
65
+ if (dismissed) return false;
64
66
 
65
- const eventCount = await getEventCount(config.eventType);
66
- const minCount = config.minEventCount ?? 3;
67
+ const eventCount = await getEventCount(config.eventType);
68
+ const minCount = config.minEventCount ?? 3;
67
69
 
68
- if (eventCount < minCount) {
69
- return false;
70
- }
70
+ if (eventCount < minCount) return false;
71
71
 
72
- const lastPromptDate = await getLastPromptDate(config.eventType);
72
+ const lastPromptDate = await getLastPromptDate(config.eventType);
73
73
 
74
- if (lastPromptDate) {
75
- const cooldownDays = config.cooldownDays ?? 90;
76
- const daysSinceLastPrompt = daysBetween(lastPromptDate, new Date());
74
+ if (lastPromptDate) {
75
+ const cooldownDays = config.cooldownDays ?? 90;
76
+ const daysSinceLastPrompt = daysBetween(lastPromptDate, new Date());
77
77
 
78
- if (daysSinceLastPrompt < cooldownDays) {
79
- return false;
78
+ if (daysSinceLastPrompt < cooldownDays) {
79
+ return false;
80
+ }
80
81
  }
81
- }
82
82
 
83
- return true;
84
- } catch (error) {
85
- return false;
83
+ return true;
84
+ });
85
+
86
+ return result.success ? result.data : false;
86
87
  }
87
- }
88
88
 
89
- /**
90
- * Mark that prompt was shown to user
91
- */
92
- export async function markPromptShown(eventType: string): Promise<void> {
93
- try {
94
- await setLastPromptDate(eventType, toISOString());
95
- } catch (error) {
96
- if (isDev()) {
97
- console.error('[RatingService] Failed to mark prompt shown:', eventType, error);
98
- }
89
+ /**
90
+ * Mark that prompt was shown to user
91
+ */
92
+ async markPromptShown(eventType: string): Promise<void> {
93
+ await this.execute("markPromptShown", async () => {
94
+ await setLastPromptDate(eventType, toISOString());
95
+ });
99
96
  }
100
- }
101
97
 
102
- /**
103
- * Mark that user has rated the app
104
- */
105
- export async function markRated(): Promise<void> {
106
- try {
107
- await setHasRated(true);
108
- } catch (error) {
109
- if (isDev()) {
110
- console.error('[RatingService] Failed to mark as rated:', error);
111
- }
98
+ /**
99
+ * Mark that user has rated the app
100
+ */
101
+ async markRated(): Promise<void> {
102
+ await this.execute("markRated", async () => {
103
+ await setHasRated(true);
104
+ });
112
105
  }
113
- }
114
106
 
115
- /**
116
- * Mark that user permanently dismissed the prompt
117
- */
118
- export async function markDismissed(): Promise<void> {
119
- try {
120
- await setDismissed(true);
121
- } catch (error) {
122
- if (isDev()) {
123
- console.error('[RatingService] Failed to mark as dismissed:', error);
124
- }
107
+ /**
108
+ * Mark that user permanently dismissed the prompt
109
+ */
110
+ async markDismissed(): Promise<void> {
111
+ await this.execute("markDismissed", async () => {
112
+ await setDismissed(true);
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Get current rating state for event type
118
+ */
119
+ async getState(eventType: string): Promise<RatingState> {
120
+ return getRatingState(eventType);
121
+ }
122
+
123
+ /**
124
+ * Reset rating data (for testing or specific event type)
125
+ */
126
+ async reset(eventType?: string): Promise<void> {
127
+ await this.execute("reset", async () => {
128
+ await resetStorage(eventType);
129
+ });
125
130
  }
126
131
  }
127
132
 
128
133
  /**
129
- * Get current rating state for event type
134
+ * Singleton instance
130
135
  */
131
- export async function getState(eventType: string): Promise<RatingState> {
132
- return getRatingState(eventType);
133
- }
136
+ let ratingServiceInstance: RatingService | null = null;
134
137
 
135
138
  /**
136
- * Reset rating data (for testing or specific event type)
139
+ * Get rating service singleton instance
137
140
  */
138
- export async function reset(eventType?: string): Promise<void> {
139
- try {
140
- await resetStorage(eventType);
141
- } catch (error) {
142
- if (isDev()) {
143
- console.error('[RatingService] Failed to reset:', eventType, error);
144
- }
141
+ export function getRatingService(): RatingService {
142
+ if (!ratingServiceInstance) {
143
+ ratingServiceInstance = new RatingService();
145
144
  }
145
+ return ratingServiceInstance;
146
146
  }
147
+
148
+ /**
149
+ * Default export for backward compatibility
150
+ */
151
+ export const ratingService = getRatingService();
@@ -37,8 +37,8 @@ export type { StarRatingProps } from './presentation/components/StarRating';
37
37
  // PRESENTATION LAYER - Components (App Store Rating)
38
38
  // =============================================================================
39
39
 
40
- export { RatingPromptModal } from './presentation/components/RatingPromptModal';
41
- export type { RatingPromptModalProps } from './presentation/components/RatingPromptModal';
40
+ export { useRatingPromptModal } from './presentation/hooks/useRatingPromptModal';
41
+ export type { RatingPromptOptions } from './presentation/hooks/useRatingPromptModal';
42
42
 
43
43
  // =============================================================================
44
44
  // PRESENTATION LAYER - Hooks (App Store Rating)
@@ -0,0 +1,122 @@
1
+ /**
2
+ * useRatingPromptModal Hook
3
+ *
4
+ * Refactored to use GenericModal and ModalConfig.
5
+ * Replaces old RatingPromptModal component.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const { RatingPrompt, showRatingPrompt } = useRatingPromptModal();
10
+ *
11
+ * return (
12
+ * <>
13
+ * <Button onPress={() => showRatingPrompt({ onPositive: ... })} />
14
+ * <RatingPrompt />
15
+ * </>
16
+ * );
17
+ * ```
18
+ */
19
+
20
+ import { useCallback } from 'react';
21
+ import { GenericModal } from '../../../../presentation/components/GenericModal';
22
+ import { useModalState } from '../../../../core/patterns/Modal/useModalState';
23
+ import type { ModalConfig } from '../../../../core/patterns/Modal/ModalConfig';
24
+ import type { RatingTranslations } from '../../domain/entities/RatingConfig';
25
+
26
+ export interface RatingPromptOptions {
27
+ /**
28
+ * Callback when user clicks positive (loves the app)
29
+ */
30
+ onPositive: () => void | Promise<void>;
31
+
32
+ /**
33
+ * Callback when user clicks negative (doesn't love the app)
34
+ */
35
+ onNegative: () => void | Promise<void>;
36
+
37
+ /**
38
+ * Callback when user clicks later
39
+ */
40
+ onLater: () => void | Promise<void>;
41
+
42
+ /**
43
+ * App name for message
44
+ */
45
+ appName?: string;
46
+
47
+ /**
48
+ * Custom translations
49
+ */
50
+ translations?: RatingTranslations;
51
+
52
+ /**
53
+ * Custom modal configuration
54
+ */
55
+ config?: Partial<ModalConfig>;
56
+ }
57
+
58
+ /**
59
+ * Rating prompt modal hook
60
+ */
61
+ export function useRatingPromptModal() {
62
+ const modal = useModalState();
63
+
64
+ const showRatingPrompt = useCallback((options: RatingPromptOptions) => {
65
+ const {
66
+ onPositive,
67
+ onNegative,
68
+ onLater,
69
+ appName = 'this app',
70
+ translations,
71
+ config: customConfig,
72
+ } = options;
73
+
74
+ const defaultTranslations: RatingTranslations = {
75
+ title: translations?.title ?? 'Enjoying the app?',
76
+ message:
77
+ translations?.message ??
78
+ `If you love using ${appName}, would you mind taking a moment to rate it?`,
79
+ positiveButton: translations?.positiveButton ?? 'Yes, I love it!',
80
+ negativeButton: translations?.negativeButton ?? 'Not really',
81
+ laterButton: translations?.laterButton ?? 'Maybe later',
82
+ };
83
+
84
+ const modalConfig: ModalConfig = {
85
+ title: defaultTranslations.title,
86
+ message: defaultTranslations.message,
87
+ icon: 'star',
88
+ iconColor: 'primary',
89
+ actions: [
90
+ {
91
+ label: defaultTranslations.positiveButton,
92
+ onPress: onPositive,
93
+ variant: 'primary',
94
+ },
95
+ {
96
+ label: defaultTranslations.negativeButton,
97
+ onPress: onNegative,
98
+ variant: 'outline',
99
+ },
100
+ {
101
+ label: defaultTranslations.laterButton,
102
+ onPress: onLater,
103
+ variant: 'text',
104
+ },
105
+ ],
106
+ dismissible: true,
107
+ ...customConfig,
108
+ };
109
+
110
+ modal.show(modalConfig);
111
+ }, [modal]);
112
+
113
+ const RatingPrompt = useCallback(() => {
114
+ return <GenericModal state={modal} testID="rating-prompt-modal" />;
115
+ }, [modal]);
116
+
117
+ return {
118
+ RatingPrompt,
119
+ showRatingPrompt,
120
+ modal,
121
+ };
122
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,18 @@
8
8
  * import { useSettings, SettingsScreen, AppearanceScreen, SettingsItemCard, DisclaimerSetting } from '@umituz/react-native-settings';
9
9
  */
10
10
 
11
+ // =============================================================================
12
+ // CORE LAYER - Base Classes & Patterns
13
+ // =============================================================================
14
+
15
+ export * from './core';
16
+
17
+ // =============================================================================
18
+ // PRESENTATION LAYER - Generic Components
19
+ // =============================================================================
20
+
21
+ export * from './presentation/components';
22
+
11
23
  // =============================================================================
12
24
  // DOMAIN LAYER - Repository Interfaces
13
25
  // =============================================================================
@@ -1,37 +1,47 @@
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
-
13
- constructor() {
14
- this.repository = new SettingsRepository();
15
- }
16
-
17
- async getSettings(userId: string): Promise<SettingsResult<UserSettings>> {
18
- return this.repository.getSettings(userId);
19
- }
20
-
21
- async saveSettings(settings: UserSettings): Promise<SettingsResult<void>> {
22
- return this.repository.saveSettings(settings);
23
- }
24
-
25
- async resetSettings(userId: string): Promise<SettingsResult<void>> {
26
- return this.repository.deleteSettings(userId);
27
- }
12
+ export class SettingsService extends BaseService {
13
+ protected serviceName = 'SettingsService';
14
+ private repository: SettingsRepository;
15
+
16
+ constructor() {
17
+ super();
18
+ this.repository = new SettingsRepository();
19
+ }
20
+
21
+ async getSettings(userId: string): Promise<SettingsResult<UserSettings>> {
22
+ return this.execute('getSettings', async () => {
23
+ return await this.repository.getSettings(userId);
24
+ });
25
+ }
26
+
27
+ async saveSettings(settings: UserSettings): Promise<SettingsResult<void>> {
28
+ return this.execute('saveSettings', async () => {
29
+ return await this.repository.saveSettings(settings);
30
+ });
31
+ }
32
+
33
+ async resetSettings(userId: string): Promise<SettingsResult<void>> {
34
+ return this.execute('resetSettings', async () => {
35
+ return await this.repository.deleteSettings(userId);
36
+ });
37
+ }
28
38
  }
29
39
 
30
40
  let settingsServiceInstance: SettingsService | null = null;
31
41
 
32
42
  export function getSettingsService(): SettingsService {
33
- if (!settingsServiceInstance) {
34
- settingsServiceInstance = new SettingsService();
35
- }
36
- return settingsServiceInstance;
43
+ if (!settingsServiceInstance) {
44
+ settingsServiceInstance = new SettingsService();
45
+ }
46
+ return settingsServiceInstance;
37
47
  }
@@ -0,0 +1,212 @@
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, TouchableOpacity } 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 { ModalConfig, 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
+ closeOnBackdropPress = true,
72
+ closeOnBackPress = true,
73
+ maxWidth,
74
+ } = config;
75
+
76
+ const handleDismiss = () => {
77
+ if (dismissible) {
78
+ state.hide();
79
+ }
80
+ };
81
+
82
+ const modalWidth = maxWidth ?? responsive.maxContentWidth * 0.9;
83
+
84
+ return (
85
+ <BaseModal
86
+ visible={visible}
87
+ onClose={handleDismiss}
88
+ closeOnBackdropPress={closeOnBackdropPress}
89
+ closeOnBackPress={closeOnBackPress}
90
+ >
91
+ <View
92
+ style={[
93
+ styles.container,
94
+ {
95
+ backgroundColor: tokens.colors.surface,
96
+ borderRadius: tokens.borders.radius.xl,
97
+ padding: tokens.spacing.lg,
98
+ maxWidth: modalWidth,
99
+ width: '90%',
100
+ },
101
+ containerStyle,
102
+ ]}
103
+ testID={testID}
104
+ >
105
+ {/* Custom Header */}
106
+ {header ? (
107
+ header
108
+ ) : (
109
+ <>
110
+ {/* Icon */}
111
+ {icon && (
112
+ <View style={styles.iconContainer}>
113
+ <AtomicIcon
114
+ name={icon}
115
+ size="xl"
116
+ color={iconColor || 'primary'}
117
+ />
118
+ </View>
119
+ )}
120
+
121
+ {/* Title and Subtitle */}
122
+ {title && (
123
+ <AtomicText
124
+ type="headlineMedium"
125
+ color="onSurface"
126
+ style={[styles.title, { marginBottom: subtitle ? tokens.spacing.xs : tokens.spacing.md }]}
127
+ >
128
+ {title}
129
+ </AtomicText>
130
+ )}
131
+
132
+ {subtitle && (
133
+ <AtomicText
134
+ type="bodyMedium"
135
+ color="onSurfaceVariant"
136
+ style={[styles.subtitle, { marginBottom: tokens.spacing.md }]}
137
+ >
138
+ {subtitle}
139
+ </AtomicText>
140
+ )}
141
+ </>
142
+ )}
143
+
144
+ {/* Custom Body or Message */}
145
+ {body ? (
146
+ body
147
+ ) : message && (
148
+ <AtomicText
149
+ type="bodyMedium"
150
+ color="onSurfaceVariant"
151
+ style={[styles.message, { marginBottom: actions.length > 0 ? tokens.spacing.lg : 0 }]}
152
+ >
153
+ {message}
154
+ </AtomicText>
155
+ )}
156
+
157
+ {/* Custom Footer or Actions */}
158
+ {footer ? (
159
+ footer
160
+ ) : actions.length > 0 && (
161
+ <View style={[styles.buttonContainer, { gap: tokens.spacing.sm }]}>
162
+ {actions.map((action, index) => (
163
+ <AtomicButton
164
+ key={index}
165
+ variant={action.variant || 'primary'}
166
+ onPress={async () => {
167
+ await action.onPress();
168
+ if (dismissible) {
169
+ state.hide();
170
+ }
171
+ }}
172
+ disabled={action.disabled}
173
+ loading={action.loading}
174
+ style={styles.button}
175
+ testID={action.testID}
176
+ >
177
+ {action.label}
178
+ </AtomicButton>
179
+ ))}
180
+ </View>
181
+ )}
182
+ </View>
183
+ </BaseModal>
184
+ );
185
+ };
186
+
187
+ const getStyles = (_tokens: ReturnType<typeof useAppDesignTokens>) =>
188
+ StyleSheet.create({
189
+ container: {
190
+ alignItems: 'center',
191
+ },
192
+ iconContainer: {
193
+ marginBottom: 16,
194
+ },
195
+ title: {
196
+ textAlign: 'center',
197
+ },
198
+ subtitle: {
199
+ textAlign: 'center',
200
+ },
201
+ message: {
202
+ textAlign: 'center',
203
+ lineHeight: 24,
204
+ },
205
+ buttonContainer: {
206
+ width: '100%',
207
+ marginTop: 16,
208
+ },
209
+ button: {
210
+ width: '100%',
211
+ },
212
+ });