@umituz/react-native-settings 5.4.8 → 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.
- package/package.json +1 -4
- package/src/core/base/BaseService.ts +141 -0
- package/src/core/index.ts +60 -0
- package/src/core/patterns/Modal/ModalConfig.ts +282 -0
- package/src/core/patterns/Modal/useModalState.ts +128 -0
- package/src/core/patterns/Screen/ScreenConfig.ts +375 -0
- package/src/core/patterns/Screen/useScreenData.ts +202 -0
- package/src/core/utils/logger.ts +138 -0
- package/src/core/utils/validators.ts +203 -0
- package/src/domains/disclaimer/index.ts +2 -2
- package/src/domains/disclaimer/presentation/hooks/useDisclaimerModal.ts +72 -0
- package/src/domains/feedback/index.ts +2 -1
- package/src/domains/feedback/presentation/hooks/useFeedbackModal.ts +182 -0
- package/src/domains/notifications/infrastructure/services/NotificationService.ts +16 -11
- package/src/domains/rating/application/services/RatingService.ts +89 -84
- package/src/domains/rating/index.ts +2 -2
- package/src/domains/rating/presentation/hooks/useRatingPromptModal.ts +122 -0
- package/src/index.ts +12 -0
- package/src/infrastructure/services/SettingsService.ts +33 -23
- package/src/presentation/components/GenericModal.tsx +212 -0
- package/src/presentation/components/GenericScreen.tsx +278 -0
- package/src/presentation/components/index.ts +27 -0
- package/src/domains/disclaimer/presentation/components/DisclaimerModal.tsx +0 -103
- package/src/domains/feedback/presentation/components/FeedbackModal.tsx +0 -99
- package/src/domains/rating/presentation/components/RatingPromptModal.tsx +0 -152
|
@@ -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 {
|
|
22
|
-
export type {
|
|
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
|
|
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
|
-
*
|
|
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 {
|
|
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
|
-
|
|
34
|
+
this.executeUnsafe('ensureConfigured', () => {
|
|
32
35
|
NotificationManager.configure();
|
|
33
36
|
this.isConfigured = true;
|
|
34
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|