@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.
- package/dist/domains/feedback/domain/entities/FeatureRequestEntity.d.ts +0 -10
- package/package.json +1 -1
- package/src/domains/feedback/domain/entities/FeatureRequestEntity.ts +0 -12
- package/src/domains/feedback/infrastructure/useFeatureRequests.ts +43 -12
- package/src/domains/feedback/presentation/screens/FeatureRequestScreen.tsx +7 -17
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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)
|
|
130
|
+
if (!db) {
|
|
131
|
+
votingInProgress.current.delete(requestId);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
113
134
|
|
|
114
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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={() =>
|
|
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={() =>
|
|
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
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|