@umituz/react-native-settings 5.4.11 → 5.4.13
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/domains/appearance/presentation/screens/AppearanceScreen.tsx +4 -0
- package/src/domains/faqs/presentation/screens/FAQScreen.tsx +7 -3
- package/src/domains/feedback/presentation/components/SupportSection.tsx +0 -1
- package/src/domains/feedback/presentation/screens/FeatureRequestScreen.tsx +21 -44
- package/src/domains/localization/presentation/components/LanguageItem.tsx +2 -2
- package/src/domains/notifications/reminders/presentation/components/ReminderItem.tsx +2 -2
- package/src/domains/rating/presentation/hooks/useAppRating.tsx +1 -1
- package/src/domains/video-tutorials/presentation/components/VideoTutorialCard.tsx +10 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.13",
|
|
4
4
|
"description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification - expo-store-review and expo-device now lazy loaded",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -197,6 +197,10 @@ export const AppearanceScreen: React.FC<AppearanceScreenProps> = ({
|
|
|
197
197
|
contentContainerStyle={{
|
|
198
198
|
paddingBottom: tokens.spacing['2xl'],
|
|
199
199
|
}}
|
|
200
|
+
removeClippedSubviews={true}
|
|
201
|
+
maxToRenderPerBatch={5}
|
|
202
|
+
windowSize={3}
|
|
203
|
+
initialNumToRender={3}
|
|
200
204
|
/>
|
|
201
205
|
</ScreenLayout>
|
|
202
206
|
);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Uses design system tokens for theming
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, { useMemo } from 'react';
|
|
7
|
+
import React, { useMemo, useCallback } from 'react';
|
|
8
8
|
import { View, StyleSheet, ViewStyle, TextStyle, useWindowDimensions, FlatList } from 'react-native';
|
|
9
9
|
import { getContentMaxWidth } from '@umituz/react-native-design-system/device';
|
|
10
10
|
import { ScreenLayout } from '@umituz/react-native-design-system/layouts';
|
|
@@ -102,7 +102,7 @@ export const FAQScreen: React.FC<FAQScreenProps> = ({
|
|
|
102
102
|
</View>
|
|
103
103
|
), [searchQuery, hasResults, searchPlaceholder, emptySearchTitle, emptySearchMessage, customStyles, tokens, contentMaxWidth]);
|
|
104
104
|
|
|
105
|
-
const renderCategory = ({ item }: { item: FAQCategory }) => (
|
|
105
|
+
const renderCategory = useCallback(({ item }: { item: FAQCategory }) => (
|
|
106
106
|
<View style={{ alignSelf: 'center', width: '100%', maxWidth: contentMaxWidth }}>
|
|
107
107
|
<FAQCategoryComponent
|
|
108
108
|
category={item}
|
|
@@ -111,7 +111,7 @@ export const FAQScreen: React.FC<FAQScreenProps> = ({
|
|
|
111
111
|
styles={customStyles?.category}
|
|
112
112
|
/>
|
|
113
113
|
</View>
|
|
114
|
-
);
|
|
114
|
+
), [contentMaxWidth, isExpanded, toggleExpansion, customStyles?.category]);
|
|
115
115
|
|
|
116
116
|
return (
|
|
117
117
|
<ScreenLayout
|
|
@@ -128,6 +128,10 @@ export const FAQScreen: React.FC<FAQScreenProps> = ({
|
|
|
128
128
|
ListFooterComponent={<View style={styles.footer} />}
|
|
129
129
|
showsVerticalScrollIndicator={false}
|
|
130
130
|
contentContainerStyle={{ paddingBottom: tokens.spacing.xl }}
|
|
131
|
+
removeClippedSubviews={true}
|
|
132
|
+
maxToRenderPerBatch={10}
|
|
133
|
+
windowSize={5}
|
|
134
|
+
initialNumToRender={8}
|
|
131
135
|
/>
|
|
132
136
|
</ScreenLayout>
|
|
133
137
|
);
|
|
@@ -8,7 +8,6 @@ import React, { useCallback } from "react";
|
|
|
8
8
|
import { Linking } from "react-native";
|
|
9
9
|
import { useAppNavigation } from "@umituz/react-native-design-system/molecules";
|
|
10
10
|
import type { FeedbackType } from "../../domain/entities/FeedbackEntity";
|
|
11
|
-
import { isDev } from "../../../../utils/devUtils";
|
|
12
11
|
|
|
13
12
|
export interface FeedbackConfig {
|
|
14
13
|
enabled?: boolean;
|
|
@@ -11,12 +11,11 @@ import {
|
|
|
11
11
|
AtomicIcon,
|
|
12
12
|
} from "@umituz/react-native-design-system/atoms";
|
|
13
13
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
14
|
-
import {
|
|
14
|
+
import { useAppNavigation } from "@umituz/react-native-design-system/molecules";
|
|
15
15
|
import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
|
|
16
|
-
import { FeedbackModal } from "../components/FeedbackModal";
|
|
17
16
|
import { ICON_PATHS } from "../../../../utils/iconPaths";
|
|
18
17
|
import { useFeatureRequests } from "../../infrastructure/useFeatureRequests";
|
|
19
|
-
import type { FeedbackType
|
|
18
|
+
import type { FeedbackType } from "../../domain/entities/FeedbackEntity";
|
|
20
19
|
import type { FeatureRequestItem } from "../../domain/entities/FeatureRequestEntity";
|
|
21
20
|
import type { FeedbackFormTexts } from "../components/FeedbackFormProps";
|
|
22
21
|
|
|
@@ -29,11 +28,10 @@ interface FeatureRequestScreenProps {
|
|
|
29
28
|
|
|
30
29
|
export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ config, texts }) => {
|
|
31
30
|
const tokens = useAppDesignTokens();
|
|
32
|
-
const
|
|
31
|
+
const navigation = useAppNavigation();
|
|
32
|
+
const { requests, userVotes, isLoading, vote, submitRequest: _submitRequest, userId } = useFeatureRequests();
|
|
33
33
|
|
|
34
34
|
const [activeTab, setActiveTab] = useState<'all' | 'my' | 'roadmap'>('all');
|
|
35
|
-
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
36
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
37
35
|
|
|
38
36
|
const t = config?.translations || {};
|
|
39
37
|
const screenTitle = t.screen_title || "Feedback & Features";
|
|
@@ -42,38 +40,21 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
42
40
|
const bannerSub = t.banner?.subtitle || `${requests.length} feature requests`;
|
|
43
41
|
const newIdeaLabel = t.new_idea || "NEW IDEA?";
|
|
44
42
|
|
|
45
|
-
const tabLabels = {
|
|
43
|
+
const tabLabels = useMemo(() => ({
|
|
46
44
|
all: t.tabs?.all || "All Requests",
|
|
47
45
|
my: t.tabs?.my || "My Feedback",
|
|
48
46
|
roadmap: t.tabs?.roadmap || "Roadmap",
|
|
49
|
-
};
|
|
47
|
+
}), [t.tabs?.all, t.tabs?.my, t.tabs?.roadmap]);
|
|
50
48
|
|
|
51
|
-
const statusLabels
|
|
49
|
+
const statusLabels = useMemo(() => ({
|
|
52
50
|
planned: t.status?.planned || "Planned",
|
|
53
51
|
review: t.status?.review || "Under Review",
|
|
54
52
|
completed: t.status?.completed || "Completed",
|
|
55
53
|
pending: t.status?.pending || "Pending",
|
|
56
54
|
dismissed: t.status?.dismissed || "Dismissed",
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const handleSubmit = useCallback(async (data: { title?: string; description: string; type?: string; rating?: number }) => {
|
|
60
|
-
setIsSubmitting(true);
|
|
61
|
-
try {
|
|
62
|
-
await submitRequest({
|
|
63
|
-
title: data.title || "New Request",
|
|
64
|
-
description: data.description,
|
|
65
|
-
type: (data.type || "feature_request") as FeedbackType,
|
|
66
|
-
rating: data.rating as FeedbackRating,
|
|
67
|
-
});
|
|
68
|
-
setIsModalVisible(false);
|
|
69
|
-
} catch (error) {
|
|
70
|
-
devWarn("[FeatureRequestScreen] Submit failed:", error);
|
|
71
|
-
} finally {
|
|
72
|
-
setIsSubmitting(false);
|
|
73
|
-
}
|
|
74
|
-
}, [submitRequest]);
|
|
55
|
+
}), [t.status?.planned, t.status?.review, t.status?.completed, t.status?.pending, t.status?.dismissed]);
|
|
75
56
|
|
|
76
|
-
const getStatusColor = (status: string) => {
|
|
57
|
+
const getStatusColor = useCallback((status: string) => {
|
|
77
58
|
switch (status) {
|
|
78
59
|
case 'planned': return '#3b82f6';
|
|
79
60
|
case 'review': return '#f59e0b';
|
|
@@ -82,7 +63,7 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
82
63
|
case 'dismissed': return '#ef4444';
|
|
83
64
|
default: return tokens.colors.textSecondary;
|
|
84
65
|
}
|
|
85
|
-
};
|
|
66
|
+
}, [tokens.colors.textSecondary]);
|
|
86
67
|
|
|
87
68
|
const filteredRequests = useMemo(() => {
|
|
88
69
|
switch (activeTab) {
|
|
@@ -145,7 +126,7 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
145
126
|
</View>
|
|
146
127
|
</View>
|
|
147
128
|
);
|
|
148
|
-
}, [userVotes, vote, tokens.colors, getStatusColor, statusLabels, t]);
|
|
129
|
+
}, [userVotes, vote, tokens.colors, getStatusColor, statusLabels, t.comment_count]);
|
|
149
130
|
|
|
150
131
|
const tabs = useMemo(() => (['all', 'my', 'roadmap'] as const), []);
|
|
151
132
|
|
|
@@ -154,7 +135,11 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
154
135
|
<AtomicText style={styles.headerTitle}>{screenTitle}</AtomicText>
|
|
155
136
|
<TouchableOpacity
|
|
156
137
|
style={[styles.addButton, { backgroundColor: tokens.colors.primary }]}
|
|
157
|
-
onPress={() =>
|
|
138
|
+
onPress={() => navigation.push('Feedback' as never, {
|
|
139
|
+
initialType: 'feature_request' as FeedbackType,
|
|
140
|
+
title: texts?.title,
|
|
141
|
+
texts: texts,
|
|
142
|
+
})}
|
|
158
143
|
>
|
|
159
144
|
<AtomicIcon
|
|
160
145
|
svgPath={ICON_PATHS['plus']}
|
|
@@ -236,22 +221,14 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
236
221
|
windowSize={5}
|
|
237
222
|
/>
|
|
238
223
|
|
|
239
|
-
{isModalVisible && (
|
|
240
|
-
<FeedbackModal
|
|
241
|
-
visible={isModalVisible}
|
|
242
|
-
onClose={() => setIsModalVisible(false)}
|
|
243
|
-
title={texts?.title}
|
|
244
|
-
onSubmit={handleSubmit}
|
|
245
|
-
texts={texts}
|
|
246
|
-
initialType="feature_request"
|
|
247
|
-
isSubmitting={isSubmitting}
|
|
248
|
-
/>
|
|
249
|
-
)}
|
|
250
|
-
|
|
251
224
|
<View style={styles.floatingHint}>
|
|
252
225
|
<TouchableOpacity
|
|
253
226
|
style={[styles.hintBadge, { backgroundColor: tokens.colors.primary }]}
|
|
254
|
-
onPress={() =>
|
|
227
|
+
onPress={() => navigation.push('Feedback' as never, {
|
|
228
|
+
initialType: 'feature_request' as FeedbackType,
|
|
229
|
+
title: texts?.title,
|
|
230
|
+
texts: texts,
|
|
231
|
+
})}
|
|
255
232
|
>
|
|
256
233
|
<AtomicText style={styles.hintText}>{newIdeaLabel}</AtomicText>
|
|
257
234
|
</TouchableOpacity>
|
|
@@ -35,7 +35,7 @@ interface LanguageItemProps {
|
|
|
35
35
|
// SVG path for checkmark icon (works without external icon library)
|
|
36
36
|
const CHECKMARK_PATH = "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z";
|
|
37
37
|
|
|
38
|
-
export const LanguageItem: React.FC<LanguageItemProps> = ({
|
|
38
|
+
export const LanguageItem: React.FC<LanguageItemProps> = React.memo(({
|
|
39
39
|
item,
|
|
40
40
|
isSelected,
|
|
41
41
|
onSelect,
|
|
@@ -135,5 +135,5 @@ export const LanguageItem: React.FC<LanguageItemProps> = ({
|
|
|
135
135
|
)}
|
|
136
136
|
</TouchableOpacity>
|
|
137
137
|
);
|
|
138
|
-
};
|
|
138
|
+
});
|
|
139
139
|
|
|
@@ -39,7 +39,7 @@ const getFrequencyIcon = (frequency: ReminderFrequency): string => {
|
|
|
39
39
|
return icons[frequency] || 'notifications';
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
export const ReminderItem: React.FC<ReminderItemProps> = ({
|
|
42
|
+
export const ReminderItem: React.FC<ReminderItemProps> = React.memo(({
|
|
43
43
|
reminder,
|
|
44
44
|
translations,
|
|
45
45
|
onToggle,
|
|
@@ -97,7 +97,7 @@ export const ReminderItem: React.FC<ReminderItemProps> = ({
|
|
|
97
97
|
</View>
|
|
98
98
|
</View>
|
|
99
99
|
);
|
|
100
|
-
};
|
|
100
|
+
});
|
|
101
101
|
|
|
102
102
|
const createStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
103
103
|
StyleSheet.create({
|
|
@@ -3,31 +3,31 @@
|
|
|
3
3
|
* Single Responsibility: Display individual video tutorial card
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React from "react";
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
7
|
import { View, Image, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
8
|
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
10
|
import type { VideoTutorial } from "../../types";
|
|
11
11
|
|
|
12
|
+
const formatDuration = (seconds: number): string => {
|
|
13
|
+
const minutes = Math.floor(seconds / 60);
|
|
14
|
+
const remainingSeconds = seconds % 60;
|
|
15
|
+
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
|
|
16
|
+
};
|
|
17
|
+
|
|
12
18
|
interface VideoTutorialCardProps {
|
|
13
19
|
readonly tutorial: VideoTutorial;
|
|
14
20
|
readonly onPress: () => void;
|
|
15
21
|
readonly horizontal?: boolean;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
export const VideoTutorialCard: React.FC<VideoTutorialCardProps> = ({
|
|
24
|
+
export const VideoTutorialCard: React.FC<VideoTutorialCardProps> = React.memo(({
|
|
19
25
|
tutorial,
|
|
20
26
|
onPress,
|
|
21
27
|
horizontal = false,
|
|
22
28
|
}) => {
|
|
23
29
|
const tokens = useAppDesignTokens();
|
|
24
|
-
const styles = getStyles(tokens);
|
|
25
|
-
|
|
26
|
-
const formatDuration = (seconds: number): string => {
|
|
27
|
-
const minutes = Math.floor(seconds / 60);
|
|
28
|
-
const remainingSeconds = seconds % 60;
|
|
29
|
-
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
|
|
30
|
-
};
|
|
30
|
+
const styles = useMemo(() => getStyles(tokens), [tokens]);
|
|
31
31
|
|
|
32
32
|
return (
|
|
33
33
|
<TouchableOpacity
|
|
@@ -84,7 +84,7 @@ export const VideoTutorialCard: React.FC<VideoTutorialCardProps> = ({
|
|
|
84
84
|
</View>
|
|
85
85
|
</TouchableOpacity>
|
|
86
86
|
);
|
|
87
|
-
};
|
|
87
|
+
});
|
|
88
88
|
|
|
89
89
|
const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) => StyleSheet.create({
|
|
90
90
|
container: { borderRadius: 12, borderWidth: 1, marginBottom: 12, overflow: "hidden" },
|