@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.
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Common Validators
3
+ *
4
+ * Reusable validation functions for domains.
5
+ * All validators should be pure functions.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { validators } from '@/core/utils/validators';
10
+ *
11
+ * if (!validators.isValidEmail(email)) {
12
+ * return { error: 'Invalid email' };
13
+ * }
14
+ * ```
15
+ */
16
+
17
+ /**
18
+ * Common validation utilities
19
+ */
20
+ export const validators = {
21
+ /**
22
+ * Check if value is not empty (null, undefined, or empty string)
23
+ */
24
+ isNotEmpty: <T>(value: T | null | undefined | ''): value is T => {
25
+ return value !== null && value !== undefined && value !== '';
26
+ },
27
+
28
+ /**
29
+ * Check if string is a valid email format
30
+ */
31
+ isValidEmail: (email: string): boolean => {
32
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
33
+ return emailRegex.test(email);
34
+ },
35
+
36
+ /**
37
+ * Check if string is a valid URL
38
+ */
39
+ isValidUrl: (url: string): boolean => {
40
+ try {
41
+ new URL(url);
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ },
47
+
48
+ /**
49
+ * Check if value is within range (inclusive)
50
+ */
51
+ isInRange: (value: number, min: number, max: number): boolean => {
52
+ return value >= min && value <= max;
53
+ },
54
+
55
+ /**
56
+ * Check if value is a positive number
57
+ */
58
+ isPositive: (value: number): boolean => {
59
+ return value > 0;
60
+ },
61
+
62
+ /**
63
+ * Check if value is a non-negative number
64
+ */
65
+ isNonNegative: (value: number): boolean => {
66
+ return value >= 0;
67
+ },
68
+
69
+ /**
70
+ * Check if array has items
71
+ */
72
+ hasItems: <T>(array: T[] | readonly T[]): boolean => {
73
+ return Array.isArray(array) && array.length > 0;
74
+ },
75
+
76
+ /**
77
+ * Check if value is a valid ISO date string
78
+ */
79
+ isValidISODate: (dateString: string): boolean => {
80
+ if (!dateString) return false;
81
+ const date = new Date(dateString);
82
+ return date instanceof Date && !isNaN(date.getTime());
83
+ },
84
+
85
+ /**
86
+ * Check if date is in the past
87
+ */
88
+ isPastDate: (dateString: string): boolean => {
89
+ const date = new Date(dateString);
90
+ return date instanceof Date && !isNaN(date.getTime()) && date < new Date();
91
+ },
92
+
93
+ /**
94
+ * Check if date is in the future
95
+ */
96
+ isFutureDate: (dateString: string): boolean => {
97
+ const date = new Date(dateString);
98
+ return date instanceof Date && !isNaN(date.getTime()) && date > new Date();
99
+ },
100
+
101
+ /**
102
+ * Check if object has required properties
103
+ */
104
+ hasProperties: <T extends object>(
105
+ obj: T,
106
+ properties: (keyof T)[]
107
+ ): boolean => {
108
+ return properties.every((prop) => prop in obj);
109
+ },
110
+
111
+ /**
112
+ * Check if string meets minimum length
113
+ */
114
+ hasMinLength: (str: string, min: number): boolean => {
115
+ return typeof str === 'string' && str.length >= min;
116
+ },
117
+
118
+ /**
119
+ * Check if string meets maximum length
120
+ */
121
+ hasMaxLength: (str: string, max: number): boolean => {
122
+ return typeof str === 'string' && str.length <= max;
123
+ },
124
+
125
+ /**
126
+ * Check if value is one of the allowed values
127
+ */
128
+ isOneOf: <T>(value: T, allowed: readonly T[]): boolean => {
129
+ return allowed.includes(value);
130
+ },
131
+
132
+ /**
133
+ * Check if time is valid (hour: minute)
134
+ */
135
+ isValidTime: (hour: number, minute: number): boolean => {
136
+ return (
137
+ validators.isInRange(hour, 0, 23) &&
138
+ validators.isInRange(minute, 0, 59)
139
+ );
140
+ },
141
+
142
+ /**
143
+ * Check if time range is valid (start before end)
144
+ */
145
+ isValidTimeRange: (
146
+ startHour: number,
147
+ startMinute: number,
148
+ endHour: number,
149
+ endMinute: number
150
+ ): boolean => {
151
+ if (!validators.isValidTime(startHour, startMinute)) return false;
152
+ if (!validators.isValidTime(endHour, endMinute)) return false;
153
+
154
+ const startMinutes = startHour * 60 + startMinute;
155
+ const endMinutes = endHour * 60 + endMinute;
156
+
157
+ // Allow ranges that span midnight (e.g., 22:00 - 06:00)
158
+ return startMinutes !== endMinutes;
159
+ },
160
+ };
161
+
162
+ /**
163
+ * Validation result type
164
+ */
165
+ export type ValidationResult<T> =
166
+ | { valid: true; data: T }
167
+ | { valid: false; error: string };
168
+
169
+ /**
170
+ * Create a validation result
171
+ */
172
+ export function valid<T>(data: T): ValidationResult<T> {
173
+ return { valid: true, data };
174
+ }
175
+
176
+ /**
177
+ * Create an invalid validation result
178
+ */
179
+ export function invalid(error: string): ValidationResult<never> {
180
+ return { valid: false, error };
181
+ }
182
+
183
+ /**
184
+ * Chain multiple validations
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * const result = validateAll(
189
+ * validators.isValidEmail(email) || 'Invalid email',
190
+ * validators.hasMinLength(email, 5) || 'Email too short'
191
+ * );
192
+ * ```
193
+ */
194
+ export function validateAll(
195
+ ...checks: (boolean | string)[]
196
+ ): ValidationResult<null> {
197
+ for (const check of checks) {
198
+ if (check !== true) {
199
+ return invalid(typeof check === 'string' ? check : 'Validation failed');
200
+ }
201
+ }
202
+ return valid(null);
203
+ }
@@ -18,8 +18,8 @@ export type { DisclaimerSettingProps } from './presentation/components/Disclaime
18
18
  export { DisclaimerCard } from './presentation/components/DisclaimerCard';
