@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.
- package/package.json +1 -1
- 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 +201 -0
- package/src/core/utils/logger.ts +138 -0
- package/src/core/utils/validators.ts +203 -0
- package/src/domains/disclaimer/index.ts +0 -3
- package/src/domains/disclaimer/presentation/components/DisclaimerSetting.tsx +18 -43
- package/src/domains/disclaimer/presentation/screens/DisclaimerScreen.tsx +42 -92
- package/src/domains/feedback/index.ts +2 -1
- package/src/domains/feedback/presentation/components/SupportSection.tsx +16 -43
- package/src/domains/feedback/presentation/screens/FeatureRequestScreen.tsx +4 -4
- package/src/domains/feedback/presentation/screens/FeedbackScreen.tsx +75 -0
- package/src/domains/notifications/infrastructure/services/NotificationService.ts +16 -13
- package/src/domains/rating/application/services/RatingService.ts +115 -79
- package/src/domains/rating/index.ts +3 -3
- package/src/domains/rating/presentation/hooks/useAppRating.tsx +42 -65
- package/src/domains/rating/presentation/screens/RatingPromptScreen.tsx +162 -0
- package/src/index.ts +12 -0
- package/src/infrastructure/services/SettingsService.ts +23 -19
- package/src/presentation/components/GenericModal.tsx +208 -0
- package/src/presentation/components/GenericScreen.tsx +273 -0
- package/src/presentation/components/index.ts +27 -0
- package/src/presentation/navigation/hooks/useSettingsScreens.ts +26 -1
- package/src/presentation/navigation/types.ts +6 -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,9 +18,6 @@ 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';
|
|
23
|
-
|
|
24
21
|
// =============================================================================
|
|
25
22
|
// PRESENTATION LAYER - Screens
|
|
26
23
|
// =============================================================================
|
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
* Used in About screens for apps that require disclaimers
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
8
|
-
* - Tappable card that opens full disclaimer
|
|
8
|
+
* - Tappable card that opens full disclaimer screen
|
|
9
9
|
* - Warning icon with background color
|
|
10
10
|
* - Internationalized title and message
|
|
11
|
-
* - Full-screen modal with scrollable content
|
|
12
11
|
* - NO shadows (CLAUDE.md compliance)
|
|
13
12
|
* - Universal across iOS, Android, Web (NO Platform.OS checks)
|
|
14
13
|
*
|
|
@@ -17,12 +16,11 @@
|
|
|
17
16
|
* - Requires translations: settings.disclaimer.title, settings.disclaimer.message, settings.disclaimer.shortMessage
|
|
18
17
|
*/
|
|
19
18
|
|
|
20
|
-
import React, {
|
|
21
|
-
import { Modal } from 'react-native';
|
|
19
|
+
import React, { useCallback } from 'react';
|
|
22
20
|
|
|
23
21
|
import { useAppDesignTokens, withAlpha } from '@umituz/react-native-design-system/theme';
|
|
22
|
+
import { useAppNavigation } from '@umituz/react-native-design-system/molecules';
|
|
24
23
|
import { DisclaimerCard } from './DisclaimerCard';
|
|
25
|
-
import { DisclaimerModal } from './DisclaimerModal';
|
|
26
24
|
|
|
27
25
|
export interface DisclaimerSettingProps {
|
|
28
26
|
/** Custom title */
|
|
@@ -48,50 +46,27 @@ export const DisclaimerSetting: React.FC<DisclaimerSettingProps> = ({
|
|
|
48
46
|
backgroundColor,
|
|
49
47
|
}) => {
|
|
50
48
|
const tokens = useAppDesignTokens();
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
return () => {
|
|
55
|
-
setModalVisible(false);
|
|
56
|
-
};
|
|
57
|
-
}, []);
|
|
49
|
+
const navigation = useAppNavigation();
|
|
58
50
|
|
|
59
51
|
const finalIconColor = iconColor || tokens.colors.warning;
|
|
60
52
|
const finalBackgroundColor = backgroundColor || withAlpha(finalIconColor, 0.1);
|
|
61
53
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}, []);
|
|
54
|
+
const handleOpenDisclaimer = useCallback(() => {
|
|
55
|
+
navigation.push('Disclaimer' as never, {
|
|
56
|
+
title,
|
|
57
|
+
content,
|
|
58
|
+
});
|
|
59
|
+
}, [navigation, title, content]);
|
|
69
60
|
|
|
70
61
|
return (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
/>
|
|
80
|
-
|
|
81
|
-
<Modal
|
|
82
|
-
visible={modalVisible}
|
|
83
|
-
animationType="none"
|
|
84
|
-
presentationStyle="pageSheet"
|
|
85
|
-
onRequestClose={handleCloseModal}
|
|
86
|
-
>
|
|
87
|
-
<DisclaimerModal
|
|
88
|
-
visible={modalVisible}
|
|
89
|
-
title={title}
|
|
90
|
-
content={content}
|
|
91
|
-
onClose={handleCloseModal}
|
|
92
|
-
/>
|
|
93
|
-
</Modal>
|
|
94
|
-
</>
|
|
62
|
+
<DisclaimerCard
|
|
63
|
+
title={title}
|
|
64
|
+
shortMessage={shortMessage}
|
|
65
|
+
iconName={iconName}
|
|
66
|
+
iconColor={finalIconColor}
|
|
67
|
+
backgroundColor={finalBackgroundColor}
|
|
68
|
+
onPress={handleOpenDisclaimer}
|
|
69
|
+
/>
|
|
95
70
|
);
|
|
96
71
|
};
|
|
97
72
|
|
|
@@ -1,115 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Disclaimer Screen
|
|
3
3
|
*
|
|
4
|
-
* Full
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - SafeAreaView wrapper for proper display
|
|
9
|
-
* - Scrollable content for long disclaimers
|
|
10
|
-
* - Customizable title and content via props or translations
|
|
11
|
-
* - NO shadows (CLAUDE.md compliance)
|
|
12
|
-
* - Universal across iOS, Android, Web
|
|
4
|
+
* Full screen for displaying disclaimer/important legal notice.
|
|
5
|
+
* Replaces modal approach with native navigation.
|
|
13
6
|
*/
|
|
14
7
|
|
|
15
8
|
import React from 'react';
|
|
16
|
-
import { View, StyleSheet } from 'react-native';
|
|
17
|
-
import { AtomicText, AtomicIcon, type IconName } from '@umituz/react-native-design-system/atoms';
|
|
9
|
+
import { View, StyleSheet, ScrollView } from 'react-native';
|
|
18
10
|
import { ScreenLayout } from '@umituz/react-native-design-system/layouts';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
11
|
+
import { AtomicText } from '@umituz/react-native-design-system/atoms';
|
|
12
|
+
import { NavigationHeader, useAppNavigation } from '@umituz/react-native-design-system/molecules';
|
|
21
13
|
|
|
22
|
-
export interface
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
titleKey?: string;
|
|
27
|
-
/** Custom content (overrides translation) */
|
|
28
|
-
content?: string;
|
|
29
|
-
/** Custom content translation key */
|
|
30
|
-
contentKey?: string;
|
|
31
|
-
/** Custom icon name */
|
|
32
|
-
iconName?: string;
|
|
14
|
+
export interface DisclaimerScreenParams {
|
|
15
|
+
title: string;
|
|
16
|
+
content: string;
|
|
17
|
+
[key: string]: unknown;
|
|
33
18
|
}
|
|
34
19
|
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
const tokens = useAppDesignTokens();
|
|
41
|
-
const styles = getStyles(tokens);
|
|
42
|
-
|
|
43
|
-
const displayTitle = title || "";
|
|
44
|
-
const displayContent = content || "";
|
|
20
|
+
export interface DisclaimerScreenProps {
|
|
21
|
+
route: {
|
|
22
|
+
params: DisclaimerScreenParams;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
45
25
|
|
|
26
|
+
export const DisclaimerScreen: React.FC<DisclaimerScreenProps> = ({ route }) => {
|
|
46
27
|
const navigation = useAppNavigation();
|
|
28
|
+
const { title, content } = route.params;
|
|
47
29
|
|
|
48
30
|
return (
|
|
49
31
|
<ScreenLayout
|
|
50
32
|
scrollable={true}
|
|
51
33
|
edges={['top', 'bottom', 'left', 'right']}
|
|
52
|
-
contentContainerStyle={styles.scrollContent}
|
|
53
34
|
hideScrollIndicator={false}
|
|
54
|
-
header={
|
|
55
|
-
<NavigationHeader
|
|
56
|
-
title={displayTitle}
|
|
57
|
-
onBackPress={() => navigation.goBack()}
|
|
58
|
-
/>
|
|
59
|
-
}
|
|
60
35
|
>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
},
|
|
70
|
-
]}
|
|
71
|
-
>
|
|
72
|
-
<AtomicIcon name={iconName as IconName} color="warning" size="xl" />
|
|
73
|
-
</View>
|
|
36
|
+
<NavigationHeader
|
|
37
|
+
title={title || 'Disclaimer'}
|
|
38
|
+
onBackPress={() => navigation.goBack()}
|
|
39
|
+
/>
|
|
40
|
+
<View style={styles.container}>
|
|
41
|
+
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
|
|
42
|
+
<AtomicText style={styles.content}>{content}</AtomicText>
|
|
43
|
+
</ScrollView>
|
|
74
44
|
</View>
|
|
75
|
-
|
|
76
|
-
{/* Title */}
|
|
77
|
-
<AtomicText type="headlineMedium" color="primary" style={styles.title}>
|
|
78
|
-
{displayTitle}
|
|
79
|
-
</AtomicText>
|
|
80
|
-
|
|
81
|
-
{/* Content */}
|
|
82
|
-
<AtomicText type="bodyMedium" color="secondary" style={styles.content}>
|
|
83
|
-
{displayContent}
|
|
84
|
-
</AtomicText>
|
|
85
45
|
</ScreenLayout>
|
|
86
46
|
);
|
|
87
47
|
};
|
|
88
48
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
},
|
|
107
|
-
title: {
|
|
108
|
-
textAlign: 'center',
|
|
109
|
-
marginBottom: tokens.spacing.lg,
|
|
110
|
-
},
|
|
111
|
-
content: {
|
|
112
|
-
lineHeight: 24,
|
|
113
|
-
textAlign: 'left',
|
|
114
|
-
},
|
|
115
|
-
});
|
|
49
|
+
const styles = StyleSheet.create({
|
|
50
|
+
container: {
|
|
51
|
+
flex: 1,
|
|
52
|
+
},
|
|
53
|
+
scrollView: {
|
|
54
|
+
flex: 1,
|
|
55
|
+
},
|
|
56
|
+
scrollContent: {
|
|
57
|
+
padding: 20,
|
|
58
|
+
paddingBottom: 40,
|
|
59
|
+
},
|
|
60
|
+
content: {
|
|
61
|
+
fontSize: 16,
|
|
62
|
+
lineHeight: 24,
|
|
63
|
+
color: 'rgba(255, 255, 255, 0.9)',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export * from './presentation/components/FeedbackForm';
|
|
7
|
-
export * from './presentation/components/FeedbackModal';
|
|
8
7
|
export { SupportSection } from './presentation/components/SupportSection';
|
|
9
8
|
export type { SupportSectionProps, FeedbackModalTexts } from './presentation/components/SupportSection';
|
|
10
9
|
export * from './presentation/hooks/useFeedbackForm';
|
|
11
10
|
export * from './domain/entities/FeedbackEntity';
|
|
12
11
|
export * from './domain/entities/FeatureRequestEntity';
|
|
12
|
+
export { FeedbackScreen } from './presentation/screens/FeedbackScreen';
|
|
13
|
+
export type { FeedbackScreenProps, FeedbackScreenParams } from './presentation/screens/FeedbackScreen';
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Agnostic of UI implementation via render props
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, {
|
|
7
|
+
import React, { useCallback } from "react";
|
|
8
8
|
import { Linking } from "react-native";
|
|
9
|
-
import {
|
|
9
|
+
import { useAppNavigation } from "@umituz/react-native-design-system/molecules";
|
|
10
10
|
import type { FeedbackType } from "../../domain/entities/FeedbackEntity";
|
|
11
11
|
import { isDev } from "../../../../utils/devUtils";
|
|
12
12
|
|
|
@@ -47,7 +47,7 @@ export interface SupportSectionProps {
|
|
|
47
47
|
onPress: () => void;
|
|
48
48
|
isLast?: boolean
|
|
49
49
|
}) => React.ReactElement | null;
|
|
50
|
-
/** Texts for the feedback
|
|
50
|
+
/** Texts for the feedback screen */
|
|
51
51
|
feedbackModalTexts?: FeedbackModalTexts;
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -58,27 +58,19 @@ export const SupportSection: React.FC<SupportSectionProps> = ({
|
|
|
58
58
|
renderItem,
|
|
59
59
|
feedbackModalTexts
|
|
60
60
|
}) => {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
console.error('[SupportSection] Failed to submit feedback:', error);
|
|
73
|
-
}
|
|
74
|
-
setModalVisible(false);
|
|
75
|
-
} finally {
|
|
76
|
-
setIsSubmitting(false);
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
setModalVisible(false);
|
|
61
|
+
const navigation = useAppNavigation();
|
|
62
|
+
|
|
63
|
+
const handleFeedbackPress = useCallback(() => {
|
|
64
|
+
if (feedbackConfig.config?.onPress) {
|
|
65
|
+
feedbackConfig.config.onPress();
|
|
66
|
+
} else if (feedbackModalTexts) {
|
|
67
|
+
navigation.push('Feedback' as never, {
|
|
68
|
+
initialType: feedbackConfig.config?.initialType,
|
|
69
|
+
title: feedbackModalTexts.title,
|
|
70
|
+
texts: feedbackModalTexts,
|
|
71
|
+
});
|
|
80
72
|
}
|
|
81
|
-
};
|
|
73
|
+
}, [navigation, feedbackConfig.config, feedbackModalTexts]);
|
|
82
74
|
|
|
83
75
|
const handleRateApp = useCallback(async () => {
|
|
84
76
|
const config = ratingConfig.config;
|
|
@@ -122,7 +114,7 @@ export const SupportSection: React.FC<SupportSectionProps> = ({
|
|
|
122
114
|
{showFeedback && feedbackConfig.config?.description && renderItem({
|
|
123
115
|
title: feedbackConfig.config.description,
|
|
124
116
|
icon: "mail",
|
|
125
|
-
onPress:
|
|
117
|
+
onPress: handleFeedbackPress,
|
|
126
118
|
isLast: !showRating
|
|
127
119
|
})}
|
|
128
120
|
|
|
@@ -135,25 +127,6 @@ export const SupportSection: React.FC<SupportSectionProps> = ({
|
|
|
135
127
|
</>
|
|
136
128
|
)
|
|
137
129
|
})}
|
|
138
|
-
|
|
139
|
-
{showFeedback && feedbackModalTexts && (
|
|
140
|
-
<FeedbackModal
|
|
141
|
-
visible={modalVisible}
|
|
142
|
-
onClose={() => setModalVisible(false)}
|
|
143
|
-
onSubmit={handleFeedbackSubmit}
|
|
144
|
-
initialType={feedbackConfig.config?.initialType}
|
|
145
|
-
isSubmitting={isSubmitting}
|
|
146
|
-
title={feedbackModalTexts.title}
|
|
147
|
-
texts={{
|
|
148
|
-
ratingLabel: feedbackModalTexts.ratingLabel,
|
|
149
|
-
descriptionPlaceholder: feedbackModalTexts.descriptionPlaceholder,
|
|
150
|
-
submitButton: feedbackModalTexts.submitButton,
|
|
151
|
-
submittingButton: feedbackModalTexts.submittingButton,
|
|
152
|
-
feedbackTypes: feedbackModalTexts.feedbackTypes,
|
|
153
|
-
defaultTitle: feedbackModalTexts.defaultTitle,
|
|
154
|
-
}}
|
|
155
|
-
/>
|
|
156
|
-
)}
|
|
157
130
|
</>
|
|
158
131
|
);
|
|
159
132
|
};
|
|
@@ -16,7 +16,7 @@ import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
|
|
|
16
16
|
import { FeedbackModal } from "../components/FeedbackModal";
|
|
17
17
|
import { ICON_PATHS } from "../../../../utils/iconPaths";
|
|
18
18
|
import { useFeatureRequests } from "../../infrastructure/useFeatureRequests";
|
|
19
|
-
import type { FeedbackRating } from "../../domain/entities/FeedbackEntity";
|
|
19
|
+
import type { FeedbackType, FeedbackRating } from "../../domain/entities/FeedbackEntity";
|
|
20
20
|
import type { FeatureRequestItem } from "../../domain/entities/FeatureRequestEntity";
|
|
21
21
|
import type { FeedbackFormTexts } from "../components/FeedbackFormProps";
|
|
22
22
|
|
|
@@ -56,14 +56,14 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
56
56
|
dismissed: t.status?.dismissed || "Dismissed",
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
const handleSubmit = useCallback(async (data: { title?: string; description: string; type?: string; rating?:
|
|
59
|
+
const handleSubmit = useCallback(async (data: { title?: string; description: string; type?: string; rating?: number }) => {
|
|
60
60
|
setIsSubmitting(true);
|
|
61
61
|
try {
|
|
62
62
|
await submitRequest({
|
|
63
63
|
title: data.title || "New Request",
|
|
64
64
|
description: data.description,
|
|
65
|
-
type: data.type || "feature_request",
|
|
66
|
-
rating: data.rating,
|
|
65
|
+
type: (data.type || "feature_request") as FeedbackType,
|
|
66
|
+
rating: data.rating as FeedbackRating,
|
|
67
67
|
});
|
|
68
68
|
setIsModalVisible(false);
|
|
69
69
|
} catch (error) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Screen
|
|
3
|
+
*
|
|
4
|
+
* Full screen for submitting feedback.
|
|
5
|
+
* Replaces modal approach with native navigation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { StyleSheet } from 'react-native';
|
|
10
|
+
import { ScreenLayout } from '@umituz/react-native-design-system/layouts';
|
|
11
|
+
import { NavigationHeader } from '@umituz/react-native-design-system/molecules';
|
|
12
|
+
import { FeedbackForm } from '../components/FeedbackForm';
|
|
13
|
+
import type { FeedbackType } from '../../domain/entities/FeedbackEntity';
|
|
14
|
+
import type { FeedbackFormProps } from '../components/FeedbackFormProps';
|
|
15
|
+
import { useAppNavigation } from '@umituz/react-native-design-system/molecules';
|
|
16
|
+
|
|
17
|
+
export interface FeedbackScreenParams {
|
|
18
|
+
initialType?: FeedbackType;
|
|
19
|
+
title?: string;
|
|
20
|
+
texts: FeedbackFormProps['texts'];
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FeedbackScreenProps {
|
|
25
|
+
route: {
|
|
26
|
+
params: FeedbackScreenParams;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const FeedbackScreen: React.FC<FeedbackScreenProps> = ({ route }) => {
|
|
31
|
+
const navigation = useAppNavigation();
|
|
32
|
+
const { initialType, title, texts } = route.params;
|
|
33
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
34
|
+
|
|
35
|
+
const handleSubmit = async (_data: { type: FeedbackType; rating: number; description: string; title: string }) => {
|
|
36
|
+
setIsSubmitting(true);
|
|
37
|
+
try {
|
|
38
|
+
// Navigate back with result
|
|
39
|
+
navigation.goBack();
|
|
40
|
+
// Note: In a real app, you'd emit an event or call a callback here
|
|
41
|
+
// For now, we'll just close the screen
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('[FeedbackScreen] Submit failed:', error);
|
|
44
|
+
} finally {
|
|
45
|
+
setIsSubmitting(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<ScreenLayout
|
|
51
|
+
scrollable={true}
|
|
52
|
+
edges={['top', 'bottom', 'left', 'right']}
|
|
53
|
+
keyboardAvoiding={true}
|
|
54
|
+
hideScrollIndicator={false}
|
|
55
|
+
contentContainerStyle={styles.content}
|
|
56
|
+
>
|
|
57
|
+
<NavigationHeader
|
|
58
|
+
title={title || 'Feedback'}
|
|
59
|
+
onBackPress={() => navigation.goBack()}
|
|
60
|
+
/>
|
|
61
|
+
<FeedbackForm
|
|
62
|
+
onSubmit={handleSubmit}
|
|
63
|
+
initialType={initialType}
|
|
64
|
+
isSubmitting={isSubmitting}
|
|
65
|
+
texts={texts}
|
|
66
|
+
/>
|
|
67
|
+
</ScreenLayout>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const styles = StyleSheet.create({
|
|
72
|
+
content: {
|
|
73
|
+
padding: 20,
|
|
74
|
+
},
|
|
75
|
+
});
|