@umituz/react-native-settings 5.3.38 → 5.3.40

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.
@@ -16,11 +16,21 @@ export declare const getFeedbackFormStyles: (_tokens: ReturnType<typeof useAppDe
16
16
  paddingVertical: number;
17
17
  borderRadius: number;
18
18
  borderWidth: number;
19
- gap: number;
19
+ marginRight: number;
20
20
  };
21
21
  ratingContainer: {
22
22
  alignItems: "center";
23
+ marginVertical: number;
24
+ paddingVertical: number;
25
+ backgroundColor: string;
26
+ borderRadius: number;
27
+ };
28
+ ratingLabel: {
29
+ fontSize: number;
30
+ fontWeight: "900";
31
+ letterSpacing: number;
23
32
  marginBottom: number;
33
+ textTransform: "uppercase";
24
34
  };
25
35
  stars: {
26
36
  flexDirection: "row";
@@ -38,11 +48,15 @@ export declare const getFeedbackFormStyles: (_tokens: ReturnType<typeof useAppDe
38
48
  borderWidth: number;
39
49
  borderRadius: number;
40
50
  padding: number;
51
+ fontSize: number;
41
52
  };
42
53
  errorText: {
43
54
  marginTop: number;
55
+ fontWeight: "600";
44
56
  };
45
57
  submitButton: {
46
58
  width: "100%";
59
+ height: number;
60
+ borderRadius: number;
47
61
  };
48
62
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-settings",
3
- "version": "5.3.38",
3
+ "version": "5.3.40",
4
4
  "description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Feature Request Entity
3
+ * Represents a community feature request that all users can see and vote on
4
+ */
5
+
6
+ export type FeatureRequestStatus = 'pending' | 'planned' | 'review' | 'completed' | 'dismissed';
7
+
8
+ export type VoteType = 'up' | 'down';
9
+
10
+ export interface FeatureRequestItem {
11
+ id: string;
12
+ title: string;
13
+ description: string;
14
+ type: string;
15
+ status: FeatureRequestStatus;
16
+ votes: number;
17
+ commentCount: number;
18
+ createdBy: string;
19
+ isAnonymous: boolean;
20
+ platform: string;
21
+ createdAt: string;
22
+ updatedAt: string;
23
+ }
24
+
25
+ export interface FeatureRequestVote {
26
+ type: VoteType;
27
+ votedAt: string;
28
+ }
29
+
30
+ export interface FeatureRequestSubmitData {
31
+ title: string;
32
+ description: string;
33
+ type: string;
34
+ rating?: number;
35
+ }
36
+
37
+ export interface FeatureRequestDataProvider {
38
+ /** Fetch all feature requests */
39
+ fetchRequests: () => Promise<FeatureRequestItem[]>;
40
+ /** Fetch current user's votes */
41
+ fetchUserVotes: (userId: string) => Promise<Record<string, VoteType>>;
42
+ /** Vote on a feature request */
43
+ onVote: (requestId: string, userId: string, type: VoteType) => Promise<void>;
44
+ /** Submit a new feature request */
45
+ onSubmit: (data: FeatureRequestSubmitData) => Promise<void>;
46
+ /** Current user ID (can be anonymous UID) */
47
+ userId: string | null;
48
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Feedback Domain
3
- * User feedback, bug reports
3
+ * User feedback, bug reports, feature requests
4
4
  */
5
5
 
6
6
  export * from './presentation/components/FeedbackForm';
@@ -8,4 +8,5 @@ export * from './presentation/components/FeedbackModal';
8
8
  export * from './presentation/components/SupportSection';
9
9
  export * from './presentation/hooks/useFeedbackForm';
10
10
  export * from './domain/entities/FeedbackEntity';
11
+ export * from './domain/entities/FeatureRequestEntity';
11
12
  export * from './domain/repositories/IFeedbackRepository';
@@ -15,37 +15,51 @@ export const getFeedbackFormStyles = (_tokens: ReturnType<typeof useAppDesignTok
15
15
  typeButton: {
16
16
  flexDirection: "row",
17
17
  alignItems: "center",
18
- paddingHorizontal: 16,
19
- paddingVertical: 8,
20
- borderRadius: 20,
21
- borderWidth: 1,
22
- gap: 6,
18
+ paddingHorizontal: 20,
19
+ paddingVertical: 10,
20
+ borderRadius: 24,
21
+ borderWidth: 1.5,
22
+ marginRight: 10,
23
23
  },
24
24
  ratingContainer: {
25
25
  alignItems: "center",
26
- marginBottom: 24,
26
+ marginVertical: 32,
27
+ paddingVertical: 16,
28
+ backgroundColor: "rgba(255,255,255,0.02)",
29
+ borderRadius: 16,
30
+ },
31
+ ratingLabel: {
32
+ fontSize: 12,
33
+ fontWeight: "900",
34
+ letterSpacing: 1,
35
+ marginBottom: 16,
36
+ textTransform: "uppercase",
27
37
  },
28
38
  stars: {
29
39
  flexDirection: "row",
30
- gap: 8,
40
+ gap: 12,
31
41
  },
32
42
  starButton: {
33
43
  padding: 4,
34
44
  },
35
45
  inputContainer: {
36
- marginBottom: 24,
46
+ marginBottom: 32,
37
47
  },
38
48
  textArea: {
39
49
  textAlignVertical: "top",
40
- minHeight: 120,
41
- borderWidth: 1,
42
- borderRadius: 8,
43
- padding: 12,
50
+ minHeight: 140,
51
+ borderWidth: 1.5,
52
+ borderRadius: 16,
53
+ padding: 16,
54
+ fontSize: 15,
44
55
  },
45
56
  errorText: {
46
57
  marginTop: 8,
58
+ fontWeight: "600",
47
59
  },
48
60
  submitButton: {
49
61
  width: "100%",
62
+ height: 56,
63
+ borderRadius: 16,
50
64
  },
51
65
  });
@@ -67,7 +67,7 @@ const FeedbackRatingSection: React.FC<FeedbackRatingSectionProps> = ({
67
67
  tokens,
68
68
  }) => (
69
69
  <View style={styles.ratingContainer}>
70
- <AtomicText type="bodyMedium" style={{ marginBottom: 8, color: tokens.colors.textSecondary }}>
70
+ <AtomicText style={[styles.ratingLabel, { color: tokens.colors.textSecondary }]}>
71
71
  {ratingLabel}
72
72
  </AtomicText>
73
73
  <View style={styles.stars}>
@@ -1,11 +1,11 @@
1
- import React, { useState, useMemo } from "react";
1
+ import React, { useState, useEffect, useCallback } from "react";
2
2
  import {
3
3
  View,
4
4
  StyleSheet,
5
5
  TouchableOpacity,
6
6
  ScrollView,
7
7
  FlatList,
8
- Image,
8
+ ActivityIndicator,
9
9
  } from "react-native";
10
10
  import {
11
11
  AtomicText,
@@ -15,177 +15,230 @@ import {
15
15
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
16
16
  import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
17
17
  import { FeedbackModal } from "../components/FeedbackModal";
18
- import type { FeedbackType, FeedbackRating } from "../../domain/entities/FeedbackEntity";
19
-
20
- interface FeatureRequest {
21
- id: string;
22
- title: string;
23
- description: string;
24
- votes: number;
25
- status: 'planned' | 'review' | 'completed';
26
- comments: number;
27
- userAvatars: string[];
28
- voted: 'up' | 'down' | null;
29
- }
18
+ import type { FeedbackType } from "../../domain/entities/FeedbackEntity";
19
+ import type {
20
+ FeatureRequestItem,
21
+ FeatureRequestDataProvider,
22
+ VoteType,
23
+ } from "../../domain/entities/FeatureRequestEntity";
30
24
 
31
- const MOCK_REQUESTS: FeatureRequest[] = [
32
- {
33
- id: "1",
34
- title: "AI Music Generation",
35
- description: "Add the ability to generate background tracks for videos using simple text prompts or mood selectors.",
36
- votes: 1250,
37
- status: 'planned',
38
- comments: 24,
39
- userAvatars: ["https://i.pravatar.cc/100?u=1", "https://i.pravatar.cc/100?u=2"],
40
- voted: null,
41
- },
42
- {
43
- id: "2",
44
- title: "Couple Video Templates",
45
- description: "Specific cinematic transition templates designed for couples' vlogs and anniversary montages.",
46
- votes: 842,
47
- status: 'review',
48
- comments: 8,
49
- userAvatars: ["https://i.pravatar.cc/100?u=3"],
50
- voted: null,
51
- },
52
- {
53
- id: "3",
54
- title: "4K Export Support",
55
- description: "Allow users to export generated AI videos in high-quality 4K resolution.",
56
- votes: 2100,
57
- status: 'completed',
58
- comments: 42,
59
- userAvatars: ["https://i.pravatar.cc/100?u=4", "https://i.pravatar.cc/100?u=5"],
60
- voted: null,
61
- },
62
- {
63
- id: "4",
64
- title: "Dark Mode Editor",
65
- description: "Make the entire editing interface dark for better focus during late-night creative sessions.",
66
- votes: 156,
67
- status: 'planned',
68
- comments: 5,
69
- userAvatars: ["https://i.pravatar.cc/100?u=6"],
70
- voted: null,
71
- }
72
- ];
25
+ interface FeatureRequestScreenProps {
26
+ config?: {
27
+ translations?: Record<string, any>;
28
+ dataProvider?: FeatureRequestDataProvider;
29
+ onSubmit?: (data: any) => Promise<void>;
30
+ };
31
+ texts: any;
32
+ }
73
33
 
74
- export const FeatureRequestScreen: React.FC<any> = ({ config, texts }) => {
34
+ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ config, texts }) => {
75
35
  const tokens = useAppDesignTokens();
76
- const [requests, setRequests] = useState(MOCK_REQUESTS);
36
+ const dataProvider = config?.dataProvider;
37
+
38
+ const [requests, setRequests] = useState<FeatureRequestItem[]>([]);
39
+ const [userVotes, setUserVotes] = useState<Record<string, VoteType>>({});
40
+ const [isLoading, setIsLoading] = useState(true);
77
41
  const [activeTab, setActiveTab] = useState<'all' | 'my' | 'roadmap'>('all');
78
42
  const [isModalVisible, setIsModalVisible] = useState(false);
43
+ const [isSubmitting, setIsSubmitting] = useState(false);
79
44
 
80
- // Use passed translations if available
81
45
  const t = config?.translations || {};
82
46
  const screenTitle = t.screen_title || "Feedback & Features";
83
47
  const trendingTitle = t.trending || "Trending Requests";
84
48
  const bannerTitle = t.banner?.title || "Community Active";
85
- const bannerSub = t.banner?.subtitle || "1.2k users voting right now";
49
+ const bannerSub = t.banner?.subtitle || `${requests.length} feature requests`;
86
50
  const newIdeaLabel = t.new_idea || "NEW IDEA?";
87
-
51
+
88
52
  const tabLabels = {
89
53
  all: t.tabs?.all || "All Requests",
90
54
  my: t.tabs?.my || "My Feedback",
91
55
  roadmap: t.tabs?.roadmap || "Roadmap",
92
56
  };
93
57
 
94
- const statusLabels = {
58
+ const statusLabels: Record<string, string> = {
95
59
  planned: t.status?.planned || "Planned",
96
60
  review: t.status?.review || "Under Review",
97
61
  completed: t.status?.completed || "Completed",
62
+ pending: t.status?.pending || "Pending",
63
+ dismissed: t.status?.dismissed || "Dismissed",
98
64
  };
99
65
 
100
- const handleVote = (id: string, type: 'up' | 'down') => {
66
+ const loadData = useCallback(async () => {
67
+ if (!dataProvider) {
68
+ setIsLoading(false);
69
+ return;
70
+ }
71
+
72
+ try {
73
+ setIsLoading(true);
74
+ const [fetchedRequests, fetchedVotes] = await Promise.all([
75
+ dataProvider.fetchRequests(),
76
+ dataProvider.userId
77
+ ? dataProvider.fetchUserVotes(dataProvider.userId)
78
+ : Promise.resolve({} as Record<string, VoteType>),
79
+ ]);
80
+ setRequests(fetchedRequests);
81
+ setUserVotes(fetchedVotes);
82
+ } catch (error) {
83
+ if (__DEV__) console.warn("[FeatureRequestScreen] Failed to load data:", error);
84
+ } finally {
85
+ setIsLoading(false);
86
+ }
87
+ }, [dataProvider]);
88
+
89
+ useEffect(() => {
90
+ loadData();
91
+ }, [loadData]);
92
+
93
+ const handleVote = useCallback(async (id: string, type: VoteType) => {
94
+ if (!dataProvider?.userId || !dataProvider.onVote) return;
95
+
96
+ const previousVote = userVotes[id];
97
+ const isUndoVote = previousVote === type;
98
+
99
+ // Optimistic update
100
+ setUserVotes(prev => {
101
+ const next = { ...prev };
102
+ if (isUndoVote) {
103
+ delete next[id];
104
+ } else {
105
+ next[id] = type;
106
+ }
107
+ return next;
108
+ });
109
+
101
110
  setRequests(prev => prev.map(req => {
102
- if (req.id === id) {
103
- // Simplified voting logic for mock
104
- let newVotes = req.votes;
105
- if (req.voted === type) {
106
- newVotes -= type === 'up' ? 1 : -1;
107
- return { ...req, votes: newVotes, voted: null };
108
- } else {
109
- if (req.voted !== null) {
110
- newVotes += type === 'up' ? 2 : -2;
111
- } else {
112
- newVotes += type === 'up' ? 1 : -1;
113
- }
114
- return { ...req, votes: newVotes, voted: type };
115
- }
111
+ if (req.id !== id) return req;
112
+ let delta = 0;
113
+ if (isUndoVote) {
114
+ delta = type === 'up' ? -1 : 1;
115
+ } else if (previousVote) {
116
+ delta = type === 'up' ? 2 : -2;
117
+ } else {
118
+ delta = type === 'up' ? 1 : -1;
116
119
  }
117
- return req;
120
+ return { ...req, votes: req.votes + delta };
118
121
  }));
119
- };
120
122
 
121
- const getStatusColor = (status: FeatureRequest['status']) => {
123
+ try {
124
+ await dataProvider.onVote(id, dataProvider.userId, type);
125
+ } catch (error) {
126
+ // Revert on failure
127
+ setUserVotes(prev => {
128
+ const next = { ...prev };
129
+ if (previousVote) {
130
+ next[id] = previousVote;
131
+ } else {
132
+ delete next[id];
133
+ }
134
+ return next;
135
+ });
136
+ loadData();
137
+ if (__DEV__) console.warn("[FeatureRequestScreen] Vote failed:", error);
138
+ }
139
+ }, [dataProvider, userVotes, loadData]);
140
+
141
+ const handleSubmit = useCallback(async (data: any) => {
142
+ if (!dataProvider?.onSubmit) return;
143
+
144
+ setIsSubmitting(true);
145
+ try {
146
+ await dataProvider.onSubmit({
147
+ title: data.title || "New Request",
148
+ description: data.description,
149
+ type: data.type || "feature_request",
150
+ rating: data.rating,
151
+ });
152
+ setIsModalVisible(false);
153
+ // Reload data to get the new request from the database
154
+ await loadData();
155
+ } catch (error) {
156
+ if (__DEV__) console.warn("[FeatureRequestScreen] Submit failed:", error);
157
+ } finally {
158
+ setIsSubmitting(false);
159
+ }
160
+ }, [dataProvider, loadData]);
161
+
162
+ const getStatusColor = (status: string) => {
122
163
  switch (status) {
123
164
  case 'planned': return '#3b82f6';
124
165
  case 'review': return '#f59e0b';
125
166
  case 'completed': return '#10b981';
167
+ case 'pending': return '#8b5cf6';
168
+ case 'dismissed': return '#ef4444';
126
169
  default: return tokens.colors.textSecondary;
127
170
  }
128
171
  };
129
172
 
130
- const renderRequestItem = ({ item }: { item: FeatureRequest }) => (
131
- <View style={[styles.card, { backgroundColor: tokens.colors.surfaceSecondary, borderColor: tokens.colors.borderLight }]}>
132
- <View style={styles.voteColumn}>
133
- <TouchableOpacity onPress={() => handleVote(item.id, 'up')}>
134
- <AtomicIcon
135
- name="expand-less"
136
- size="md"
137
- color={item.voted === 'up' ? "primary" : "textSecondary" as any}
138
- />
139
- </TouchableOpacity>
140
- <AtomicText style={[styles.voteCount, { color: item.voted === 'up' ? tokens.colors.primary : tokens.colors.textPrimary }]}>
141
- {item.votes}
142
- </AtomicText>
143
- <TouchableOpacity onPress={() => handleVote(item.id, 'down')}>
144
- <AtomicIcon
145
- name="expand-more"
146
- size="md"
147
- color={item.voted === 'down' ? "primary" : "textSecondary" as any}
148
- />
149
- </TouchableOpacity>
150
- </View>
173
+ const filteredRequests = (() => {
174
+ switch (activeTab) {
175
+ case 'my':
176
+ return requests.filter(r => r.createdBy === dataProvider?.userId);
177
+ case 'roadmap':
178
+ return requests.filter(r => r.status === 'planned' || r.status === 'completed' || r.status === 'review');
179
+ default:
180
+ return requests;
181
+ }
182
+ })();
151
183
 
152
- <View style={styles.cardContent}>
153
- <View style={styles.cardHeader}>
154
- <AtomicText style={styles.cardTitle}>{item.title}</AtomicText>
155
- <View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) + '20', borderColor: getStatusColor(item.status) + '40' }]}>
156
- <AtomicText style={[styles.statusText, { color: getStatusColor(item.status) }]}>
157
- {(statusLabels[item.status] || item.status).toUpperCase()}
158
- </AtomicText>
159
- </View>
184
+ const renderRequestItem = ({ item }: { item: FeatureRequestItem }) => {
185
+ const voted = userVotes[item.id] || null;
186
+
187
+ return (
188
+ <View style={[styles.card, { backgroundColor: tokens.colors.surfaceSecondary, borderColor: tokens.colors.borderLight }]}>
189
+ <View style={styles.voteColumn}>
190
+ <TouchableOpacity onPress={() => handleVote(item.id, 'up')}>
191
+ <AtomicIcon
192
+ name="chevron-up"
193
+ size="md"
194
+ color={voted === 'up' ? "primary" : "textSecondary" as any}
195
+ />
196
+ </TouchableOpacity>
197
+ <AtomicText style={[styles.voteCount, { color: voted === 'up' ? tokens.colors.primary : tokens.colors.textPrimary }]}>
198
+ {item.votes}
199
+ </AtomicText>
200
+ <TouchableOpacity onPress={() => handleVote(item.id, 'down')}>
201
+ <AtomicIcon
202
+ name="chevron-down"
203
+ size="md"
204
+ color={voted === 'down' ? "primary" : "textSecondary" as any}
205
+ />
206
+ </TouchableOpacity>
160
207
  </View>
161
-
162
- <AtomicText style={[styles.cardDescription, { color: tokens.colors.textSecondary }]}>
163
- {item.description}
164
- </AtomicText>
165
-
166
- <View style={styles.cardFooter}>
167
- <View style={styles.avatarGroup}>
168
- {item.userAvatars.map((url, i) => (
169
- <Image key={i} source={{ uri: url }} style={[styles.avatar, { borderColor: tokens.colors.surfaceSecondary }]} />
170
- ))}
171
- {item.comments > 5 && (
172
- <View style={[styles.avatarMore, { backgroundColor: tokens.colors.surfaceVariant }]}>
173
- <AtomicText style={styles.avatarMoreText}>+{item.comments}</AtomicText>
174
- </View>
175
- )}
208
+
209
+ <View style={styles.cardContent}>
210
+ <View style={styles.cardHeader}>
211
+ <AtomicText style={styles.cardTitle}>{item.title}</AtomicText>
212
+ <View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) + '20', borderColor: getStatusColor(item.status) + '40' }]}>
213
+ <AtomicText style={[styles.statusText, { color: getStatusColor(item.status) }]}>
214
+ {(statusLabels[item.status] || item.status).toUpperCase()}
215
+ </AtomicText>
216
+ </View>
176
217
  </View>
177
- <AtomicText style={[styles.commentCount, { color: tokens.colors.textTertiary }]}>
178
- {item.comments} {t.comment_count?.replace('{{count}}', '') || 'comments'}
218
+
219
+ <AtomicText style={[styles.cardDescription, { color: tokens.colors.textSecondary }]}>
220
+ {item.description}
179
221
  </AtomicText>
222
+
223
+ <View style={styles.cardFooter}>
224
+ <View style={styles.platformBadge}>
225
+ <AtomicText style={[styles.platformText, { color: tokens.colors.textTertiary }]}>
226
+ {item.platform}
227
+ </AtomicText>
228
+ </View>
229
+ <AtomicText style={[styles.commentCount, { color: tokens.colors.textTertiary }]}>
230
+ {item.commentCount} {t.comment_count?.replace('{{count}}', '') || 'comments'}
231
+ </AtomicText>
232
+ </View>
180
233
  </View>
181
234
  </View>
182
- </View>
183
- );
235
+ );
236
+ };
184
237
 