19
19
  export type { DisclaimerCardProps } from './presentation/components/DisclaimerCard';
20
20
 
21
- export { DisclaimerModal } from './presentation/components/DisclaimerModal';
22
- export type { DisclaimerModalProps } from './presentation/components/DisclaimerModal';
21
+ export { useDisclaimerModal } from './presentation/hooks/useDisclaimerModal';
22
+ export type { DisclaimerModalOptions } from './presentation/hooks/useDisclaimerModal';
23
23
 
24
24
  // =============================================================================
25
25
  // PRESENTATION LAYER - Screens
@@ -0,0 +1,72 @@
1
+ /**
2
+ * useDisclaimerModal Hook
3
+ *
4
+ * Refactored to use GenericModal and ModalConfig.
5
+ * Replaces old DisclaimerModal component.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const { DisclaimerModal, showDisclaimer } = useDisclaimerModal();
10
+ *
11
+ * return (
12
+ * <>
13
+ * <Button onPress={() => showDisclaimer({ title: 'Terms', content: '...' })} />
14
+ * <DisclaimerModal />
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
+
25
+ export interface DisclaimerModalOptions {
26
+ /**
27
+ * Disclaimer title
28
+ */
29
+ title: string;
30
+
31
+ /**
32
+ * Disclaimer content
33
+ */
34
+ content: string;
35
+
36
+ /**
37
+ * Custom modal configuration
38
+ */
39
+ config?: Partial<ModalConfig>;
40
+ }
41
+
42
+ /**
43
+ * Disclaimer modal hook
44
+ */
45
+ export function useDisclaimerModal() {
46
+ const modal = useModalState();
47
+
48
+ const showDisclaimer = useCallback((options: DisclaimerModalOptions) => {
49
+ const { title, content, config: customConfig } = options;
50
+
51
+ const modalConfig: ModalConfig = {
52
+ title,
53
+ message: content,
54
+ dismissible: true,
55
+ closeOnBackdropPress: true,
56
+ closeOnBackPress: true,
57
+ ...customConfig,
58
+ };
59
+
60
+ modal.show(modalConfig);
61
+ }, [modal]);
62
+
63
+ const DisclaimerModal = useCallback(() => {
64
+ return <GenericModal state={modal} testID="disclaimer-modal" />;
65
+ }, [modal]);
66
+
67
+ return {
68
+ DisclaimerModal,
69
+ showDisclaimer,
70
+ modal,
71
+ };
72
+ }
@@ -4,7 +4,8 @@
4
4
  */
5
5
 
6
6
  export * from './presentation/components/FeedbackForm';
7
- export * from './presentation/components/FeedbackModal';
7
+ export { useFeedbackModal } from './presentation/hooks/useFeedbackModal';
8
+ export type { FeedbackModalOptions } from './presentation/hooks/useFeedbackModal';
8
9
  export { SupportSection } from './presentation/components/SupportSection';
