@umituz/react-native-settings 5.3.41 → 5.3.42
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 -12
- package/dist/domains/feedback/infrastructure/useFeatureRequests.d.ts +21 -0
- package/dist/domains/feedback/presentation/screens/FeatureRequestScreen.d.ts +1 -11
- package/dist/presentation/hooks/useSettingsScreenConfig.d.ts +0 -2
- package/dist/presentation/screens/types/UserFeatureConfig.d.ts +0 -3
- package/package.json +1 -1
- package/src/domains/feedback/domain/entities/FeatureRequestEntity.ts +0 -13
- package/src/domains/feedback/infrastructure/useFeatureRequests.ts +188 -0
- package/src/domains/feedback/presentation/screens/FeatureRequestScreen.tsx +48 -293
- package/src/presentation/hooks/useSettingsScreenConfig.ts +2 -13
- package/src/presentation/navigation/hooks/useSettingsScreens.ts +0 -3
- package/src/presentation/screens/types/UserFeatureConfig.ts +0 -3
|
@@ -28,15 +28,3 @@ export interface FeatureRequestSubmitData {
|
|
|
28
28
|
type: string;
|
|
29
29
|
rating?: number;
|
|
30
30
|
}
|
|
31
|
-
export interface FeatureRequestDataProvider {
|
|
32
|
-
/** Fetch all feature requests */
|
|
33
|
-
fetchRequests: () => Promise<FeatureRequestItem[]>;
|
|
34
|
-
/** Fetch current user's votes */
|
|
35
|
-
fetchUserVotes: (userId: string) => Promise<Record<string, VoteType>>;
|
|
36
|
-
/** Vote on a feature request */
|
|
37
|
-
onVote: (requestId: string, userId: string, type: VoteType) => Promise<void>;
|
|
38
|
-
/** Submit a new feature request */
|
|
39
|
-
onSubmit: (data: FeatureRequestSubmitData) => Promise<void>;
|
|
40
|
-
/** Current user ID (can be anonymous UID) */
|
|
41
|
-
userId: string | null;
|
|
42
|
-
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFeatureRequests Hook
|
|
3
|
+
* Internal hook — fetches, votes, submits feature requests via Firestore
|
|
4
|
+
* Works with both authenticated and anonymous users
|
|
5
|
+
*/
|
|
6
|
+
import type { FeatureRequestItem, VoteType } from "../domain/entities/FeatureRequestEntity";
|
|
7
|
+
export interface UseFeatureRequestsResult {
|
|
8
|
+
requests: FeatureRequestItem[];
|
|
9
|
+
userVotes: Record<string, VoteType>;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
vote: (requestId: string, type: VoteType) => Promise<void>;
|
|
12
|
+
submitRequest: (data: {
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
type: string;
|
|
16
|
+
rating?: number;
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
reload: () => Promise<void>;
|
|
19
|
+
userId: string | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function useFeatureRequests(): UseFeatureRequestsResult;
|
|
@@ -1,12 +1,2 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
|
|
3
|
-
interface FeatureRequestScreenProps {
|
|
4
|
-
config?: {
|
|
5
|
-
translations?: Record<string, any>;
|
|
6
|
-
dataProvider?: FeatureRequestDataProvider;
|
|
7
|
-
onSubmit?: (data: any) => Promise<void>;
|
|
8
|
-
};
|
|
9
|
-
texts: any;
|
|
10
|
-
}
|
|
11
|
-
export declare const FeatureRequestScreen: React.FC<FeatureRequestScreenProps>;
|
|
12
|
-
export {};
|
|
2
|
+
export declare const FeatureRequestScreen: React.FC<any>;
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { SettingsConfig, SettingsTranslations } from "../screens/types";
|
|
9
9
|
import type { FeedbackFormData } from "../utils/config-creators";
|
|
10
|
-
import type { FeatureRequestDataProvider } from "../../domains/feedback/domain/entities/FeatureRequestEntity";
|
|
11
10
|
import type { AppInfo, FAQData, UserProfileDisplay, AdditionalScreen, AccountConfig } from "../navigation/types";
|
|
12
11
|
export interface SettingsFeatures {
|
|
13
12
|
notifications?: boolean;
|
|
@@ -27,7 +26,6 @@ export interface UseSettingsScreenConfigParams {
|
|
|
27
26
|
faqData?: FAQData;
|
|
28
27
|
isPremium: boolean;
|
|
29
28
|
onFeedbackSubmit: (data: FeedbackFormData) => Promise<void>;
|
|
30
|
-
featureRequestDataProvider?: FeatureRequestDataProvider;
|
|
31
29
|
additionalScreens?: AdditionalScreen[];
|
|
32
30
|
features?: SettingsFeatures;
|
|
33
31
|
translations?: SettingsTranslations;
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { FeatureVisibility } from "./BaseTypes";
|
|
6
6
|
import type { FeedbackType } from "../../../domains/feedback/domain/entities/FeedbackEntity";
|
|
7
|
-
import type { FeatureRequestDataProvider } from "../../../domains/feedback/domain/entities/FeatureRequestEntity";
|
|
8
7
|
import type { FAQCategory } from "../../../domains/faqs/domain/entities/FAQEntity";
|
|
9
8
|
import type { SettingsStackParamList } from "../../navigation/types";
|
|
10
9
|
/**
|
|
@@ -47,8 +46,6 @@ export interface FeedbackConfig {
|
|
|
47
46
|
}) => Promise<void>;
|
|
48
47
|
/** Custom handler to open feedback screen (overrides default modal) */
|
|
49
48
|
onPress?: () => void;
|
|
50
|
-
/** Data provider for feature request screen (fetching, voting, submitting) */
|
|
51
|
-
dataProvider?: FeatureRequestDataProvider;
|
|
52
49
|
}
|
|
53
50
|
/**
|
|
54
51
|
* FAQ Settings Configuration
|
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.42",
|
|
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",
|
|
@@ -33,16 +33,3 @@ export interface FeatureRequestSubmitData {
|
|
|
33
33
|
type: string;
|
|
34
34
|
rating?: number;
|
|
35
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
|
-
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFeatureRequests Hook
|
|
3
|
+
* Internal hook — fetches, votes, submits feature requests via Firestore
|
|
4
|
+
* Works with both authenticated and anonymous users
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useEffect, useCallback } from "react";
|
|
8
|
+
import { Platform } from "react-native";
|
|
9
|
+
import { useAuth } from "@umituz/react-native-auth";
|
|
10
|
+
import {
|
|
11
|
+
collection,
|
|
12
|
+
doc,
|
|
13
|
+
getDocs,
|
|
14
|
+
getDoc,
|
|
15
|
+
setDoc,
|
|
16
|
+
addDoc,
|
|
17
|
+
updateDoc,
|
|
18
|
+
deleteDoc,
|
|
19
|
+
increment,
|
|
20
|
+
orderBy,
|
|
21
|
+
query,
|
|
22
|
+
serverTimestamp,
|
|
23
|
+
} from "firebase/firestore";
|
|
24
|
+
import { getFirestore } from "@umituz/react-native-firebase";
|
|
25
|
+
import type {
|
|
26
|
+
FeatureRequestItem,
|
|
27
|
+
FeatureRequestStatus,
|
|
28
|
+
VoteType,
|
|
29
|
+
} from "../domain/entities/FeatureRequestEntity";
|
|
30
|
+
|
|
31
|
+
const COLLECTION = "feature_requests";
|
|
32
|
+
|
|
33
|
+
export interface UseFeatureRequestsResult {
|
|
34
|
+
requests: FeatureRequestItem[];
|
|
35
|
+
userVotes: Record<string, VoteType>;
|
|
36
|
+
isLoading: boolean;
|
|
37
|
+
vote: (requestId: string, type: VoteType) => Promise<void>;
|
|
38
|
+
submitRequest: (data: { title: string; description: string; type: string; rating?: number }) => Promise<void>;
|
|
39
|
+
reload: () => Promise<void>;
|
|
40
|
+
userId: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useFeatureRequests(): UseFeatureRequestsResult {
|
|
44
|
+
const { user } = useAuth();
|
|
45
|
+
const userId = user?.uid ?? null;
|
|
46
|
+
|
|
47
|
+
const [requests, setRequests] = useState<FeatureRequestItem[]>([]);
|
|
48
|
+
const [userVotes, setUserVotes] = useState<Record<string, VoteType>>({});
|
|
49
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
50
|
+
|
|
51
|
+
const fetchAll = useCallback(async () => {
|
|
52
|
+
const db = getFirestore();
|
|
53
|
+
if (!db) {
|
|
54
|
+
setIsLoading(false);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
setIsLoading(true);
|
|
60
|
+
|
|
61
|
+
// Fetch requests
|
|
62
|
+
const q = query(collection(db, COLLECTION), orderBy("votes", "desc"));
|
|
63
|
+
const snapshot = await getDocs(q);
|
|
64
|
+
|
|
65
|
+
const items: FeatureRequestItem[] = snapshot.docs.map((d) => {
|
|
66
|
+
const data = d.data();
|
|
67
|
+
return {
|
|
68
|
+
id: d.id,
|
|
69
|
+
title: data.title ?? "",
|
|
70
|
+
description: data.description ?? "",
|
|
71
|
+
type: data.type ?? "feature_request",
|
|
72
|
+
status: (data.status ?? "pending") as FeatureRequestStatus,
|
|
73
|
+
votes: data.votes ?? 0,
|
|
74
|
+
commentCount: data.commentCount ?? 0,
|
|
75
|
+
createdBy: data.createdBy ?? "",
|
|
76
|
+
isAnonymous: data.isAnonymous ?? false,
|
|
77
|
+
platform: data.platform ?? "unknown",
|
|
78
|
+
createdAt: data.createdAt?.toDate?.()?.toISOString?.() ?? new Date().toISOString(),
|
|
79
|
+
updatedAt: data.updatedAt?.toDate?.()?.toISOString?.() ?? new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
setRequests(items);
|
|
84
|
+
|
|
85
|
+
// Fetch user votes
|
|
86
|
+
if (userId) {
|
|
87
|
+
const votes: Record<string, VoteType> = {};
|
|
88
|
+
for (const reqDoc of snapshot.docs) {
|
|
89
|
+
const voteRef = doc(db, COLLECTION, reqDoc.id, "votes", userId);
|
|
90
|
+
const voteSnap = await getDoc(voteRef);
|
|
91
|
+
if (voteSnap.exists()) {
|
|
92
|
+
votes[reqDoc.id] = voteSnap.data().type as VoteType;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
setUserVotes(votes);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (__DEV__) console.warn("[useFeatureRequests] Load failed:", error);
|
|
99
|
+
} finally {
|
|
100
|
+
setIsLoading(false);
|
|
101
|
+
}
|
|
102
|
+
}, [userId]);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
fetchAll();
|
|
106
|
+
}, [fetchAll]);
|
|
107
|
+
|
|
108
|
+
const vote = useCallback(async (requestId: string, type: VoteType) => {
|
|
109
|
+
if (!userId) return;
|
|
110
|
+
|
|
111
|
+
const db = getFirestore();
|
|
112
|
+
if (!db) return;
|
|
113
|
+
|
|
114
|
+
const previousVote = userVotes[requestId];
|
|
115
|
+
const isUndo = previousVote === type;
|
|
116
|
+
|
|
117
|
+
// Optimistic UI
|
|
118
|
+
setUserVotes((prev) => {
|
|
119
|
+
const next = { ...prev };
|
|
120
|
+
if (isUndo) { delete next[requestId]; } else { next[requestId] = type; }
|
|
121
|
+
return next;
|
|
122
|
+
});
|
|
123
|
+
setRequests((prev) =>
|
|
124
|
+
prev.map((r) => {
|
|
125
|
+
if (r.id !== requestId) return r;
|
|
126
|
+
let delta = 0;
|
|
127
|
+
if (isUndo) delta = type === "up" ? -1 : 1;
|
|
128
|
+
else if (previousVote) delta = type === "up" ? 2 : -2;
|
|
129
|
+
else delta = type === "up" ? 1 : -1;
|
|
130
|
+
return { ...r, votes: r.votes + delta };
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const voteRef = doc(db, COLLECTION, requestId, "votes", userId);
|
|
136
|
+
const requestRef = doc(db, COLLECTION, requestId);
|
|
137
|
+
const existing = await getDoc(voteRef);
|
|
138
|
+
|
|
139
|
+
if (existing.exists()) {
|
|
140
|
+
const prev = existing.data().type as VoteType;
|
|
141
|
+
if (prev === type) {
|
|
142
|
+
await deleteDoc(voteRef);
|
|
143
|
+
await updateDoc(requestRef, { votes: increment(type === "up" ? -1 : 1) });
|
|
144
|
+
} else {
|
|
145
|
+
await setDoc(voteRef, { type, votedAt: serverTimestamp() });
|
|
146
|
+
await updateDoc(requestRef, { votes: increment(type === "up" ? 2 : -2) });
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
await setDoc(voteRef, { type, votedAt: serverTimestamp() });
|
|
150
|
+
await updateDoc(requestRef, { votes: increment(type === "up" ? 1 : -1) });
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// Rollback
|
|
154
|
+
if (__DEV__) console.warn("[useFeatureRequests] Vote failed:", error);
|
|
155
|
+
setUserVotes((prev) => {
|
|
156
|
+
const next = { ...prev };
|
|
157
|
+
if (previousVote) next[requestId] = previousVote; else delete next[requestId];
|
|
158
|
+
return next;
|
|
159
|
+
});
|
|
160
|
+
fetchAll();
|
|
161
|
+
}
|
|
162
|
+
}, [userId, userVotes, fetchAll]);
|
|
163
|
+
|
|
164
|
+
const submitRequest = useCallback(async (data: { title: string; description: string; type: string; rating?: number }) => {
|
|
165
|
+
const db = getFirestore();
|
|
166
|
+
if (!db) throw new Error("Firestore not available");
|
|
167
|
+
if (!userId) throw new Error("User not authenticated");
|
|
168
|
+
|
|
169
|
+
await addDoc(collection(db, COLLECTION), {
|
|
170
|
+
title: data.title,
|
|
171
|
+
description: data.description,
|
|
172
|
+
type: data.type || "feature_request",
|
|
173
|
+
status: "pending",
|
|
174
|
+
votes: 1,
|
|
175
|
+
commentCount: 0,
|
|
176
|
+
createdBy: userId,
|
|
177
|
+
isAnonymous: user?.isAnonymous ?? false,
|
|
178
|
+
platform: Platform.OS,
|
|
179
|
+
rating: data.rating ?? null,
|
|
180
|
+
createdAt: serverTimestamp(),
|
|
181
|
+
updatedAt: serverTimestamp(),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await fetchAll();
|
|
185
|
+
}, [userId, user?.isAnonymous, fetchAll]);
|
|
186
|
+
|
|
187
|
+
return { requests, userVotes, isLoading, vote, submitRequest, reload: fetchAll, userId };
|
|
188
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState,
|
|
1
|
+
import React, { useState, useCallback } from "react";
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
StyleSheet,
|
|
@@ -15,29 +15,13 @@ 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
|
|
19
|
-
import type {
|
|
20
|
-
FeatureRequestItem,
|
|
21
|
-
FeatureRequestDataProvider,
|
|
22
|
-
VoteType,
|
|
23
|
-
} from "../../domain/entities/FeatureRequestEntity";
|
|
18
|
+
import { useFeatureRequests } from "../../infrastructure/useFeatureRequests";
|
|
19
|
+
import type { FeatureRequestItem, VoteType } from "../../domain/entities/FeatureRequestEntity";
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
config?: {
|
|
27
|
-
translations?: Record<string, any>;
|
|
28
|
-
dataProvider?: FeatureRequestDataProvider;
|
|
29
|
-
onSubmit?: (data: any) => Promise<void>;
|
|
30
|
-
};
|
|
31
|
-
texts: any;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ config, texts }) => {
|
|
21
|
+
export const FeatureRequestScreen: React.FC<any> = ({ config, texts }) => {
|
|
35
22
|
const tokens = useAppDesignTokens();
|
|
36
|
-
const
|
|
23
|
+
const { requests, userVotes, isLoading, vote, submitRequest, userId } = useFeatureRequests();
|
|
37
24
|
|
|
38
|
-
const [requests, setRequests] = useState<FeatureRequestItem[]>([]);
|
|
39
|
-
const [userVotes, setUserVotes] = useState<Record<string, VoteType>>({});
|
|
40
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
41
25
|
const [activeTab, setActiveTab] = useState<'all' | 'my' | 'roadmap'>('all');
|
|
42
26
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
43
27
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
@@ -63,101 +47,26 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
63
47
|
dismissed: t.status?.dismissed || "Dismissed",
|
|
64
48
|
};
|
|
65
49
|
|
|
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
50
|
const handleVote = useCallback(async (id: string, type: VoteType) => {
|
|
94
|
-
|
|
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
|
-
|
|
110
|
-
setRequests(prev => prev.map(req => {
|
|
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;
|
|
119
|
-
}
|
|
120
|
-
return { ...req, votes: req.votes + delta };
|
|
121
|
-
}));
|
|
122
|
-
|
|
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]);
|
|
51
|
+
await vote(id, type);
|
|
52
|
+
}, [vote]);
|
|
140
53
|
|
|
141
54
|
const handleSubmit = useCallback(async (data: any) => {
|
|
142
|
-
if (!dataProvider?.onSubmit) return;
|
|
143
|
-
|
|
144
55
|
setIsSubmitting(true);
|
|
145
56
|
try {
|
|
146
|
-
await
|
|
57
|
+
await submitRequest({
|
|
147
58
|
title: data.title || "New Request",
|
|
148
59
|
description: data.description,
|
|
149
60
|
type: data.type || "feature_request",
|
|
150
61
|
rating: data.rating,
|
|
151
62
|
});
|
|
152
63
|
setIsModalVisible(false);
|
|
153
|
-
// Reload data to get the new request from the database
|
|
154
|
-
await loadData();
|
|
155
64
|
} catch (error) {
|
|
156
65
|
if (__DEV__) console.warn("[FeatureRequestScreen] Submit failed:", error);
|
|
157
66
|
} finally {
|
|
158
67
|
setIsSubmitting(false);
|
|
159
68
|
}
|
|
160
|
-
}, [
|
|
69
|
+
}, [submitRequest]);
|
|
161
70
|
|
|
162
71
|
const getStatusColor = (status: string) => {
|
|
163
72
|
switch (status) {
|
|
@@ -173,9 +82,9 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
173
82
|
const filteredRequests = (() => {
|
|
174
83
|
switch (activeTab) {
|
|
175
84
|
case 'my':
|
|
176
|
-
return requests.filter(r => r.createdBy ===
|
|
85
|
+
return requests.filter(r => r.createdBy === userId);
|
|
177
86
|
case 'roadmap':
|
|
178
|
-
return requests.filter(r =>
|
|
87
|
+
return requests.filter(r => ['planned', 'completed', 'review'].includes(r.status));
|
|
179
88
|
default:
|
|
180
89
|
return requests;
|
|
181
90
|
}
|
|
@@ -188,21 +97,13 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
188
97
|
<View style={[styles.card, { backgroundColor: tokens.colors.surfaceSecondary, borderColor: tokens.colors.borderLight }]}>
|
|
189
98
|
<View style={styles.voteColumn}>
|
|
190
99
|
<TouchableOpacity onPress={() => handleVote(item.id, 'up')}>
|
|
191
|
-
<AtomicIcon
|
|
192
|
-
name="chevron-up"
|
|
193
|
-
size="md"
|
|
194
|
-
color={voted === 'up' ? "primary" : "textSecondary" as any}
|
|
195
|
-
/>
|
|
100
|
+
<AtomicIcon name="chevron-up" size="md" color={voted === 'up' ? "primary" : "textSecondary" as any} />
|
|
196
101
|
</TouchableOpacity>
|
|
197
102
|
<AtomicText style={[styles.voteCount, { color: voted === 'up' ? tokens.colors.primary : tokens.colors.textPrimary }]}>
|
|
198
103
|
{item.votes}
|
|
199
104
|
</AtomicText>
|
|
200
105
|
<TouchableOpacity onPress={() => handleVote(item.id, 'down')}>
|
|
201
|
-
<AtomicIcon
|
|
202
|
-
name="chevron-down"
|
|
203
|
-
size="md"
|
|
204
|
-
color={voted === 'down' ? "primary" : "textSecondary" as any}
|
|
205
|
-
/>
|
|
106
|
+
<AtomicIcon name="chevron-down" size="md" color={voted === 'down' ? "primary" : "textSecondary" as any} />
|
|
206
107
|
</TouchableOpacity>
|
|
207
108
|
</View>
|
|
208
109
|
|
|
@@ -221,11 +122,9 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
221
122
|
</AtomicText>
|
|
222
123
|
|
|
223
124
|
<View style={styles.cardFooter}>
|
|
224
|
-
<
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
</AtomicText>
|
|
228
|
-
</View>
|
|
125
|
+
<AtomicText style={[styles.platformText, { color: tokens.colors.textTertiary }]}>
|
|
126
|
+
{item.platform.toUpperCase()}
|
|
127
|
+
</AtomicText>
|
|
229
128
|
<AtomicText style={[styles.commentCount, { color: tokens.colors.textTertiary }]}>
|
|
230
129
|
{item.commentCount} {t.comment_count?.replace('{{count}}', '') || 'comments'}
|
|
231
130
|
</AtomicText>
|
|
@@ -332,180 +231,36 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
332
231
|
};
|
|
333
232
|
|
|
334
233
|
const styles = StyleSheet.create({
|
|
335
|
-
container: {
|
|
336
|
-
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
},
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
},
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
},
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
},
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
},
|
|
368
|
-
tabLabel: {
|
|
369
|
-
fontSize: 14,
|
|
370
|
-
fontWeight: '600',
|
|
371
|
-
color: 'rgba(255,255,255,0.5)',
|
|
372
|
-
},
|
|
373
|
-
banner: {
|
|
374
|
-
flexDirection: 'row',
|
|
375
|
-
alignItems: 'center',
|
|
376
|
-
padding: 16,
|
|
377
|
-
borderRadius: 16,
|
|
378
|
-
borderWidth: 1,
|
|
379
|
-
gap: 12,
|
|
380
|
-
marginBottom: 20,
|
|
381
|
-
},
|
|
382
|
-
bannerIconContainer: {
|
|
383
|
-
position: 'relative',
|
|
384
|
-
},
|
|
385
|
-
pulseDot: {
|
|
386
|
-
position: 'absolute',
|
|
387
|
-
top: 0,
|
|
388
|
-
right: 0,
|
|
389
|
-
width: 8,
|
|
390
|
-
height: 8,
|
|
391
|
-
borderRadius: 4,
|
|
392
|
-
backgroundColor: '#10b981',
|
|
393
|
-
},
|
|
394
|
-
bannerTitle: {
|
|
395
|
-
fontSize: 14,
|
|
396
|
-
fontWeight: '700',
|
|
397
|
-
},
|
|
398
|
-
bannerSub: {
|
|
399
|
-
fontSize: 12,
|
|
400
|
-
},
|
|
401
|
-
sectionTitle: {
|
|
402
|
-
fontSize: 18,
|
|
403
|
-
fontWeight: '700',
|
|
404
|
-
marginBottom: 16,
|
|
405
|
-
},
|
|
406
|
-
listContent: {
|
|
407
|
-
gap: 12,
|
|
408
|
-
paddingBottom: 40,
|
|
409
|
-
},
|
|
410
|
-
card: {
|
|
411
|
-
flexDirection: 'row',
|
|
412
|
-
padding: 16,
|
|
413
|
-
borderRadius: 16,
|
|
414
|
-
borderWidth: 1,
|
|
415
|
-
gap: 12,
|
|
416
|
-
},
|
|
417
|
-
voteColumn: {
|
|
418
|
-
alignItems: 'center',
|
|
419
|
-
gap: 4,
|
|
420
|
-
width: 40,
|
|
421
|
-
},
|
|
422
|
-
voteCount: {
|
|
423
|
-
fontSize: 13,
|
|
424
|
-
fontWeight: '800',
|
|
425
|
-
},
|
|
426
|
-
cardContent: {
|
|
427
|
-
flex: 1,
|
|
428
|
-
gap: 8,
|
|
429
|
-
},
|
|
430
|
-
cardHeader: {
|
|
431
|
-
flexDirection: 'row',
|
|
432
|
-
justifyContent: 'space-between',
|
|
433
|
-
alignItems: 'flex-start',
|
|
434
|
-
gap: 8,
|
|
435
|
-
},
|
|
436
|
-
cardTitle: {
|
|
437
|
-
fontSize: 15,
|
|
438
|
-
fontWeight: '700',
|
|
439
|
-
flex: 1,
|
|
440
|
-
},
|
|
441
|
-
statusBadge: {
|
|
442
|
-
paddingHorizontal: 8,
|
|
443
|
-
paddingVertical: 2,
|
|
444
|
-
borderRadius: 8,
|
|
445
|
-
borderWidth: 1,
|
|
446
|
-
},
|
|
447
|
-
statusText: {
|
|
448
|
-
fontSize: 9,
|
|
449
|
-
fontWeight: '900',
|
|
450
|
-
},
|
|
451
|
-
cardDescription: {
|
|
452
|
-
fontSize: 13,
|
|
453
|
-
lineHeight: 18,
|
|
454
|
-
},
|
|
455
|
-
cardFooter: {
|
|
456
|
-
flexDirection: 'row',
|
|
457
|
-
alignItems: 'center',
|
|
458
|
-
justifyContent: 'space-between',
|
|
459
|
-
marginTop: 4,
|
|
460
|
-
},
|
|
461
|
-
platformBadge: {
|
|
462
|
-
paddingHorizontal: 6,
|
|
463
|
-
paddingVertical: 2,
|
|
464
|
-
},
|
|
465
|
-
platformText: {
|
|
466
|
-
fontSize: 10,
|
|
467
|
-
fontWeight: '600',
|
|
468
|
-
textTransform: 'uppercase',
|
|
469
|
-
},
|
|
470
|
-
commentCount: {
|
|
471
|
-
fontSize: 11,
|
|
472
|
-
},
|
|
473
|
-
floatingHint: {
|
|
474
|
-
position: 'absolute',
|
|
475
|
-
bottom: 40,
|
|
476
|
-
right: 16,
|
|
477
|
-
zIndex: 100,
|
|
478
|
-
},
|
|
479
|
-
hintBadge: {
|
|
480
|
-
paddingHorizontal: 12,
|
|
481
|
-
paddingVertical: 6,
|
|
482
|
-
borderRadius: 20,
|
|
483
|
-
shadowColor: '#000',
|
|
484
|
-
shadowOffset: { width: 0, height: 4 },
|
|
485
|
-
shadowOpacity: 0.3,
|
|
486
|
-
shadowRadius: 4,
|
|
487
|
-
elevation: 8,
|
|
488
|
-
},
|
|
489
|
-
hintText: {
|
|
490
|
-
color: '#fff',
|
|
491
|
-
fontSize: 10,
|
|
492
|
-
fontWeight: '900',
|
|
493
|
-
textTransform: 'uppercase',
|
|
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
|
-
},
|
|
234
|
+
container: { padding: 16 },
|
|
235
|
+
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 12 },
|
|
236
|
+
headerTitle: { fontSize: 20, fontWeight: '800' },
|
|
237
|
+
addButton: { width: 36, height: 36, borderRadius: 12, alignItems: 'center', justifyContent: 'center' },
|
|
238
|
+
tabsContainer: { flexDirection: 'row', paddingHorizontal: 16, borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.05)' },
|
|
239
|
+
tab: { paddingVertical: 12, paddingHorizontal: 16, borderBottomWidth: 2, borderBottomColor: 'transparent' },
|
|
240
|
+
tabLabel: { fontSize: 14, fontWeight: '600', color: 'rgba(255,255,255,0.5)' },
|
|
241
|
+
banner: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, borderWidth: 1, gap: 12, marginBottom: 20 },
|
|
242
|
+
bannerIconContainer: { position: 'relative' },
|
|
243
|
+
pulseDot: { position: 'absolute', top: 0, right: 0, width: 8, height: 8, borderRadius: 4, backgroundColor: '#10b981' },
|
|
244
|
+
bannerTitle: { fontSize: 14, fontWeight: '700' },
|
|
245
|
+
bannerSub: { fontSize: 12 },
|
|
246
|
+
sectionTitle: { fontSize: 18, fontWeight: '700', marginBottom: 16 },
|
|
247
|
+
listContent: { gap: 12, paddingBottom: 40 },
|
|
248
|
+
card: { flexDirection: 'row', padding: 16, borderRadius: 16, borderWidth: 1, gap: 12 },
|
|
249
|
+
voteColumn: { alignItems: 'center', gap: 4, width: 40 },
|
|
250
|
+
voteCount: { fontSize: 13, fontWeight: '800' },
|
|
251
|
+
cardContent: { flex: 1, gap: 8 },
|
|
252
|
+
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', gap: 8 },
|
|
253
|
+
cardTitle: { fontSize: 15, fontWeight: '700', flex: 1 },
|
|
254
|
+
statusBadge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 8, borderWidth: 1 },
|
|
255
|
+
statusText: { fontSize: 9, fontWeight: '900' },
|
|
256
|
+
cardDescription: { fontSize: 13, lineHeight: 18 },
|
|
257
|
+
cardFooter: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 4 },
|
|
258
|
+
platformText: { fontSize: 10, fontWeight: '600' },
|
|
259
|
+
commentCount: { fontSize: 11 },
|
|
260
|
+
floatingHint: { position: 'absolute', bottom: 40, right: 16, zIndex: 100 },
|
|
261
|
+
hintBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 4, elevation: 8 },
|
|
262
|
+
hintText: { color: '#fff', fontSize: 10, fontWeight: '900', textTransform: 'uppercase' },
|
|
263
|
+
loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingTop: 100 },
|
|
264
|
+
emptyState: { alignItems: 'center', justifyContent: 'center', paddingVertical: 60, gap: 12 },
|
|
265
|
+
emptyText: { fontSize: 14, fontWeight: '500' },
|
|
511
266
|
});
|
|
@@ -14,7 +14,6 @@ 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";
|
|
18
17
|
import type { AppInfo, FAQData, UserProfileDisplay, AdditionalScreen, AccountConfig } from "../navigation/types";
|
|
19
18
|
|
|
20
19
|
export interface SettingsFeatures {
|
|
@@ -36,7 +35,6 @@ export interface UseSettingsScreenConfigParams {
|
|
|
36
35
|
faqData?: FAQData;
|
|
37
36
|
isPremium: boolean;
|
|
38
37
|
onFeedbackSubmit: (data: FeedbackFormData) => Promise<void>;
|
|
39
|
-
featureRequestDataProvider?: FeatureRequestDataProvider;
|
|
40
38
|
additionalScreens?: AdditionalScreen[];
|
|
41
39
|
features?: SettingsFeatures;
|
|
42
40
|
translations?: SettingsTranslations;
|
|
@@ -54,7 +52,7 @@ export interface SettingsScreenConfigResult {
|
|
|
54
52
|
export const useSettingsScreenConfig = (
|
|
55
53
|
params: UseSettingsScreenConfigParams
|
|
56
54
|
): SettingsScreenConfigResult => {
|
|
57
|
-
const { appInfo, faqData, isPremium, onFeedbackSubmit,
|
|
55
|
+
const { appInfo, faqData, isPremium, onFeedbackSubmit, features = {}, translations } = params;
|
|
58
56
|
|
|
59
57
|
const {
|
|
60
58
|
notifications: showNotifications = true,
|
|
@@ -104,15 +102,6 @@ export const useSettingsScreenConfig = (
|
|
|
104
102
|
translations,
|
|
105
103
|
};
|
|
106
104
|
|
|
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
|
-
|
|
116
105
|
if (config.subscription && typeof config.subscription === 'object') {
|
|
117
106
|
config.subscription = {
|
|
118
107
|
...config.subscription,
|
|
@@ -143,7 +132,7 @@ export const useSettingsScreenConfig = (
|
|
|
143
132
|
}
|
|
144
133
|
|
|
145
134
|
return config;
|
|
146
|
-
}, [baseSettingsConfig, translations
|
|
135
|
+
}, [baseSettingsConfig, translations]);
|
|
147
136
|
|
|
148
137
|
const userProfile = useMemo(() => createUserProfileDisplay({
|
|
149
138
|
profileData: userProfileData,
|
|
@@ -133,12 +133,9 @@ 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;
|
|
137
136
|
const featureRequestScreen = createScreenWithProps("FeatureRequest", FeatureRequestScreen, {
|
|
138
137
|
config: {
|
|
139
|
-
...feedbackConfig,
|
|
140
138
|
translations: translations?.feedback,
|
|
141
|
-
dataProvider: feedbackConfig?.dataProvider,
|
|
142
139
|
},
|
|
143
140
|
texts: {
|
|
144
141
|
feedbackTypes: [
|
|
@@ -5,7 +5,6 @@
|
|
|
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";
|
|
9
8
|
import type { FAQCategory } from "../../../domains/faqs/domain/entities/FAQEntity";
|
|
10
9
|
import type { SettingsStackParamList } from "../../navigation/types";
|
|
11
10
|
|
|
@@ -45,8 +44,6 @@ export interface FeedbackConfig {
|
|
|
45
44
|
onSubmit?: (data: { type: FeedbackType; rating: number; description: string; title: string }) => Promise<void>;
|
|
46
45
|
/** Custom handler to open feedback screen (overrides default modal) */
|
|
47
46
|
onPress?: () => void;
|
|
48
|
-
/** Data provider for feature request screen (fetching, voting, submitting) */
|
|
49
|
-
dataProvider?: FeatureRequestDataProvider;
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
/**
|