185
238
  const header = (
186
239
  <View style={styles.header}>
187
240
  <AtomicText style={styles.headerTitle}>{screenTitle}</AtomicText>
188
- <TouchableOpacity
241
+ <TouchableOpacity
189
242
  style={[styles.addButton, { backgroundColor: tokens.colors.primary }]}
190
243
  onPress={() => setIsModalVisible(true)}
191
244
  >
@@ -194,11 +247,21 @@ export const FeatureRequestScreen: React.FC<any> = ({ config, texts }) => {
194
247
  </View>
195
248
  );
196
249
 
250
+ if (isLoading) {
251
+ return (
252
+ <ScreenLayout header={header} edges={['top', 'bottom']}>
253
+ <View style={styles.loadingContainer}>
254
+ <ActivityIndicator size="large" color={tokens.colors.primary} />
255
+ </View>
256
+ </ScreenLayout>
257
+ );
258
+ }
259
+
197
260
  return (
198
261
  <ScreenLayout header={header} edges={['top', 'bottom']}>
199
262
  <View style={styles.tabsContainer}>
200
263
  {(['all', 'my', 'roadmap'] as const).map((tab) => (
201
- <TouchableOpacity
264
+ <TouchableOpacity
202
265
  key={tab}
203
266
  onPress={() => setActiveTab(tab)}
204
267
  style={[styles.tab, activeTab === tab && { borderBottomColor: tokens.colors.primary }]}
@@ -226,44 +289,43 @@ export const FeatureRequestScreen: React.FC<any> = ({ config, texts }) => {
226
289
 
227
290
  <AtomicText style={styles.sectionTitle}>{trendingTitle}</AtomicText>
228
291
 
229
- <FlatList
230
- data={requests}
231
- renderItem={renderRequestItem}
232
- keyExtractor={item => item.id}
233
- scrollEnabled={false}
234
- contentContainerStyle={styles.listContent}
235
- />
292
+ {filteredRequests.length === 0 ? (
293
+ <View style={styles.emptyState}>
294
+ <AtomicIcon name="chatbubble-outline" size="xl" color="textTertiary" />
295
+ <AtomicText style={[styles.emptyText, { color: tokens.colors.textTertiary }]}>
296
+ {t.empty || "No requests yet. Be the first!"}
297
+ </AtomicText>
298
+ </View>
299
+ ) : (
300
+ <FlatList
301
+ data={filteredRequests}
302
+ renderItem={renderRequestItem}
303
+ keyExtractor={item => item.id}
304
+ scrollEnabled={false}
305
+ contentContainerStyle={styles.listContent}
306
+ />
307
+ )}
236
308
  </ScrollView>
237
309
 
238
310
  {isModalVisible && (
239
311
  <FeedbackModal
240
312
  visible={isModalVisible}
241
313
  onClose={() => setIsModalVisible(false)}
242
- onSubmit={async (data: any) => {
243
- console.log("Submitted:", data);
244
- setIsModalVisible(false);
245
- // Add to mock list for "live" feel
246
- const newReq: FeatureRequest = {
247
- id: Date.now().toString(),
248
- title: data.title || "New Request",
249
- description: data.description,
250
- votes: 1,
251
- status: 'review',
252
- comments: 0,
253
- userAvatars: ["https://i.pravatar.cc/100?u=me"],
254
- voted: 'up',
255
- };
256
- setRequests(prev => [newReq, ...prev]);
257
- }}
314
+ title={texts?.title}
315
+ onSubmit={handleSubmit}
258
316
  texts={texts}
259
317
  initialType="feature_request"
318
+ isSubmitting={isSubmitting}
260
319
  />
261
320
  )}
262
321
 
263
322
  <View style={styles.floatingHint}>
264
- <View style={[styles.hintBadge, { backgroundColor: tokens.colors.primary }]}>
323
+ <TouchableOpacity
324
+ style={[styles.hintBadge, { backgroundColor: tokens.colors.primary }]}
325
+ onPress={() => setIsModalVisible(true)}
326
+ >
265
327
  <AtomicText style={styles.hintText}>{newIdeaLabel}</AtomicText>
266
- </View>
328
+ </TouchableOpacity>
267
329
  </View>
268
330
  </ScreenLayout>
269
331
  );
@@ -396,30 +458,14 @@ const styles = StyleSheet.create({
396
458
  justifyContent: 'space-between',
397
459
  marginTop: 4,
398
460
  },
399
- avatarGroup: {
400
- flexDirection: 'row',
401
- alignItems: 'center',
402
- },
403
- avatar: {
404
- width: 24,
405
- height: 24,
406
- borderRadius: 12,
407
- borderWidth: 2,
408
- marginLeft: -8,
409
- },
410
- avatarMore: {
411
- width: 24,
412
- height: 24,
413
- borderRadius: 12,
414
- alignItems: 'center',
415
- justifyContent: 'center',
416
- marginLeft: -8,
417
- borderWidth: 2,
418
- borderColor: 'transparent',
461
+ platformBadge: {
462
+ paddingHorizontal: 6,
463
+ paddingVertical: 2,
419
464
  },
420
- avatarMoreText: {
421
- fontSize: 8,
422
- fontWeight: '800',
465
+ platformText: {
466
+ fontSize: 10,
467
+ fontWeight: '600',
468
+ textTransform: 'uppercase',
423
469
  },
424
470
  commentCount: {
425
471
  fontSize: 11,
@@ -445,5 +491,21 @@ const styles = StyleSheet.create({
445
491
  fontSize: 10,
446
492
  fontWeight: '900',
447
493
  textTransform: 'uppercase',
448
- }
494
+ },
495
+ loadingContainer: {
496
+ flex: 1,
497
+ justifyContent: 'center',
498
+ alignItems: 'center',
499
+ paddingTop: 100,
500
+ },
501
+ emptyState: {
502
+ alignItems: 'center',
503
+ justifyContent: 'center',
504
+ paddingVertical: 60,
505
+ gap: 12,
506
+ },
507
+ emptyText: {
508
+ fontSize: 14,
509
+ fontWeight: '500',
510
+ },
449
511
  });
@@ -14,6 +14,7 @@ import { translateFAQData } from "../utils/faqTranslator";
14
14
  import { useSettingsConfigFactory } from "../utils/settingsConfigFactory";
15
15
  import type { SettingsConfig, SettingsTranslations } from "../screens/types";
16
16
  import type { FeedbackFormData } from "../utils/config-creators";
17
+ import type { FeatureRequestDataProvider } from "../../domains/feedback/domain/entities/FeatureRequestEntity";
17
18
  import type { AppInfo, FAQData, UserProfileDisplay, AdditionalScreen, AccountConfig } from "../navigation/types";
18
19
 
19
20
  export interface SettingsFeatures {
@@ -35,6 +36,7 @@ export interface UseSettingsScreenConfigParams {
35
36
  faqData?: FAQData;
36
37
  isPremium: boolean;
37
38
  onFeedbackSubmit: (data: FeedbackFormData) => Promise<void>;
39
+ featureRequestDataProvider?: FeatureRequestDataProvider;
38
40
  additionalScreens?: AdditionalScreen[];
39
41
  features?: SettingsFeatures;
40
42
  translations?: SettingsTranslations;
@@ -52,7 +54,7 @@ export interface SettingsScreenConfigResult {
52
54
  export const useSettingsScreenConfig = (
53
55
  params: UseSettingsScreenConfigParams
54
56
  ): SettingsScreenConfigResult => {
55
- const { appInfo, faqData, isPremium, onFeedbackSubmit, features = {}, translations } = params;
57
+ const { appInfo, faqData, isPremium, onFeedbackSubmit, featureRequestDataProvider, features = {}, translations } = params;
56
58
 
57
59
  const {
58
60
  notifications: showNotifications = true,
@@ -102,6 +104,15 @@ export const useSettingsScreenConfig = (
102
104
  translations,
103
105
  };
104
106
 
107
+ // Merge dataProvider into feedback config
108
+ if (featureRequestDataProvider && config.feedback) {
109
+ const existingFeedback = typeof config.feedback === 'object' ? config.feedback : { enabled: true };
110
+ config.feedback = {
111
+ ...existingFeedback,
112
+ dataProvider: featureRequestDataProvider,
113
+ };
114
+ }
115
+
105
116
  if (config.subscription && typeof config.subscription === 'object') {
106
117
  config.subscription = {
107
118
  ...config.subscription,
@@ -132,7 +143,7 @@ export const useSettingsScreenConfig = (
132
143
  }
133
144
 
134
145
  return config;
135
- }, [baseSettingsConfig, translations]);
146
+ }, [baseSettingsConfig, translations, featureRequestDataProvider]);
136
147
 
137
148
  const userProfile = useMemo(() => createUserProfileDisplay({
138
149
  profileData: userProfileData,
@@ -133,8 +133,13 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
133
133
  title: videoTutorialConfig?.title || featureTranslations?.videoTutorial?.title || "",
134
134
  });
135
135
 
136
+ const feedbackConfig = typeof config?.feedback === 'object' ? config.feedback : undefined;
136
137
  const featureRequestScreen = createScreenWithProps("FeatureRequest", FeatureRequestScreen, {
137
- config: config?.feedback,
138
+ config: {
139
+ ...feedbackConfig,
140
+ translations: translations?.feedback,
141
+ dataProvider: feedbackConfig?.dataProvider,
142
+ },
138
143
  texts: {
139
144
  feedbackTypes: [
140
145
  { type: 'general', label: translations?.feedbackModal?.types?.general || 'General' },
@@ -147,6 +152,17 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
147
152
  descriptionPlaceholder: translations?.feedbackModal?.descriptionPlaceholder || 'Tell us more...',
148
153
  submitButton: translations?.feedbackModal?.submitButton || 'Submit',
149
154
  submittingButton: translations?.feedbackModal?.submittingButton || 'Submitting...',
155
+ title: translations?.feedbackModal?.title || 'Submit Feedback',
156
+ defaultTitle: (type: string) => {
157
+ const titles: Record<string, string> = {
158
+ general: translations?.feedbackModal?.types?.general || type,
159
+ bug_report: translations?.feedbackModal?.types?.bugReport || type,
160
+ feature_request: translations?.feedbackModal?.types?.featureRequest || type,
161
+ improvement: translations?.feedbackModal?.types?.improvement || type,
162
+ other: translations?.feedbackModal?.types?.other || type,
163
+ };
164
+ return titles[type] || type;
165
+ },
150
166
  }
151
167
  });
152
168
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { FeatureVisibility } from "./BaseTypes";
7
7
  import type { FeedbackType } from "../../../domains/feedback/domain/entities/FeedbackEntity";
8
+ import type { FeatureRequestDataProvider } from "../../../domains/feedback/domain/entities/FeatureRequestEntity";
8
9
  import type { FAQCategory } from "../../../domains/faqs/domain/entities/FAQEntity";
9
10
  import type { SettingsStackParamList } from "../../navigation/types";
10
11
 
@@ -44,6 +45,8 @@ export interface FeedbackConfig {
44
45
  onSubmit?: (data: { type: FeedbackType; rating: number; description: string; title: string }) => Promise<void>;
45
46
  /** Custom handler to open feedback screen (overrides default modal) */
46
47
  onPress?: () => void;
48
+ /** Data provider for feature request screen (fetching, voting, submitting) */
49
+ dataProvider?: FeatureRequestDataProvider;
47
50
  }
48
51
 
49
52
  /**