9
10
  export type { SupportSectionProps, FeedbackModalTexts } from './presentation/components/SupportSection';
10
11
  export * from './presentation/hooks/useFeedbackForm';
@@ -0,0 +1,182 @@
1
+ /**
2
+ * useFeedbackModal Hook
3
+ *
4
+ * Refactored to use GenericModal and ModalConfig.
5
+ * Replaces old FeedbackModal component.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const { FeedbackModal, showFeedbackModal } = useFeedbackModal();
10
+ *
11
+ * return (
12
+ * <>
13
+ * <Button onPress={() => showFeedbackModal({ onSubmit: ... })} />
14
+ * <FeedbackModal />
15
+ * </>
16
+ * );
17
+ * ```
18
+ */
19
+
20
+ import { useCallback, useState } from 'react';
21
+ import { View, TouchableOpacity, StyleSheet } from 'react-native';
22
+ import { BaseModal } from '@umituz/react-native-design-system/molecules';
23
+ import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system/atoms';
24
+ import { ScreenLayout } from '@umituz/react-native-design-system/layouts';
25
+ import { useAppDesignTokens } from '@umituz/react-native-design-system/theme';
26
+ import { FeedbackForm } from '../components/FeedbackForm';
27
+ import type { FeedbackType, FeedbackRating } from '../../domain/entities/FeedbackEntity';
28
+ import type { FeedbackFormProps } from '../components/FeedbackFormProps';
29
+
30
+ export interface FeedbackModalOptions {
31
+ /**
32
+ * Submit callback
33
+ */
34
+ onSubmit: (data: {
35
+ type: FeedbackType;
36
+ rating: FeedbackRating;
37
+ description: string;
38
+ title: string;
39
+ }) => Promise<void>;
40
+
41
+ /**
42
+ * Initial feedback type
43
+ */
44
+ initialType?: FeedbackType;
45
+
46
+ /**
47
+ * Modal title
48
+ */
49
+ title?: string;
50
+
51
+ /**
52
+ * Modal subtitle
53
+ */
54
+ subtitle?: string;
55
+
56
+ /**
57
+ * Form texts
58
+ */
59
+ texts: FeedbackFormProps['texts'];
60
+
61
+ /**
62
+ * Custom configuration
63
+ */
64
+ config?: {
65
+ showHeader?: boolean;
66
+ showCloseButton?: boolean;
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Feedback modal hook
72
+ */
73
+ export function useFeedbackModal() {
74
+ const [visible, setVisible] = useState(false);
75
+ const [options, setOptions] = useState<FeedbackModalOptions | null>(null);
76
+ const [isSubmitting, setIsSubmitting] = useState(false);
77
+
78
+ const tokens = useAppDesignTokens();
79
+ const styles = getStyles(tokens);
80
+
81
+ const showFeedbackModal = useCallback((newOptions: FeedbackModalOptions) => {
82
+ setOptions(newOptions);
83
+ setVisible(true);
84
+ }, []);
85
+
86
+ const hideFeedbackModal = useCallback(() => {
87
+ setVisible(false);
88
+ setIsSubmitting(false);
89
+ }, []);
90
+
91
+ const handleSubmit = useCallback(async (data: any) => {
92
+ if (!options) return;
93
+
94
+ setIsSubmitting(true);
95
+ try {
96
+ await options.onSubmit(data);
97
+ hideFeedbackModal();
98
+ } catch (error) {
99
+ setIsSubmitting(false);
100
+ }
101
+ }, [options, hideFeedbackModal]);
102
+
103
+ const header = options?.config?.showHeader !== false ? (
104
+ <View style={styles.header}>
105
+ <View style={styles.headerText}>
106
+ {options?.title && (
107
+ <AtomicText style={{ fontSize: 20, fontWeight: '600' }}>
108
+ {options.title}
109
+ </AtomicText>
110
+ )}
111
+ {options?.subtitle && (
112
+ <AtomicText style={{ fontSize: 14, marginTop: 4 }}>
113
+ {options.subtitle}
114
+ </AtomicText>
115
+ )}
116
+ </View>
117
+ {options?.config?.showCloseButton !== false && (
118
+ <TouchableOpacity
119
+ onPress={hideFeedbackModal}
120
+ style={[styles.closeButton, { backgroundColor: tokens.colors.surfaceVariant }]}
121
+ >
122
+ <AtomicIcon name="close" size="sm" color="onSurface" />
123
+ </TouchableOpacity>
124
+ )}
125
+ </View>
126
+ ) : undefined;
127
+
128
+ const FeedbackModal = useCallback(() => {
129
+ if (!options) return null;
130
+
131
+ return (
132
+ <BaseModal visible={visible} onClose={hideFeedbackModal}>
133
+ <ScreenLayout
134
+ header={header}
135
+ scrollable={true}
136
+ edges={[]}
137
+ keyboardAvoiding={true}
138
+ contentContainerStyle={styles.content}
139
+ hideScrollIndicator={false}
140
+ >
141
+ <FeedbackForm
142
+ onSubmit={handleSubmit}
143
+ initialType={options.initialType}
144
+ isSubmitting={isSubmitting}
145
+ texts={options.texts}
146
+ />
147
+ </ScreenLayout>
148
+ </BaseModal>
149
+ );
150
+ }, [visible, options, header, styles, handleSubmit, isSubmitting, hideFeedbackModal]);
151
+
152
+ return {
153
+ FeedbackModal,
154
+ showFeedbackModal,
155
+ hideFeedbackModal,
156
+ isVisible: visible,
157
+ };
158
+ }
159
+
160
+ const getStyles = (_tokens: ReturnType<typeof useAppDesignTokens>) =>
161
+ StyleSheet.create({
162
+ header: {
163
+ flexDirection: 'row',
164
+ justifyContent: 'space-between',
165
+ alignItems: 'center',
166
+ padding: 16,
167
+ borderBottomWidth: 1,
168
+ },
169
+ headerText: {
170
+ flex: 1,
171
+ },
172
+ closeButton: {
173
+ width: 36,
174
+ height: 36,
175
+ borderRadius: 18,
176
+ justifyContent: 'center',
177
+ alignItems: 'center',
178
+ },
179
+ content: {
180
+ padding: 20,
181
+ },
182
+ });
@@ -1,14 +1,15 @@
1
1
  /**
2
- * NotificationService
2
+ * Notification Service
3
3
  *
4
4
  * Simple facade for offline notification system.
5
5
  * Works in ALL apps - offline, online, hybrid - no backend required.
6
+ * Refactored to extend BaseService.
6
7
  *
7
8
  * @module NotificationService
8
9
  */
9
10
 
10
11
  import { NotificationManager } from './NotificationManager';
11
- import { isDev } from '../../../../utils/devUtils';
12
+ import { BaseService } from '../../../../core/base/BaseService';
12
13
 
13
14
  export * from './types';
14
15
 
@@ -16,26 +17,24 @@ export * from './types';
16
17
  * Notification service singleton
17
18
  * Provides simple access to notification manager
18
19
  */
19
- export class NotificationService {
20
+ export class NotificationService extends BaseService {
21
+ protected serviceName = 'NotificationService';
20
22
  private static instance: NotificationService | null = null;
21
23
  private isConfigured = false;
22
24
 
23
25
  readonly notifications = new NotificationManager();
24
26
 
25
27
  private constructor() {
28
+ super();
26
29
  // Configuration deferred to first use
27
30
  }
28
31
 
29
32
  private ensureConfigured() {
30
33
  if (!this.isConfigured) {
31
- try {
34
+ this.executeUnsafe('ensureConfigured', () => {
32
35
  NotificationManager.configure();
33
36
  this.isConfigured = true;
34
- } catch (error) {
35
- if (isDev()) {
36
- console.error('[NotificationService] Failed to configure NotificationManager:', error);
37
- }
38
- }
37
+ });
39
38
  }
40
39
  }
41
40
 
@@ -51,7 +50,10 @@ export class NotificationService {
51
50
  */
52
51
  async requestPermissions(): Promise<boolean> {
53
52
  this.ensureConfigured();
54
- return await this.notifications.requestPermissions();
53
+ const result = await this.execute('requestPermissions', async () => {
54
+ return await this.notifications.requestPermissions();
55
+ });
56
+ return result.success ? result.data : false;
55
57
  }
56
58
 
57
59
  /**
@@ -59,7 +61,10 @@ export class NotificationService {
59
61
  */
60
62
  async hasPermissions(): Promise<boolean> {
61
63
  this.ensureConfigured();
62
- return await this.notifications.hasPermissions();
64
+ const result = await this.execute('hasPermissions', async () => {
65
+ return await this.notifications.hasPermissions();
66
+ });
67
+ return result.success ? result.data : false;
63
68
  }
64
69
  }
65
70