@umituz/react-native-settings 5.3.42 → 5.3.43

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.
@@ -18,13 +18,3 @@ export interface FeatureRequestItem {
18
18
  createdAt: string;
19
19
  updatedAt: string;
20
20
  }
21
- export interface FeatureRequestVote {
22
- type: VoteType;
23
- votedAt: string;
24
- }
25
- export interface FeatureRequestSubmitData {
26
- title: string;
27
- description: string;
28
- type: string;
29
- rating?: number;
30
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-settings",
3
- "version": "5.3.42",
3
+ "version": "5.3.43",
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",
@@ -21,15 +21,3 @@ export interface FeatureRequestItem {
21
21
  createdAt: string;
22
22
  updatedAt: string;
23
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
- }
@@ -4,7 +4,7 @@
4
4
  * Works with both authenticated and anonymous users
5
5
  */
6
6
 
7
- import { useState, useEffect, useCallback } from "react";
7
+ import { useState, useEffect, useCallback, useRef } from "react";
8
8
  import { Platform } from "react-native";
9
9
  import { useAuth } from "@umituz/react-native-auth";
10
10
  import {
@@ -48,6 +48,13 @@ export function useFeatureRequests(): UseFeatureRequestsResult {
48
48
  const [userVotes, setUserVotes] = useState<Record<string, VoteType>>({});
49
49
  const [isLoading, setIsLoading] = useState(true);
50
50
 
51
+ // Ref to avoid stale closure in vote()
52
+ const userVotesRef = useRef(userVotes);
53
+ userVotesRef.current = userVotes;
54
+
55
+ // Guard against rapid double-tap on the same request
56
+ const votingInProgress = useRef(new Set<string>());
57
+
51
58
  const fetchAll = useCallback(async () => {
52
59
  const db = getFirestore();
53
60
  if (!db) {
@@ -58,7 +65,6 @@ export function useFeatureRequests(): UseFeatureRequestsResult {
58
65
  try {
59
66
  setIsLoading(true);
60
67
 
61
- // Fetch requests
62
68
  const q = query(collection(db, COLLECTION), orderBy("votes", "desc"));
63
69
  const snapshot = await getDocs(q);
64
70
 
@@ -82,17 +88,25 @@ export function useFeatureRequests(): UseFeatureRequestsResult {
82
88
 
83
89
  setRequests(items);
84
90
 
85
- // Fetch user votes
86
- if (userId) {
87
- const votes: Record<string, VoteType> = {};
88
- for (const reqDoc of snapshot.docs) {
91
+ // Fetch user votes in parallel (not N+1 sequential)
92
+ if (userId && snapshot.docs.length > 0) {
93
+ const votePromises = snapshot.docs.map(async (reqDoc) => {
89
94
  const voteRef = doc(db, COLLECTION, reqDoc.id, "votes", userId);
90
95
  const voteSnap = await getDoc(voteRef);
91
96
  if (voteSnap.exists()) {
92
- votes[reqDoc.id] = voteSnap.data().type as VoteType;
97
+ return [reqDoc.id, voteSnap.data().type as VoteType] as const;
93
98
  }
99
+ return null;
100
+ });
101
+
102
+ const results = await Promise.all(votePromises);
103
+ const votes: Record<string, VoteType> = {};
104
+ for (const result of results) {
105
+ if (result) votes[result[0]] = result[1];
94
106
  }
95
107
  setUserVotes(votes);
108
+ } else {
109
+ setUserVotes({});
96
110
  }
97
111
  } catch (error) {
98
112
  if (__DEV__) console.warn("[useFeatureRequests] Load failed:", error);
@@ -108,10 +122,18 @@ export function useFeatureRequests(): UseFeatureRequestsResult {
108
122
  const vote = useCallback(async (requestId: string, type: VoteType) => {
109
123
  if (!userId) return;
110
124
 
125
+ // Prevent rapid double-tap on the same request
126
+ if (votingInProgress.current.has(requestId)) return;
127
+ votingInProgress.current.add(requestId);
128
+
111
129
  const db = getFirestore();
112
- if (!db) return;
130
+ if (!db) {
131
+ votingInProgress.current.delete(requestId);
132
+ return;
133
+ }
113
134
 
114
- const previousVote = userVotes[requestId];
135
+ // Read latest vote from ref (not stale closure)
136
+ const previousVote = userVotesRef.current[requestId] as VoteType | undefined;
115
137
  const isUndo = previousVote === type;
116
138
 
117
139
  // Optimistic UI
@@ -150,23 +172,26 @@ export function useFeatureRequests(): UseFeatureRequestsResult {
150
172
  await updateDoc(requestRef, { votes: increment(type === "up" ? 1 : -1) });
151
173
  }
152
174
  } catch (error) {
153
- // Rollback
154
175
  if (__DEV__) console.warn("[useFeatureRequests] Vote failed:", error);
176
+ // Rollback using previous known state
155
177
  setUserVotes((prev) => {
156
178
  const next = { ...prev };
157
179
  if (previousVote) next[requestId] = previousVote; else delete next[requestId];
158
180
  return next;
159
181
  });
160
182
  fetchAll();
183
+ } finally {
184
+ votingInProgress.current.delete(requestId);
161
185
  }
162
- }, [userId, userVotes, fetchAll]);
186
+ }, [userId, fetchAll]);
163
187
 
164
188
  const submitRequest = useCallback(async (data: { title: string; description: string; type: string; rating?: number }) => {
165
189
  const db = getFirestore();
166
190
  if (!db) throw new Error("Firestore not available");
167
191
  if (!userId) throw new Error("User not authenticated");
168
192
 
169
- await addDoc(collection(db, COLLECTION), {
193
+ // Create the feature request
194
+ const docRef = await addDoc(collection(db, COLLECTION), {
170
195
  title: data.title,
171
196
  description: data.description,
172
197
  type: data.type || "feature_request",
@@ -181,6 +206,12 @@ export function useFeatureRequests(): UseFeatureRequestsResult {
181
206
  updatedAt: serverTimestamp(),
182
207
  });
183
208
 
209
+ // Create the creator's upvote doc so votes count matches reality
210
+ await setDoc(doc(db, COLLECTION, docRef.id, "votes", userId), {
211
+ type: "up" as VoteType,
212
+ votedAt: serverTimestamp(),
213
+ });
214
+
184
215
  await fetchAll();
185
216
  }, [userId, user?.isAnonymous, fetchAll]);
186
217
 
@@ -4,13 +4,11 @@ import {
4
4
  StyleSheet,
5
5
  TouchableOpacity,
6
6
  ScrollView,
7
- FlatList,
8
7
  ActivityIndicator,
9
8
  } from "react-native";
10
9
  import {
11
10
  AtomicText,
12
11
  AtomicIcon,
13
- AtomicButton,
14
12
  } from "@umituz/react-native-design-system/atoms";
15
13
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
16
14
  import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
@@ -47,10 +45,6 @@ export const FeatureRequestScreen: React.FC<any> = ({ config, texts }) => {
47
45
  dismissed: t.status?.dismissed || "Dismissed",
48
46
  };
49
47
 
50
- const handleVote = useCallback(async (id: string, type: VoteType) => {
51
- await vote(id, type);
52
- }, [vote]);
53
-
54
48
  const handleSubmit = useCallback(async (data: any) => {
55
49
  setIsSubmitting(true);
56
50
  try {
@@ -90,19 +84,19 @@ export const FeatureRequestScreen: React.FC<any> = ({ config, texts }) => {
90
84
  }
91
85
  })();
92
86
 
93
- const renderRequestItem = ({ item }: { item: FeatureRequestItem }) => {
87
+ const renderRequestCard = (item: FeatureRequestItem) => {
94
88
  const voted = userVotes[item.id] || null;
95
89
 
96
90
  return (
97
- <View style={[styles.card, { backgroundColor: tokens.colors.surfaceSecondary, borderColor: tokens.colors.borderLight }]}>
91
+ <View key={item.id} style={[styles.card, { backgroundColor: tokens.colors.surfaceSecondary, borderColor: tokens.colors.borderLight }]}>
98
92
  <View style={styles.voteColumn}>
99
- <TouchableOpacity onPress={() => handleVote(item.id, 'up')}>
93
+ <TouchableOpacity onPress={() => vote(item.id, 'up')}>
100
94
  <AtomicIcon name="chevron-up" size="md" color={voted === 'up' ? "primary" : "textSecondary" as any} />
101
95
  </TouchableOpacity>
102
96
  <AtomicText style={[styles.voteCount, { color: voted === 'up' ? tokens.colors.primary : tokens.colors.textPrimary }]}>
103
97
  {item.votes}
104
98
  </AtomicText>
105
- <TouchableOpacity onPress={() => handleVote(item.id, 'down')}>
99
+ <TouchableOpacity onPress={() => vote(item.id, 'down')}>
106
100
  <AtomicIcon name="chevron-down" size="md" color={voted === 'down' ? "primary" : "textSecondary" as any} />
107
101
  </TouchableOpacity>
108
102
  </View>
@@ -196,13 +190,9 @@ export const FeatureRequestScreen: React.FC<any> = ({ config, texts }) => {
196
190
  </AtomicText>
197
191
  </View>
198
192
  ) : (
199
- <FlatList
200
- data={filteredRequests}
201
- renderItem={renderRequestItem}
202
- keyExtractor={item => item.id}
203
- scrollEnabled={false}
204
- contentContainerStyle={styles.listContent}
205
- />
193
+ <View style={styles.listContent}>
194
+ {filteredRequests.map(renderRequestCard)}
195
+ </View>
206
196
  )}
207
197
  </ScrollView>
208
198